Posts tagged NServiceBus

Probably the most powerful CQRS/Event Sourcing platform in .NET

In the beginning… there were two awesome pieces of code: Ncqrs created by Pieter and Event Store by Jonathan… Now, let me introduce them.

Ncqrs is highly focused on popularizing CQRS and Event Sourcing ideas and, because of that, it is not tied to one particular flavor of CQRS. With Ncqrs you can as well create simple projects backed by RDBMS event store without enforcing aggregate boundaries, as highly scalable reliable solutions without the need of 2PC. The true power of Ncqrs is it’s rich model for representing aggregates. It allows automatic mapping of event handlers using conventions, attributes and via an internal DSL. Ncqrs aggregates can contain entities and nested entities (entities inside entities).

On the other hand, Jonathan’s Event Store is focused on ‘hardcore’ Event Sourcing. The learning curve is high. With Ncqrs you can really write code in the ORMish style. I don’t think you should, but if you can, you have all the facilities, like Unit of Work. You can’t do this with Event Store. This also means that you have smaller chances to create a poorly designed solution. But the feature that makes Event Store a really an awesome tool is support for processing idempotency, integrity and optimistic concurrency based on very loose semantics of most NoSQL databases.

So, why not combine the rich aggregates and asynchronous event processing engine of Ncqrs with storage options (all flavors of SQL, RavenDB, MongoDB), concurrency and idempotency of Event Store? It turned out to be a fairly simple task.

Receiving a command

My first decision was to give up the Ncqrs commanding infrastructure. I wanted to address the scenario where commands are shared resources (e.g. in the enterprise) and they are not allowed to be dependant upon particular framework like Ncqrs (which is an implementation detail of one system). This decision lead me to swap a command service for an interface like this one

public interface IRemoteFacade
{
    void Execute(CommandMetadata metadata, Action<AggregateRoot> commandAction);
}

public class CommandMetadata
{
    public Guid CommandId { get; set; }
    public int? LastKnownRevision { get; set; }
    public Guid? TargetId { get; set; }
    public Type TargetType { get; set; }
}

Instead of forcing command to implement and interface defined by me (or be decorated by my attributes) so that I can handle them in an automatic way, I acknowledge that commands are autonomous beings and that it is handler’s responsibility to extract necessary metadata from the command object. So, why don’t define ICommandHandler interface like this one

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

at this level? At first I thought it would be a good idea. Then I realized that this kind of model is already implemented in most transport-level frameworks like NServiceBus. I don’t want to duplicate their functionality. I’d like transport-level handlers (e.g. IHandleMessage<T> in NServiceBus) to call my IRemoteFacade.

Processing the command

After command metadata is extracted and command is handed over to IRemoteFacade, the real processing begins. First, a new instance of aggregate of specified class is created. Then, if the command is targeted against an existing aggregate, an event stream consisting of all events from the latest snapshot up to the last known revision is retrieved. If no revision is provided in the command (which means that optimistic concurrency is disabled for particular command), event are retrieved until most recent commit.

The state of the aggregate is reconstructed using retrieved events. Then, the action provided in Execute call is invoke against the aggregate object.

The last (but certainly not least) activity is saving the state of the aggregate. Events resulting from processing the command are retrieved from the aggregate object, transformed and pushed into the Event Store. If there were optimistic concurrency violations, exception will be thrown. Also, if Event Store detected duplicate command it will signal this by throwing an exception. It is up to IRemoteFacade caller to catch this exception and (most probably) swallow it.

Integration with NServiceBus

All this stuff would be useless if commands cannot be send to the IRemoteFacade. There has to be some transport mechanism. While I don’t want to tie to a single transport, I want to support some common (in my opinion) scenarios. I decided to add NServiceBus support first.

NServiceBus has a notion of message handlers defined as IHandleMessages<T> interface. OOTB you would have to code the facade calling code by hand. To avoid duplication I prepared a special base class:

