Simple Event Store - działa na produkcji

ses, eventsourcing

Troszkę mi to zajęło. Od kilku dni Simple Event Store (w skrócie SES) działa w środowisku produkcyjnym. Prace co prawda jeszcze w pełni nie zakończone, ale podstawowa funkcjonalność już działa i to całkiem nieźle (o tym będzie w innym poście).

SES jest dosyć prostą implementacją, która nie zrywa czapek z głów jeśli chodzi o prędkość. Jego zaleta to niski próg wejścia oraz łatwość w użyciu.

Co najważniejsze SES to nie jest framework. SES to biblioteczka, która wspomaga, porządkuje i ukrywa cały kod infrastrukturalny dając developerowi możliwość skupienia się nad samą logiką biznesową. SES jako odrębna biblioteka powstał w wyniku wyjęcia infrastrukturalnego kodu z całego projektu.

Instalacja

SES można pobrać bezpośrednio z githuba i skompilować samemu lub zainstalować bezpośrednio z serwera nuget.

SimpleEventStore.Abstracts

NuGet Status

Tu są zgromadzone wszystkie interfejsy oraz pomocnicze implementacje, które są potrzebne do działania zarówno podstawowego komponentu SimpleEventStore jak i SimpleEventStore.Subscriptions.

SimpleEventStore

NuGet Status

Podstawowa implementacja, która implementuje główny interfejs do zarządzania SES, czyli IEventStore.

SimpleEventStore.MsSql

NuGet Status

Implementacja SimpleEventStore dla bazy danych MS Sql Server.

SimpleEventStore.Domain

NuGet Status

Podstawowe interfejsy i implementacje dla obiektów tworzonej domeny biznesowej takich jak: Aggregate, Repository czy ValueObject. Biblioteka ta nie jest potrzebna do działania SES, ale znacznie ułatwia posługiwanie się nim.

SimpleEventStore.Subscriptions

NuGet Status

SimpleEventSore.Subscriptions automatyzuje dla nas proces czytania logu ze zdarzeniami. Zajmuje się wywoływaniem handlerów, a także utrzymuje stan ostatnio przetworzonych danych.

SimpleEventStore.Subscriptions.MsSql

NuGet Status

Implementacja SimpleEventStore.Subscriptions dla bazy danych MS Sql Server.

Możesz zainstalować wszystkie paczki wykonując pokolei poniższe linie w okienku Package Manager Console.

Podstawowe paczki pozwalające na zapis zdarzeń do bazy:

    Install-Package SimpleEventStore.Abstracts
    Install-Package SimpleEventStore
    Install-Package SimpleEventStore.MsSql

Paczki pozwalające na zautomatyzowane przetwarzanie zdarzeń z bazy:

    Install-Package SimpleEventStore.Subscriptions
    Install-Package SimpleEventStore.Subscriptions.MsSql

W kolejnych postach opiszę dokładniej jak wygląda proces konfiguracji oraz podam przykładowe przypadki użycia. Stay tuned... :)

Dodanie parametru table-valued spowalnia wykonanie zapytania

sql, optymalizacja, ses, eventsourcing

Próba optymalizacji

Podczas implementacji Ses.MsSql chciałem jak najbardziej zoptymalizować zapis danych do bazy. Jedną ze znanych metod jest zmniejszenie ilości odwołań do serwera bazy danych.

Kiedy pojawia się więcej niż jeden rekord do zapisania standardowo zapisujemy w pętli jeden po drugim. Niestety każdy zapis to wywołanie zapytania typu INSERT, czyli strzał do bazy. A gdyby tak wysłać do serwera wszystkie rekordy naraz?

MS SQL Server od wersji 2008 posiada taką możliwość. Możemy zdefiniować własny typ, na przykład:

CREATE TYPE dbo.NewEvents AS TABLE (
    [Version] INT NOT NULL,
    [ContractName] NVARCHAR(225) NOT NULL,
    [Payload] VARBINARY(MAX) NOT NULL
)

Teraz kawałek SQLa, w którym skorzystamy z naszego nowego typu:

--DECLARE @Events NewEvents

