Wybór podejścia do synchronizacji wiadomości z kolejki zmieniających stan domeny biznesowej.

0

Szukam jak najlepszego rozwiązania do radzenia sobie z równoległymi operacjami na tej samej encji biznesowej w przypadku gdy mam do czynienia z możliwością równoległego przetwarzania wiadomości zmieniających stan danej encji biznesowej.

Powiedzmy, że mamy system A, oparty na kolejce (jako warstwa transportu Apache kafka). Do integracji z frontem używany jest REST API
działającej na zasadzie:

  • przyjmij zadanie
  • zakolejkuj
  • zwróć 202.

Ponadto aplikacja integruje się z innymi mikrousługami w podobny sposób.

Przybliżony problem

  1. Użytkownik inicjalizuje process w systemie A (tworzymy encje biznesowo)
  2. Podczas inicjalizacji procesu, zakładamy asynchroniczne zadania w innych systemach (powiedzmy w systemach B i C)
    3a) Użytkownikowi pozwalamy działać dalej i może uzupełniać dalsze dane (modyfikuje encję biznesowo)
    3b) system A może zacząc przetwarzać wynik zadania z systemu B i C (pobranie z kolejki) i dorzucić te dane do encji biznesowej.
  3. Użytkownik po kroku 3a w systemie A wykonuje akcję, która w zależności od tego czy dane z kroku 3b dojdą czy nie wykona się inaczej (Przy czym dane z systemów B i C i tak finalnie muszą zostać dociągnięte, gdyż będzie miało to finalnie wpływ na dalsze kroki)

Na tym etapie, może się zdarzyć, że operacje 3a z (3b lub 4) nastąpią w tym samym czasie i może dojść do konfliktu.

Większość porad ogranicza się do stosowania albo Optimistic Concurrency albo pessimistic concurrency.

przy czym w wersji:

  1. Optimistic Concurrency - w przypadku wykrycia konfliktu musiałbym zapewnić jakoś, żeby wiadomość jednak została potem jeszcze raz przetworzona po jakimś czasie np. poprzez opublikowanie jeszcze raz wiadomości co trochę przeczy idei tego podejścia, której celem miało być, że w przypadku dwóch zadań, tylko jeden się wykona.
  2. Pessimistic concurrency. - poległo by na zakładaniu rozproszonej blokady (np. przy pomocy redlocka redisa, lub bazy danych) na handlerze wiadomości po wspólnej korelacji dla procesu. Tutaj problem pojawia się taki, że takie podejście raczej będzie miało negatywny wpływ wydajnościowy + powinno się założyć jakiś rozsądny maksymalny czas życia blokady, więc i tak mogłyby nastąpić przypadki, że jedno zadanie po jakimś czasie może niepożądane zacząć się procesować. Plus dochodzi kwestia, że powinno w jakimś ograniczonym czasie albo zatwierdzić albo nie zczytanie wiadomości w kafce inaczej inna instancja aplikacji zacznie czytać wiadomość z danego topica.
  3. Zapewnienie już na kolejce, że wszystkie wiadomości dla jakiejś korelacji by były przetwarzane sekwencyjnie lecz szukając w internecie nic takiego na ten moment nie znalazłem w przypadku kafki.
  4. Może ogólnie nie należy bawić się w blokady a dociągane w tle dane odkładać jakoś na boku a dopiero na jakimś etapie procesu gdzie np. nie będzie możliwości wykonania akcji przez użytkownika wykonać uzupełnienie encji biznesowej, żeby wpadła w ostatecznie spójny stan.

Co o tym myślicie, może znacie lepsze podejście dla takich przypadków?

0

Ja chyba nie do końca rozumiem problem, szczególnie jeśli dane z systemów B i C i tak finalnie muszą zostać dociągnięte. Dlaczego akcja uzytkownika 3a nie zostaje znów jakoś zakolejkowana / wrzucona w jakis handler który blokuje się i czeka aż dane z B i C dojdą i dopiero wtedy realizuje operacje?
Zresztą to w ogóle nie jest pytanie techniczne tylko czysto biznesowe, bo to od wymagań biznesu zależy co się powinno stać w takiej czy innej sytuacji.

0