public abstract class CommandHandlerBase<TMessage, TAggregateRoot> : IHandleMessages<TMessage>
    where TMessage : IMessage
    where TAggregateRoot : AggregateRoot
{
    public IRemoteFacade RemoteFacade { get; set; }
    public IBus Bus { get; set; }

    public virtual void Handle(TMessage message)
    {
        var metadata = ExtractMetadata(message);
        if (metadata.TargetType == null)
        {
            metadata.TargetType = typeof (TAggregateRoot);
        }
        try
        {
            RemoteFacade.Execute(metadata, x => Handle(message, (TAggregateRoot)x));
        }
        catch (DuplicateCommitException ex)
        {
        }
    }

    protected abstract CommandMetadata ExtractMetadata(TMessage message);
    protected abstract void Handle(TMessage message, TAggregateRoot aggregateRoot);
}

The code is straightforward. All you have to do is create your own code for extracting command metadata (or its envelope) and for invoking particular action on the aggregate root. The Handle method is also a good place to put command validation logic. Now, how can I tell NServiceBus to use all this infrastructure? It is as simple as calling one method:

_bus = Configure.With()
    .Log4Net()
    .DefaultBuilder()
    .XmlSerializer()
    .MsmqTransport()
    .IsTransactional(true)
    .PurgeOnStartup(true)
    .UnicastBus()
    .LoadMessageHandlers()
    .InstallNcqrs()
    .UseInMemoryPersistenceEngine()
    .CreateBus()
    .Start();

The additional UseInMemoryPersistenceEngine method instructs Event Store which storage engine to use.

Summary and what’s next

By combining the strongest points of Ncqrs and Event Store I believe I created the most powerful and universal tool for implementing CQRS in .NET platform. Add NServiceBus for command transport and you have a complete end-to-end solution.

The source code can be downloaded from my fork of Ncqrs on github. It is still a prototype but you can see the power it brings.

The next goal is to integrate it smoothly with asynchronous event processing engine of Ncqrs.

VN:F [1.9.13_1145]
Rating: 5.0/5 (5 votes cast)

Jak zwątpiłem w transakcje

Transakcje to fajna sprawa. Polubiłem je od pierwszego użycia. Zostałem oczarowany przez ich magiczną właściwość — zwalniają z myślenia o spójności danych. Cool, przecież nie lubię myśleć. Jeszcze bardziej byłem oczarowany, gdy odkryłem transakcje rozproszone. To dopiero jazda. Mogę coś “zapdejtować” na tej bazie, na tej drugiej bazie i jeszcze wrzucić komunikat do kolejki MSMQ i wszystko wykona się transakcyjnie — w całości lub wcale.

Od dłuższego czasu zaczynam jednak wątpić w transakcje, szczególnie te rozproszone. Zbyt często ja sam lub ktoś ze znajomych wpada przez nie w pułapkę bez wyjścia. Zamiast używać najodpowiedniejszej technologii, musimy wtedy wziąć taką, która współdziała z naszym sposobem zarządzania transakcjami.

Najczęstszym problemem, na który natrafiałem jest odbieranie i wysyłanie komunikatów do kolejek z jednoczesnym zapisem danych w bazie. Pierwsza rzecz, która przychodzi w takim wypadku do głowy, to transakcja, która obejmuje zarówno infrastrukturę kolejkową, jak i RDBMS. Pierwsza nie oznacza, niestety, najlepsza.

Nieodpowiednia technologia

Wymaganie transakcyjności w komunikacji z RDBMS i kolejkami pchnęło mnie kilka lat temu do wykorzystania SQL Server Service Broker jako kolejki komunikatów. Unikalną cechą Service Broker’a jest fakt, że żyjąc wewnątrz silnika bazodanowego, jest w naturalny sposób zintegrowany z mechanizmami transakcyjnymi SQL Server. Nie ma potrzeby stosowania (wolnych) transakcji rozproszonych, aby wysłać komunikat i zrobić przysłowiowy “apdejt”.

Niestety poza tą jedną zaletą, Service Broker ma bardzo wiele wad, z których największą jest brak dobrego gotowego API w C#, o modelu usługowym (np. WCF) nie wspominając. Jest bardzo trudny w użyciu i utrzymywaniu. Definitywnie był to zły wybór technologiczny, podyktowany jedynie chęcią zastosowania mechanizmu transakcji.

Niezgodność API