INSERT INTO [Streams]([StreamId],[CommitId],[Version],[ContractName],[Payload],[CreatedAtUtc])
SELECT
    @StreamId,
    @CommitId,
    [Version],
    [ContractName],
    [Payload],
    @CreatedAtUtc
FROM
    @Events
ORDER BY
    [Version];

Taka konstrukcja pozwala na wrzucenie do tabeli Streams wszystkich rekordów przesłanych w strukturze NewEvents. Pokazany powyżej przykład zapytania to tylko wycinek procedury, która odpowiedzialna jest za dodawanie nowych zdarzeń do bazy projektu Ses.MsSql.

No dobra, mamy załatwioną część po stronie sqla. Teraz kod c#:

await cmd
    .AddInputParam(SqlQueries.InsertEvents.ParamStreamId, DbType.Guid, streamId)
    .AddInputParam(SqlQueries.InsertEvents.ParamCommitId, DbType.Guid, commitId)
    .AddInputParam(SqlQueries.InsertEvents.ParamCreatedAtUtc, DbType.DateTime, DateTime.UtcNow)
    .AddInputParam(SqlQueries.InsertEvents.ParamMetadataPayload, DbType.Binary, metadata, true)
    .AddInputParam(SqlQueries.InsertEvents.ParamIsLockable, DbType.Boolean, isLockable)
    .AddInputParam(SqlQueries.InsertEvents.ParamEvents, records)
    .ExecuteNonQueryAsync(cancellationToken)
    .ConfigureAwait(false);

gdzie records to poprostu:

IEnumerable<SqlDataRecord>

Typ SqlDataRecord został dodany do framework.net już w wersji 2.0.

Pokazany powyżej przykład daje (powinien dać) nam taką przewagę nad pojedynczym insertem, że przesłanie jednego czy wielu rekordów nie powinno zwiększać czasu wykonania (przynajmniej w pewnej skali).

I faktycznie tak jest. Czy przesyłam 1, 10 czy 100 rekordów to nadal czasy wykonania są podobne - zachowuje pewną liniowość.

Fiasko

Niestety kiedy wykonałem pomiary dla procedury bez parametru typu table-valued to wyszło mi, że cała konstrukcja wykonuje się ponad 9 razy szybciej. Problem opisywałem już jakiś czas temu na SO. Do tej pory nie znalazłem rozwiązania.

Wnioski

Taka optymalizacja przynajmniej w tym przypadku traci jakikolwiek sens.

Co więcej, większość biznesowych operacji generuje zazwyczaj jedno (dwa) zdarzenia. Dla takiej ilości wystarczająco wydajnym mechanizmem jest zapis zdarzeń rekord po rekordzie, a na dodatek w przypadku wystąpienia jakiegokolwiek błędu (błąd wersji agregatu - concurrency) od razu dostajemy informację, na którym zdarzeniu się to stało i możemy bezpośrednio podjąć odpowiednie kroki. O czym w kolejnych wpisach.

SES - Podstawowe interfejsy

ses, eventstore, eventsourcing

Dzisiaj zrobiłem kilka zmian, choć nie wiele, bo miałem na głowie inną pilną robotę. Wieczorkiem usiadłem i w sumie nie wszystko co wcześniej napisałem mi się podoba. Jutro zatem duże zmiany.

Mimo to jest kilka rzeczy, które w miarę się krystalizują i prawdopodobnie w tej postaci zostaną zachowane.

IEventStore

Podstawowy interface całej biblioteki, czyli IEventStore.

public interface IEventStore
{
    Task<IReadOnlyEventStream> Load(
        Guid streamId,
        bool pessimisticLock,
        CancellationToken cancellationToken = default(CancellationToken));

    Task SaveChanges(
        Guid streamId,
        int expectedVersion,
        IEventStream stream,
        CancellationToken cancellationToken = default(CancellationToken));
}

Definiuje tylko dwie metody:

  • Load - wczytuje dane z bazy danych
  • SaveChanges - zapisuje nowe zdarzenia do bazy danych

Oczywiście pod nimi kryje się troszkę logiki takiej jak serializacja/deserializacja, konwersja do nowszej wersji, rozwiązywanie konfliktów w przypadku wykrycia kolizji wersji, itd.