Lepsze podejscie to no nosql. Są takie zwierzaki jak transakcje... yo

0

Ja tu widzę inne dane.
Te które uzupełnia użytkownik z systemu A i te które są zewnętrze B i C.
Zdefiniuj dwa różne byty. Systemowy (b i c) i ten od usera. Poczekaj aż dojdzie wszystko a później zgodnie z polityka biznesowa dobierz strategie do hierarchizacji tych danych. Czy zrob cokolwiek co powinno się zdarzyć.

0

Rozumiem to tak, że user przechodzi sobie przez jakiś wizard i na końcu albo te dane z B i C dojadą, albo nie. Należy rozważyć pesymistyczny scenariusz - co w sytuacji, kiedy np. system B będzie przeciążony i nie będzie w stanie obsłużyć zadania?

Co zrobisz w tej sytuacji zależy od biznesu tj. napisał @Shalom. Ja bym poszedł w stronę taka, aby puścić usera warunkowo (jemu się wydaje, ze wszystko gra, ma umowę) - swojego rodzaju optimistic lock, ale biznesowy. Dzięki temu jest dobry UX, ale przede wszystkim konwersja ($$$). Pozostałe dane potrzebne do finalizacji procesu trzeba by ogarnąć back-officowo - poczekać aż spłyną dane z systemów B i C albo w przypadku awarii popchnąć to.

0
Charles_Ray napisał(a):

Rozumiem to tak, że user przechodzi sobie przez jakiś wizard i na końcu albo te dane z B i C dojadą, albo nie. Należy rozważyć pesymistyczny scenariusz - co w sytuacji, kiedy np. system B będzie przeciążony i nie będzie w stanie obsłużyć zadania?

Co zrobisz w tej sytuacji zależy od biznesu tj. napisał @Shalom. Ja bym poszedł w stronę taka, aby puścić usera warunkowo (jemu się wydaje, ze wszystko gra, ma umowę) - swojego rodzaju optimistic lock, ale biznesowy. Dzięki temu jest dobry UX, ale przede wszystkim konwersja ($$$). Pozostałe dane potrzebne do finalizacji procesu trzeba by ogarnąć back-officowo - poczekać aż spłyną dane z systemów B i C albo w przypadku awarii popchnąć to.

Komunikacja miedzy systemem A i B, będzie luźna. Jeżeli nie da się zarejestrować zadania w systemie B przez system A, to pewnie na początku staramy się ponowić, jak kilka prób nie powiedzie się, to kończymy proces i powiadamy użytkownika, że wystąpił błąd techniczny i prosimy spróbować później. Natomiast jeżeli przetworzenie już po zarejestrowaniu akcji zakończy się błędem, to zapewnię ma możliwość automatycznych ponowień albo ręcznych. Jak napisałem wcześniej komunikacja miedzy systemami jest dość luźna. System B wrzuca dane wynikowe na kolejkę a system A zbiera z niej i próbuje te dane powiązać z encją biznesową/agregatem.

Jako tako, chyba troszkę źle przedstawiłem swoją myśl wcześniej i nie zrozumieliście jaki mam problem. W mojej kwestii raczej chodzi techniczne sposoby radzenia sobie w sytuacjach, gdy aplikacja zbierze w tym samym czasie dwa wiadomości, które będą modyfikować to samo encje biznesowo, którą powiedzmy, że jest aplikacja/wniosek, w której są trzymane np. dane o kliencie, ofertę, powiedzmy, że jakieś zewnętrzne dane potencjalnego klienta (wyliczone dane z systemu B), które potem będą miały wpływ na dostępność poszczególnych ofert dla klienta czy też ogólnie na to, czy zostanie podpisana umowa.

np. załóżmy, że mamy dwa event handlery w systemie A

  1. UserOfferChangedEventHandler (aplikacji 123)
    a) pobranie agregatu aplikacji z warstwy trwałości.
    b) przeliczenie zdolności klienta dla tej oferty (może trwać od kilku do kilkunastu sekund)
    c) ustawnie na agregacie wyniku przeliczenia
    d) zapis agregatu do bazy.

  2. ExternalCustomerDataDownloadedEventHandler (dla aplikacji 123)
    a) pobranie agregatu aplikacji z warstwy trwałości.
    b) ustawnie na agregacie zewnętrznych danych klienta
    c) zapis agregatu do bazy.