W każdym nietrywialnym systemie wykorzystuje się wiele zewnętrznych bibliotek. Bardzo rzadko pochodzą one od jednego dostawcy. Zdecydowanie częściej jest to mieszanka rozwiązań open source oraz COTS. Problem pojawia się, gdy dwie z bibliotek mają współpracować w ramach jednej transakcji. Weźmy jako przykład NHibernate i NServiceBus. Ten pierwszy posiada własną abstrakcję reprezentującą transakcje, podczas gdy pod spodem korzysta z transakcji ADO.NET. Ten drugi wykorzystuje transakcje System.Transactions do dostępu do MSMQ.

W przypadku użycia obu technologii w jednej transakcji, pojawia się problem, jak sprawić, aby każda z bibliotek mogła wykorzystywać swoje API odwołując się do tej samej fizycznej transakcji. W wypadku wspomnianej pary, jedynym rozwiązaniem jest pozwolić NHibernate używać zewnętrznych transakcji System.Transactions. Jest to jednak zamiana jednego problemu na inny. Do niedawna bowiem pojawiał się w NServiceBus wyciek pamięci, ponieważ zachowanie NHibernate w wypadku współpracy z System.Transactions jest bardzo słabo udokumentowane i łatwo o błąd wynikający z niezrozumienia.

Błedne implementacje transakcyjności

Jonathan Oliver opisał swoje testy kompatybilności różnych silników bazodanowych z transakcjami rozproszonymi na swoim blogu. Wnioski nie są optymistyczne: jedynie sterowniki do SQL Server i Oracle w pełni i bez problemów je obsługują.

Z drugiej strony nie dziwie się, że mniej płatne lub darmowe rozwiązania nie wspierają rozproszonych transakcji. Zdecydowana większość systemów radzi sobie bez nich, więc zysk (w sensie pieniędzy z licencji lub satysfakcji użytkowników) z implementacji wsparcia dla nich byłby znikomy.

Proste rozwiązanie

Rozwiązanie jest oczywiście proste. Wymaga jednak nieco innego podejścia do projektowania komunikacji. Wystarczy zadbać o to, aby każdy komunikat był

  • albo idempotentny (wielokrotne przetworzenie takiego komunikatu daje taki sam efekt, jak przetworzenie jednokrotne),
  • albo jednoznacznie identyfikowalny (unikalne ID).

Drugi przypadek można sprowadzić do pierwszego dodając rejestr przetworzonych komunikatów zawierający ich unikalne ID i przed obsłużeniem komunikatu sprawdzać, czy aby nie został przetworzony wcześniej.

Po spełnieniu któregoś z powyższych warunków zyskujemy możliwość rozłącznego zarządzania transakcją związaną z obieraniem komunikatu (MSMQ, ServiceBroker) oraz transakcją bazodanową. Ta pierwsza powinna być zatwierdzana dopiero po zatwierdzeniu tej drugiej. Powoduje to, że mamy pewność, iż każdy komunikat zostanie przetworzony co najmniej raz. Z drugiej strony idempotentność gwarantuje nam, że skutki wielokrotnego przetworzenia będą takie, jak jednokrotnego. Ostatecznie więc uzyskujemy semantykę dokładnie raz — taką samą jak przy zastosowaniu transakcji rozproszonych.

Praktyka

Dokładnie taki mechanizm zastosowałem w swoim ostatnim systemie. Pozwolił mi on na użycie klienta Service Broker (użycie tej kolejki było narzucone z góry) w połączeniu z NHibernate bez konieczności integracji obu technologii. Ponieważ transakcje były rozdzielone, klient kolejek nie musiał wiedzieć nic o dostępie do danych i vice versa. Prawdopodobnie zaoszczędziło mi to kilka dni pracy przy implementacji, testach i poprawianiu bugów w warstwie integracyjnej. Nauczyłem się także, że najlepszą strategią integracji technologii jest unikanie integracji technologii, kiedy to tylko możliwe.

VN:F [1.9.13_1145]
Rating: 5.0/5 (1 vote cast)

Czym jest NServiceBus?

Matt Burton, jeden z deweloperów NServiceBus, zainspirował mnie wczoraj do napisania tej notki zadając pytanie, czy można określić NSB mianem application server. Matt miał problem prowadząc swoje szkolenie z NSB, ponieważ stwierdzenie, że NServiceBus to ESB nie trafiało do słuchaczy.

