Jak pozwolić NHibernate ponowić próby zablokowanych transakcji podczas korzystania z sesji na żądanie?

Jakiego wzorca / architektury używasz w 3-warstwowej aplikacji używającej NHibernate, która musi obsługiwać powtórki w przypadku niepowodzeń transakcji, gdy używasz wzorca Session-Per-Request? (ponieważ ISession staje się nieważny po wyjątku, nawet jeśli jest to impas, timeout lub wyjątek livelock).

Author: Daniel A. White, 2010-10-25

1 answers

Uwaga 2 W dzisiejszych czasach nigdy nie umieszczałbym write-transactions wewnątrz projektu www - ale zamiast tego używałbym messaging + queues i mieć workera w tle obsługującego wiadomości mające na celu wykonanie transakcji.

Chciałbym jednak nadal używać transakcji do odczytu, aby uzyskać spójne dane; wraz z izolacją MVCC/Snapshot, z projektów internetowych. W takim przypadku przekonasz się, że session-per-request-per-transaction jest całkowicie w porządku.

Uwaga 1 pomysły tego postu zostały umieszczone w Castle Transactions framework i moim nowym NHibernate Facility.

Ok, oto ogólny pomysł. Załóżmy, że chcesz utworzyć niezrealizowane zamówienie dla klienta. Na przykład przeglądarka / aplikacja MVC, które tworzą nową strukturę danych z odpowiednimi informacjami (lub otrzymujesz tę strukturę danych z sieci): {]}

[Serializable]
class CreateOrder /*: IMessage*/
{
    // immutable
    private readonly string _CustomerName;
    private readonly decimal _Total;
    private readonly Guid _CustomerId;

    public CreateOrder(string customerName, decimal total, Guid customerId)
    {
        _CustomerName = customerName;
        _Total = total;
        _CustomerId = customerId;
    }

    // put ProtoBuf attribute
    public string CustomerName
    {
        get { return _CustomerName; }
    }

    // put ProtoBuf attribute
    public decimal Total
    {
        get { return _Total; }
    }

    // put ProtoBuf attribute
    public Guid CustomerId
    {
        get { return _CustomerId; }
    }
}
Potrzebujesz czegoś, żeby się tym zająć. Prawdopodobnie byłoby to polecenie handler w jakimś autobusie służbowym. Słowo "Obsługa poleceń" jest jednym z wielu i równie dobrze możesz nazwać je "usługą" lub "usługą domeny" lub "obsługą wiadomości". Jeśli zajmowałbyś się programowaniem funkcjonalnym, to byłaby to twoja implementacja skrzynki z wiadomościami, lub gdybyś robił Erlanga lub Akkę, byłby to aktor.
class CreateOrderHandler : IHandle<CreateOrder>
{
    public void Handle(CreateOrder command)
    {
        With.Policy(IoC.Resolve<ISession>, s => s.BeginTransaction(), s =>
        {
            var potentialCustomer = s.Get<PotentialCustomer>(command.CustomerId);
            potentialCustomer.CreateOrder(command.Total);
            return potentialCustomer;
        }, RetryPolicies.ExponentialBackOff.RetryOnLivelockAndDeadlock(3));
    }
}

interface IHandle<T> /* where T : IMessage */
{
    void Handle(T command);
}

Powyższe pokazuje użycie API, które możesz wybrać dla danej domeny problemu (stan aplikacji/Obsługa transakcji).

Realizacja Z:

static class With
{
    internal static void Policy(Func<ISession> getSession,
                                       Func<ISession, ITransaction> getTransaction,
                                       Func<ISession, EntityBase /* abstract 'entity' base class */> executeAction,
                                       IRetryPolicy policy)
    {
        //http://fabiomaulo.blogspot.com/2009/06/improving-ado-exception-management-in.html

        while (true)
        {
            using (var session = getSession())
            using (var t = getTransaction(session))
            {
                var entity = executeAction(session);
                try
                {
                    // we might not always want to update; have another level of indirection if you wish
                    session.Update(entity);
                    t.Commit();
                    break; // we're done, stop looping
                }
                catch (ADOException e)
                {
                    // need to clear 2nd level cache, or we'll get 'entity associated with another ISession'-exception

                    // but the session is now broken in all other regards will will throw exceptions
                    // if you prod it in any other way
                    session.Evict(entity);

                    if (!t.WasRolledBack) t.Rollback(); // will back our transaction

                    // this would need to be through another level of indirection if you support more databases
                    var dbException = ADOExceptionHelper.ExtractDbException(e) as SqlException;

                    if (policy.PerformRetry(dbException)) continue;
                    throw; // otherwise, we stop by throwing the exception back up the layers
                }
            }
        }
    }
}