Załóżmy, że dane akcje wykonają się w podanej kolejności:

  • 1a)
  • 2a)
  • 2b)
  • 2c)
  • 1b)
  • 1c)
  • 1d)

W takiej, kolejności bez żadnych zabezpieczeń, może dojść do sytuacji, że dane z handlera 2 zostaną nadpisane i znikną. Jako tako szukam czegoś, co by mogło zastąpić używanie pesymistycznej współbieżności/ rozproszonego locka (czyli na starcie handlera założenie blokady per identyfikator encji biznesowej i jej zwolnienie jak się wykona handler, więc wtedy inny handler jest stopowany do czasu aż blokada nie zostanie zwolniona).

Optimistic concurrency z teorii w takim scenariuszu raczej nie pasuje, gdyż oba handlery chciałbym, żeby się wykonały a nie jeden, choć gdzieś widziałem przykład, użycia Optimistic concurrency na zasadzie:
1 spróbuj wykonać
2a jak konflikt to opublikuj jeszcze raz wiadomość (np. na inny topic, który po n czasie zostanie jeszcze raz przetworzony.)
2b jak nie ma konfliktu to zatwierdź zaczytanie wiadomości.

Zastanawiałem, czy istnieje inny lepszy sposób na poradzenie sobie z tym problemem poza używaniem blokad na rozpoczętym zadaniu lub sprawdzaniu konfliktów i późniejszym ponawianiu. O uszy obiło mi się, że niby RabbitMQ umożliwia zrobienie sekwencyjnego przetwarzania wiadomości na jakieś korelacji i nie wiem czy by coś takiego nie było najlepsze dla mojego przypadku, jeżeli coś takiego by się dało wykonać w miarę małym nakładem pracy przy użyciu kafki. W międzyczasie szukając informacji padł także gdzieś temat Akka, ale nie jestem zaznajomiony z tym tym i ciężko mi powiedzieć czy to w ogóle ma sens do mojego przypadku.

Jako tako zdaje sobie sprawę, że ogólnie te dane zewnętrzne by mogły nie być zapisywane w ramach agregatu wniosku a tak jak wspomniał @DKN traktowane jako oddzielny byt, czy to inny agragate root, czy to w płaskiej strukturze wpis do jakieś tabeli, a kolejne takie dorzucenia danych nie powodowały by modyfikacji istniejących wpisów a nowe wpisy co by zapewniło idempotentność lecz mam narzucony już jakoś strukturę bazy i model domeny i z zmian to mogę głównie zaprojektować sposób integracji i jakieś aspekty techniczne jak zapewnienie spójności.

Także w tym wątku chciałem sprawdzić jak ludzie sobie radzą takimi przypadkami. Czy może pesymistyczna współbieżność jest ok, albo ogólnie nie używa się blokad i jak to możliwe powszechnie stara się, żeby była Idempotentność operacji.

0

"W mojej kwestii raczej chodzi techniczne sposoby radzenia sobie w sytuacjach, gdy aplikacja zbierze w tym samym czasie dwa wiadomości, które będą modyfikować to samo encje biznesowo,"

Takie luźnie pytanie. Zastanawiałeś się dlaczego taka sytuacja występuje? Co gdyby "ExternalCustomerData" było osobnym bytem mającym "wskaźnik" do Customera? Czy miałbyś wtedy race condition na zapisie agregatu klienta?

0

Ad. Optimistic locking - możesz wtedy rzucić błąd i przetworzyć wiadomość ponownie, dlaczego tak nie chcesz?

Ad. @yarel i osobna encja - fajne podejście, może nie potrzebujesz modyfikować tej encji? A jeśli musisz, to kolejna opcja - event sourcing? Wtedy odraczasz aplikowanie zmian do momentu wczytania encji.

Pessimistic lock, np. SELECT FOR UPDATE (SQL) albo findAndModify (Mongo) możesz zrobić na poziomie pojedynczego wiersza/dokumentu - to tez może być ok, bez jakiegoś strasznego podupadku na performance.

1 użytkowników online, w tym zalogowanych: 0, gości: 1