Nie wiem, jak Wam, ale mnie ESB kojarzy się od razu z wielkim i ciężkim kawałem infrastruktury, który jest centralnym punktem architektury SOA danej organizacji. Pisałem już nieco o tym tutaj, porównując wzorce Message Broker i Message Bus. NSB jest dokładnym przeciwieństwem ESB, jednak w praktyce służy do tego samego. To trochę mylące, więc spróbujmy znaleźć jakąś lepszą analogię.

Niestety stwierdzenie, że NServiceBus to szyna komunikatów (bez odwoływania się do magicznego skrótu ESB) zdaje się nie trafiać do ludzi. Dlaczego? Pewnie dlatego, że w coś takiego jak szyna ciężko sobie wyobrazić w oderwaniu od kontekstu konkretnego wdrożenia. Jeśli widzimy serwerownię z 10 serwerami, na każdym z nich inny system i wszystkie te systemy wykorzystują do komunikacji NSB — to tak, wtedy możemy sobie wyobrazić NServiceBus jako szynę. Ale patrząc w kontekście pojedynczego systemu? Czym jest NSB dla mojego systemu?

I tu dochodzimy do tego, co zaproponował Matt, czyli application server. Serwer aplikacyjny jest platformą, na której działa aplikacja. Musi ona być, oczywiście, napisana w pewien specyficzny sposób — zgodnie z założeniami twórców serwera. W zamian za dochowanie wierności tym założeniom, serwer aplikacyjny oferuje wiele udogodnień, takich jak:

  • wymuszenie spójności architektonicznej (poprzez nałożenie niezbędnych ograniczeń)
  • gotowy do użycia mechanizm komunikacji, pozwalający udostępnić funkcje systemu na zewnątrz
  • gotowe rozwiązania dla problemów związanych z aspektami niefunkcjonalnymi systemu: wydajnością, bezpieczeństwem itp.

Powyższa definicja jest oczywiście moją osobistą. Zgodnie z nią, serwerami aplikacyjnymi są np. (niespodzianka!) dowolny serwer aplikacyjny J2EE, tandem IIS/WCF oraz właśnie NSB.

Serwer J2EE służy do budowania aplikacji będącej kolekcją tzw. beanów. Na zewnątrz mogą być udostępniane session beans (komunikacja synchroniczna) oraz message-driven beans (komunikacja asynchroniczna – JMS).

IIS/WCF pozwala budować systemy jako kolekcję usług Web Service udostępnianych na zewnątrz dowolnym kanałem (WAS).

NServiceBus natomiast zakłada, że aplikacja jest zbiorem tzw. message handlerów — obiektów obsługujących rozmaite komunikaty. Obiekty te są bardzo podobne do message-driven beans, zapewne dlatego, że służą do tego samego celu — obsługi komunikatów pochodzących z kolejki.

To oczywiście bardzo duże uproszczenie. Każda z wymienionych trzech technologii posiada dużo więcej dodatkowych możliwości. Nie o możliwości tu jednak chodzi, ale o charakter, o model, którego dany serwer aplikacyjny używa do reprezentacji hostowanej aplikacji. Dlaczego model jest tak ważny? Spróbujcie hostować za pomocą WCF coś, co nie jest usługą Web Service, np. proces uruchamiany co 5 minut. Coś takiego nie ma reprezentacji w modelu WCF, trzeba więc zrobić jakieś obejścia tu i tam. Podobnie w NServiceBus, gdzie tego typu zadania można zrealizować wysyłając komunikat do tzw. timer service, ale rozwiązaniu takiemu daleko do elegancji. W takim wypadku być może powinniśmy użyć Quartz.NET, jako serwera aplikacji? No i mamy zadanie dla architekta…:-)

VN:F [1.9.13_1145]
Rating: 5.0/5 (2 votes cast)

NHibernate, NServiceBus i transakcje

Dziś chciałbym podzielić się z Wami moimi refleksjami na temat sposobu zarządzania transakcjami w NHibernate, ze szczególnym uwzględnieniem nietrywialnego przypadku, kiedy w ramach jednej transakcji wykorzystujemy zarówno NHibernate, jak i NServiceBus. Posłużę się w tym celu kodem DDDSample.Net.