Jak widzisz, potrzebujemy nowej jednostki pracy; ISession za każdym razem, gdy coś pójdzie nie tak. Dlatego pętla znajduje się na zewnątrz poleceń/bloków. Posiadanie funkcji jest równoznaczne z posiadaniem instancji fabrycznych, z tym że wywołujemy bezpośrednio instancję obiektu, a nie wywołujemy na niej metodę. To sprawia, że dla milszego rozmówcy-API imho.

Chcemy dość sprawnej obsługi tego, jak wykonujemy powtórki, więc mamy interfejs, który może być zaimplementowany przez różni opiekunowie, zwani IRetryHandler. Powinno być możliwe łańcuchowanie ich dla każdego aspektu (tak, jest bardzo blisko AOP), który chcesz wyegzekwować przepływ sterowania. Podobnie jak działa AOP, wartość zwracana jest używana do sterowania przepływem sterowania, ale tylko w sposób prawda / fałsz, co jest naszym wymogiem.

interface IRetryPolicy
{
    bool PerformRetry(SqlException ex);
}
/ Align = "left" / To jest to, co byś mapował ze swoim *.hbm.pliki xml / FluentNHibernate.

Ma metodę, która odpowiada 1: 1 z wysłanym poleceniem. To sprawia, że programy obsługi poleceń są całkowicie oczywiste do odczytania.

Ponadto, z dynamicznym językiem z pisaniem kaczek, pozwoli Ci mapować nazwy typów poleceń do metod, podobnie jak robi to Ruby / Smalltalk.

Jeśli zajmowałeś się pozyskiwaniem zdarzeń, obsługa transakcji byłaby podobna, z wyjątkiem transakcji, która nie byłaby zgodna z NHibernate. Następstwem jest zapisanie zdarzeń utworzonych przez wywołanie CreateOrder (decimal) i udostępnij obiektowi mechanizm ponownego odczytu zapisanych zdarzeń ze sklepu.

Ostatnią rzeczą, którą należy zauważyć jest to, że nadpisuję trzy metody, które stworzyłem. Jest to wymóg ze strony NHibernate, ponieważ potrzebuje sposobu na poznanie, kiedy jednostka jest równa innej, jeśli znajduje się w zestawach/workach. Więcej o mojej realizacji tutaj . W każdym razie jest to przykładowy kod i nie obchodzi mnie teraz mój klient, więc nie wdrażam oni:

sealed class PotentialCustomer : EntityBase
{
    public void CreateOrder(decimal total)
    {
        // validate total
        // run business rules

        // create event, save into event sourced queue as transient event
        // update private state
    }

    public override bool IsTransient() { throw new NotImplementedException(); }
    protected override int GetTransientHashCode() { throw new NotImplementedException(); }
    protected override int GetNonTransientHashCode() { throw new NotImplementedException(); }
}

Potrzebujemy metody tworzenia polityki retry. Oczywiście możemy to zrobić na wiele sposobów. Tutaj łączę płynny interfejs z instancją tego samego obiektu tego samego typu, co typ metody statycznej. Implementuję interfejs wprost tak, aby w interfejsie fluent nie były widoczne żadne inne metody. Ten interfejs wykorzystuje tylko moje' przykładowe ' implementacje poniżej.

internal class RetryPolicies : INonConfiguredPolicy
{
    private readonly IRetryPolicy _Policy;

    private RetryPolicies(IRetryPolicy policy)
    {
        if (policy == null) throw new ArgumentNullException("policy");
        _Policy = policy;
    }

    public static readonly INonConfiguredPolicy ExponentialBackOff =
        new RetryPolicies(new ExponentialBackOffPolicy(TimeSpan.FromMilliseconds(200)));