IEvent (i IMemento)

Zdarzenie marker, ale ułatwia kilka rzeczy. Po pierwsze w łatwy sposób mogę posługiwać się snapshotem, gdzie jeśli istnieje to zawsze będzie pierwszym elementem w liście odczytanych zdarzeń, bo IMemento dziedziczy po IEvent.

Dzięki temu mogę zrobić tak:

var events = await _settings.Persistor.Load(streamId, pessimisticLock);
var snapshot = events[0] as IMemento;
var currentVersion = snapshot?.Version + events.Count ?? events.Count;
return new ReadOnlyEventStream(events, currentVersion);

Słowem wyjaśnienia. Ładujemy zdarzenia. Sprawdzamy, czy pierwszy element to IMemento. Jeśli tak to znaczy, że mamy załadowany snapshot, a zdarzenia są tylko te, które mają wersję od niego starszą. Jeśli nie jest to IMemento to znaczy, że mamy załadowane wszystkie zdarzenia dla danego streamId.

Takie podejście przekłada się także na to jak będzie wyglądała implementacja IAggregate, ale o tym innym razem.

IReadOnlyEventStream

Definuje obiekt, w którym dostaniemy w całości załadowane dane dla danego streamId.

public interface IReadOnlyEventStream
{
    IReadOnlyList<IEvent> Events { get; } // Lista zdarzeń
    int CurrentVersion { get; } // Bieżąca wersja w bazie danych
}

IEventStream

W odróżnieniu od poprzedniego interfejsu IEventStream definiuje nam obiekt, który wysyłamy do eventstore nowe zdarzenia, aby zapisał je do bazy danych.

public interface IEventStream
{
    Guid CommitId { get; } // Identyfikator zmiany
    IEvent[] Events { get; } // Lista nowych zdarzeń
    IDictionary<string, object> Metadata { get; set; } // Metadane, związane z zapisywanymi zdarzeniami
}

PesimissticLock

Pewnie intryguje Was ten parametr. I słusznie. W sumie to się zastanawiam czy nie powinienem zmienić, aby miał domyślną wartość na false. A skąd w ogóle taki pomysł, aby coś takiego umieszczać w EventStore? O tym w kolejnym wpisie. :)

Simple Event Store

ses, eventstore, eventsourcing

Właśnie umieściłem na githubie zarys projektu SimpleEventStore (w skrócie SES). W założeniach ma to być biblioteczka (library - not framework) wspomagająca tworzenie aplikacji opartych o eventsouring (ES), gdzie dane są przechowywane w relacyjnej bazie danych. Na początek w podstawowej implementacji MS SQL Server.

Główne założenia do projektu:

  • całe API asynchroniczne (async/await)
  • bez dodatkowych zależności
  • możliwość użycia dowolnego silnika relacyjnej bazy danych
  • wsparcie dla optymistycznego (i pesymistycznego dla szczególnych przypadków) blokowania
  • możliwość użycia dowolnej biblioteki do logowania dzięki interfejsowi ILogger
  • możliwość użycia dowolnej biblioteki do serializacji/deserializacji danych dzięki interfejsowi ISerializer
  • automatyczna konwersja zdarzeń do nowszej wersji
  • wbudowany i łatwo podmienialny mechanizm subskrypcji
  • idempotentny subskrybent z automatu

Dodatkową zaletą ma być odczyt zdarzeń przy zachowaniu porządku zapisu. Przedstawię to na następującym przykładzie. Załóżmy, że klient rejestruje się w naszej aplikacji. Z punktu widzenia logiki biznesowej ważne jest, aby wiedzieć, że klient się zarejestrował (jedno zdarzenie) oraz dodatkową informacją jest to, że zaakceptował możliwość odbierania informacji handlowych w postaci newslettera (drugie zdarzenie).

Nasz agregat (User) wyemituje zatem dwa zdarzenia:

  • UserRegistered
  • UserNewsletterAccepted

SimpleEventStore w podstawowej implementacji (czyli dla MS SQL Server) będzie gwarantował, że zapisane zdarzenia będą mogły być przetwarzane przez subskrybentów zawsze w tej samej kolejności. Nie będzie możliwa sytuacja, że system będzie chciał oznaczać możliwość wysyłania newslettera do niezarejestrowanego jeszcze użytkownika.