Aby wprowadzić Was w klimat, przypomnę jak wygląda architektura DDDSample. Począwszy od najwyższego poziomu, występują tam następujące warstwy:

  • WebUI (ASP.NET MVC) – prezentacja danych, interfejs
  • Application – stanowi fasadę dla modelu domeny udostępniając interfejs poszczególnych akcji/komend
  • Domain – logika biznesowa żyje tutaj
  • Domain.Persistence.NHibernate – mapowania NHibernate oraz implementacje repozytoriów

Która warstwa odpowiada zatem za transakcje? Oczywiście Application. Dlaczego? Ponieważ jednostką izolacji są akcje/komendy — każda taka jednostka wykonywana jest w ramach osobnej transakcji.

Ponieważ transakcyjność postrzegam (zwykle) jako jedno z wymagań niefunkcjonalnych, implementuje ją za pomocą aspektów. Korzystam przy tym z możliwości mojego ulubionego kontenera Unity, jednak to samo można zrobić za pomocą niemal dowolnego innego kontenera. Moja warstwa Application składa się par (interfejs, klasa), przy czym interfejs zdefiniowany jest nie po to, aby umożliwić zmianę implementacji, ale tylko i wyłącznie po to, aby umożliwić AOP (tak, wiem, że można to samo osiągnąć za pomocą metod wirtualnych)