    IRetryPolicy INonConfiguredPolicy.RetryOnLivelockAndDeadlock(int retries)
    {
        return new ChainingPolicy(new[] {new SqlServerRetryPolicy(retries), _Policy});
    }
}

Potrzebujemy interfejsu do częściowo całkowitego wywołania do płynnego interfejs. To daje nam bezpieczeństwo. Potrzebujemy zatem dwóch operatorów dereferencji (np.' full stop' -- (.)), z dala od naszego typu statycznego, przed zakończeniem konfigurowania polityki.

internal interface INonConfiguredPolicy
{
    IRetryPolicy RetryOnLivelockAndDeadlock(int retries);
}

Polityka łączenia może zostać rozwiązana. Jego implementacja sprawdza, czy wszystkie jego dzieci powracają dalej, a ponieważ to sprawdza, wykonuje również w nich logikę.

internal class ChainingPolicy : IRetryPolicy
{
    private readonly IEnumerable<IRetryPolicy> _Policies;

    public ChainingPolicy(IEnumerable<IRetryPolicy> policies)
    {
        if (policies == null) throw new ArgumentNullException("policies");
        _Policies = policies;
    }

    public bool PerformRetry(SqlException ex)
    {
        return _Policies.Aggregate(true, (val, policy) => val && policy.PerformRetry(ex));
    }
}

Ta zasada pozwala bieżącemu wątkowi na uśpienie pewnego czasu; czasami baza danych jest przeciążona i ma wiele czytelnicy / pisarze ciągle próbujący czytać byłby de facto DOS-atakiem na bazę danych (zobacz, co się stało kilka miesięcy temu, gdy Facebook rozbił się, ponieważ ich serwery cache wszystkie zapytały ich bazy danych w tym samym czasie).

internal class ExponentialBackOffPolicy : IRetryPolicy
{
    private readonly TimeSpan _MaxWait;
    private TimeSpan _CurrentWait = TimeSpan.Zero; // initially, don't wait

    public ExponentialBackOffPolicy(TimeSpan maxWait)
    {
        _MaxWait = maxWait;
    }

    public bool PerformRetry(SqlException ex)
    {
        Thread.Sleep(_CurrentWait);
        _CurrentWait = _CurrentWait == TimeSpan.Zero ? TimeSpan.FromMilliseconds(20) : _CurrentWait + _CurrentWait;
        return _CurrentWait <= _MaxWait;
    }
}

Podobnie w każdym dobrym systemie opartym na SQL musimy poradzić sobie z blokadami. Nie możemy tak naprawdę zaplanować tych dogłębnych, szczególnie podczas korzystania z NHibernate, innych niż utrzymywanie Ścisłej Polityki transakcyjnej-żadnych ukrytych transakcji; i bądź ostrożny z Open-Session-In-View . Istnieje również problem produktu kartezjańskiego/N + 1 wybiera problem, o którym musisz pamiętać, jeśli pobierasz dużo danych. Zamiast tego możesz mieć wiele zapytań lub słowo kluczowe "fetch" HQL.

internal class SqlServerRetryPolicy : IRetryPolicy
{
    private int _Tries;
    private readonly int _CutOffPoint;

    public SqlServerRetryPolicy(int cutOffPoint)
    {
        if (cutOffPoint < 1) throw new ArgumentOutOfRangeException("cutOffPoint");
        _CutOffPoint = cutOffPoint;
    }

    public bool PerformRetry(SqlException ex)
    {
        if (ex == null) throw new ArgumentNullException("ex");
        // checks the ErrorCode property on the SqlException
        return SqlServerExceptions.IsThisADeadlock(ex) && ++_Tries < _CutOffPoint;
    }
}

Klasa pomocnicza poprawiająca odczyt kodu.

internal static class SqlServerExceptions
{
    public static bool IsThisADeadlock(SqlException realException)
    {
        return realException.ErrorCode == 1205;
    }
}

Nie zapomnij również poradzić sobie z awariami sieci w IConnectionFactory (delegując, być może, implementując IConnection).


PS: Session-per-request is a złamany wzór, jeśli nie tylko czytasz. Zwłaszcza jeśli czytasz to samo, co piszesz i nie zamawiasz czytań tak, aby były wszystkie, zawsze, przed zapisem.

 34
Author: Henrik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-12 00:46:59