Przetwarzanie zdarzeń bez zachowania kolejności budzi wiele problemów. Ten temat powraca jak bumerang na łamach grupy DDD/CQRS/ES.

Wszystko to co powyżej zostało określone w założeniach da się wykonać dzięki temu, że relacyjne bazy danych wspierają transakcje. To bardzo ułatwia. :)

Poniżej przykładowy (oczekiwany) kod obrazujący api SES:

var options = new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    var id = SequentalGuid.NewGuid();

    var aggregate = new ShoppingCart();
    aggregate.AddItem(SequentalGuid.NewGuid(), name: "Product 1", quantity: 3);


    var stream = new EventStream(id)

    // Appending events
    stream.Append(aggregate.TakeUncommittedEvents());

    // Adding metadata item (key, value)
    stream.Advanced.AddMetadata("RequestIP", "0.0.0.0");
    stream.Advanced.AddMetadata("User", "John Doe");

    await store.SaveChanges(stream);

    await scope.Complete();
}

A teraz z użyciem Repository, który skrzętnie ukrywa niepotrzebne szczegóły z punktu widzenia obsługi logiki samej aplikacji:

using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    using (var repo = new SourcedRepo<ShoppingCart>(store))
    {
        var aggregate = await repo.Load(id);

        aggregate.AddItem(Guid.NewGuid(), "Product 1", 3);

        await repo.SaveChanges(aggregate);
    }
    scope.Complete();
}

Na potrzeby jednego z projektów popełniłem już podobne rozwiązanie. Niestety wcześniej zbyt mocno wzorowałem się na NEventStore. Nie wpłynęło to dobrze na prawidłową implementację wcześniejszego rozwiązania. Zresztą jeden z commiterów NES także sam zauważył, że NES poszedł w złym kierunku.

Na pierwszy rzut oka można sobie pomyśleć 'to nie będzie działać wydajnie'. Nic bardziej mylnego. W poprzednim rozwiązaniu na moim i5 (dysk SSD) spokojnie dało się wyciągnąć ponad 6000 msg/sec. Jak na mój gust całkiem nieźle.

Przesiadka z Wordpressa na Pretzel

blog

Przesiadka z Wordpressa na Pretzel

Dzisiaj postanowiłem coś zrobić ze swoim blogiem. Podoba mi się prostota statycznych generatorów. Pełna kontrola nad wypluwanym kodem html. Do tego łatwość modyfikacji oraz szybkość działania. Wszystko to bez żadnej bazy danych. Pisanie nowego posta to po prostu dodanie nowego pliku.

Nie ma oczywiście tu żadnego edytora. Do edycji Twoich postów może być każdy program, który potrafi edytować pliki tekstowe. Ten tekst piszę w Visual Studio Code, ale równie dobrze mogę użyć Worda wraz ze sprawdzaniem składni. :)

Najlepsze jest to, że większość statycznych generatorów obsługuje Markdown. Prostota tego rozwiązania powala i daje niesamowitą elastyczność w pisaniu. Używając Wordpressa musiałem pokombinować co i jak, aby sklecić cały tekst wraz z obrazkami, wstawkami kodu, itd. Tutaj wklejam czysty kod z notatnika i niemalże po kilku małych poprawkach mam sformatowany post.

Pretzel

Sam Pretzel to statyczny generator stron html, który działa na takiej samej zasadzie jak Jekyll, ale jest napisany w c#. Projekt jest open-source i można go znaleźć na githubie.

Składnia taka sama jak w Jekyll, do tego Markdown i w sumie nic więcej do szczęścia nie potrzeba.

Publikacja nowego posta wymaga uruchomienia generatora z linii komend posługując się poniższą (dowcipną) składnią:

    pretzel bake blog.softio.pl

Po wygenerowaniu w odpowiednim katalogu otrzymujemy pliki wynikowe, które wystarczy, że wgramy (zwykłe copy) na nasz serwer.

Jeśli ktoś zastanawia się nad zmianą platformy to szczerze polecam. Na prosty blog dla deva w zupełności wystarczy.