public interface IBookingService
{
	TrackingId BookNewCargo(UnLocode origin, UnLocode destination, DateTime arrivalDeadline);
	//...
public class BookingService : IBookingService
{
	public TrackingId BookNewCargo(UnLocode originUnLocode, UnLocode destinationUnLocode, DateTime arrivalDeadline)
	{
		Location origin = _locationRepository.Find(originUnLocode);
		Location destination = _locationRepository.Find(destinationUnLocode);
		//...
Korzystam z tego w kodzie konfigurującym kontener DI:

container.Configure<Interception>()</pre>
	//Ustaw sposób przechwytywania
	.SetInterceptorFor<IBookingService>(new InterfaceInterceptor())
	.SetInterceptorFor<IHandlingEventService>(new InterfaceInterceptor())
	//Dodaj nową "politykę"
	.AddPolicy("Transactions")
	//Wykorzystującą aspekt obsługi transakcji
	.AddCallHandler<TransactionCallHandler>()
	//I podłącz do wszystkich interfejsów z assembly
	.AddMatchingRule(new AssemblyMatchingRule("DDDSample.Application"));</div>
<div>
Dzięki temu każde wywołanie fasad z warstwy Application automatycznie opakowywane jest w transakcje. Pozostaje jeszcze wyjaśnić, jak wygląda wspomniany aspekt. Oto kluczowy kawałek kodu:

Który wykorzystując API sesji kontekstowych tworzy nową transakcję, wywołuje właściwą metodę, po czym, jeśli nie wystąpił żaden wyjątek, zatwierdza transakcję. W przypadku wyjątku transakcja pozostaje niezatwierdzona, a wyjątek (nienaruszony) przelatuje do warstw wyższych.

public class TransactionCallHandler : ICallHandler
{
	private readonly ISessionFactory _sessionFactory;
	public TransactionCallHandler(ISessionFactory sessionFactory)
	{
		_sessionFactory = sessionFactory;
	}
	public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
	{
		using (ITransaction tx = _sessionFactory.GetCurrentSession().BeginTransaction())
		{
			IMethodReturn result = getNext()(input, getNext);
			if (result.Exception == null)
			{
				tx.Commit();
			}
			return result;
		}
	}
	public int Order { get; set;}
}

Bardzo lubię ten kawałek kodu. Niestety nie sprawdza się on w moim ulubionym scenariuszu: NHibernate + NServiceBus. W takim przypadku potrzebuje transakcji rozproszonej System.Transactions. Swego czasu Ayende pisał o tym, że NHibernate “automagicznie” współdziała z System.Transactions. Ostatnio jednak pojawił się w NHibernate koncept ITransactionFactory, którego zadaniem jest (chyba?) poprawa jakości tego współdziałania. Domyślna implementacja fabryki, AdoNetWithDistrubtedTransactionFactory, jak sama nazwa sugeruje wspiera transakcje rozproszone. Niestety nie udało mi się sprawić, aby działała w najprostszym scenariuszu integracji z NServiceBus. Zrezygnowałem więc z tego ficzera i powróciłem do “starego dobrego” AdoNetTransactionFactory (który teraz trzeba sobie skonfigurować samemu). Niezbędna była jednak modyfikacja  mojego TransactionCallHandler:

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
	IMethodReturn result;
	SqlConnection sqlConnection = _sessionFactory.GetCurrentSession().Connection as SqlConnection;
	if (sqlConnection == null)
	{
		throw new NotSupportedException("Only SqlConnection is supported.");
	}
	using (TransactionScope tx = new TransactionScope())
	{
		sqlConnection.EnlistTransaction(Transaction.Current);
		result = getNext()(input, getNext);
		if (result.Exception == null)
		{
			_sessionFactory.GetCurrentSession().Flush();
			tx.Complete();
		}
	}
	sqlConnection.EnlistTransaction(null);
	return result;
}

Zamiast korzystać z API transakcji NHibernate, korzystam bezpośrednio z System.Transactions.  Negatywnym skutkiem tego podejścia jest ograniczenie wspieranych baz do SQLServera 2005/2008. Niestety ADO.NET nie umożliwia niezależnego od sterownika bazy danych wpinania połączeń w transakcje rozproszone.

Pierwsze wywołanie EnlistTransaction wpina aktualne połączenie, na którym działa sesja NHibernate do transakcji rozproszonej. Drugie wywołanie (to z null-em) odłącza połączenie od transakcji. Jeśli transakcja ta nie została wcześniej zatwierdzona (tx.Complete()), zmiany są wycofywane na poziomie bazy danych.

Jest jeszcze jeden szkopuł. NHibernate domyślnie zwalnia połączenia związane z sesją najwcześniej, jak może. Jest to zachowanie optymalne z punktu widzenia wydajności, jednak w przypadku takiego zarządzania transakcjami, niepoprawne. Nie chcemy przecież, aby nasze połączenie, które podłączyliśmy do rozproszonej transakcji, zostało zamknięte. Aby temu zapobiec, musimy do konfiguracji NHibernate dodać następujący wpis:

<property name=connection.release_mode>on_close</property>

Na koniec jeszcze jedna niemiła informacja: powyższy sposób zarządzania transakcjami jest niekompatybilny z cache 2-go poziomu NHibernate. Co to oznacza? Otóż modyfikacje dokonane na danych w ramach tak zrealizowanych transakcji nie zostaną uwzględnione w cache 2-go poziomu. Nie sprawia to jednak problemu, jeśli nie modyfikujemy danych cache-owanych. Można więc próbować obejścia, polegającego na stosowaniu obu pokazanych wersji TransactionCallHandler w zależności od tego, czy transakcje rozproszone są wymagane.

Taka strategia powinna sprawdzać się dobrze, ponieważ wymaganie transakcji rozproszonych jest związane z odbieraniem / wysyłaniem komunikatów NServiceBus, a tego rodzaju akcje nie powinny modyfikować danych słownikowych (które są zwykle cache-owane).

VN:F [1.9.13_1145]
Rating: 5.0/5 (1 vote cast)

NServiceBus w kontekście zwykłej aplikacji – notka sponsorowana

Niniejsza notka została zamówiona przez Procenta za pomocą komentarza do poprzedniej notki. Chcesz zamówić notkę? Zostaw komentarz:)

Jak już napisałem, NServiceBus pozwala budować szyny informacyjne. Jest jednak (w przeciwieństwie do ciężkich rozwiązań ESB) zorientowany na wykorzystanie w pojedynczym systemie. Czym jest system, to już każdy może sobie odpowiedzieć. Może to być jedna mała aplikacja, może być także cały zestaw aplikacji CRM, ERP i innych stanowiących kompleksowy system informatyczny obsługujący np. internetowy sklep z warzywami.

Nie zrozumcie mnie źle, NServiceBus może być użyty do komunikacji z innym systemem nie wykorzystującym NSB, jednak jest to przypadek szczególny. Zdecydowanie lepiej biblioteka ta sprawdza się jako wewnętrzny mechanizm komunikacji dla luźno powiązanego systemu. W takim kontekście NSB może nam zaoferować możliwość osiągnięcia:

  • wysokiej przepustowości
  • wysokiej dostępności
  • lepszego modelowania procesów biznesowych

Jak to możliwe? Zaraz opowiem. Dla niecierpliwych tylko krótka informacja: wszystko to dzięki asynchronicznej komunikacji.

Wysoka przepustowość

Powiedzmy, że system składa się z trzech modułów A, B i C. Tylko moduł A udostępniany jest na zewnątrz, ale moduł ten musi wywołać usługę modułu B, aby wykonać swoją pracę. Moduł B wewnętrznie korzysta zaś z C — ot taki łańcuszek szczęścia. Możemy go zaimplementować dosyć za pomocą usług Web Service korzystając ze wsparcia WCF dla transakcji rozproszonych. Ma to jednak wiele negatywnych konsekwencji, o czym miałem okazję już mówić przy okazji spotkania KGD.NET. Najważniejszymi problemami, z punktu widzenia przepustowości, są:
  • blokowanie zasobów bazodanowych jednocześnie w 3 bazach danych przez transakcje rozproszoną
  • blokowanie wątków oczekujących na odpowiedź Web Service z usług B oraz C

Mając do dyspozycji narzędzia oferowane przez NServiceBus możemy tak zaprojektować system, że moduły A, B i C przesyłają sobie komunikaty zlecające zadania asynchronicznie. Jeśli moduł A do przetworzenia komunikatu żądania potrzebuje informacji X z modułu B, a moduł B produkuje informację X jako wynik swojego przetwarzania, to zwykły schemat request-response:

można zastąpić następującym:

Dzięki temu nikt na nikogo nie czeka. Zarówno zadania (strzałki czerwone), jak i stan (strzałki niebieskie) płyną od modułu do modułu. Jest to analogia taśmy produkcyjnej, podczas gdy rozwiązanie synchroniczne bardziej przypomina pracę rzemieślnika posiadającego dwóch pomocników.

Wysoka dostępność

Popatrzcie jeszcze raz na diagramy sekwencji dla przepustowości. Co się stanie, jeśli moduł C na jakiś czas zostanie wyłączony? Żadne żądanie klienta nie będzie mogło być obsłużone. Pozostałe dwa moduły są kompletnie bezradne i uzależnione od działania modułu C.

A teraz przypadek z NSB. Ponieważ informacje X i Y są przesyłane asynchronicznie do modułów ich potrzebujących i są tam przechowywane lokalnie, brak dostępności modułu C oznacza, że dane będą przez jakiś czas nieco nieaktualne. Taki stan rzeczy jest prawdopodobnie całkowicie akceptowalny. Kiedy tylko moduł C zostanie uruchomiony ponownie, przetworzy zaległe żądania i opublikuje nowe wartości danej Y. Klient niczego nie zauważy.

Lepsze modelowanie procesów biznesowych

Często zdarza się sytuacja, że w systemie wykonujemy na tych samych obiektach kolejno kilka zadań: A, B, C. Pomyślne zakończenie zadania A umożliwia wykonanie B i tak dalej. Często realizuje się to za pomocą kilku wątków, które okresowo odpytują bazę danych czy czasem nie istnieją jakieś obiekty w stanie do wykonania zadania X i jeśli istnieją, wykonują zadanie. Dla programistów takie podejście jest oczywiste. Dla ludzi biznesu nie koniecznie. W ich języku wymaganie to można zapisać jako: wykonanie zadania A powoduje rozpoczęcie zadania B. Stosując wspomniane rozwiązanie gubimy po drodze ten związek przyczynowo-skutkowy.

Po co nam tutaj NServiceBus? Otóż zamiast wyszukiwać obiekty w określonym “stanie” można, po pomyślnym wykonaniu zadania publikować na szynie NSB zdarzenie zadanie X wykonane pomyślnie. Co z tego, że jedynym subskrybentem zdarzenia będzie ten sam system, który je publikuje? W niczym to nie przeszkadza, a za to sprawia, że kod nareszcie przypomina biznes, który wspiera i automatyzuje.

VN:F [1.9.13_1145]
Rating: 0.0/5 (0 votes cast)