Clean architecture - zadania w tle (cykliczne odpytywanie zewnętrznego serwisu)

1

Jak byście zaimplementowali cykliczne odpytywanie zewnętrznego serwisu stosując clean architecture?

Przez clean architecture mam na myśli to:
screenshot-20210823185628.png

Przykładowy scenariusz:

  1. Użytkownik przesyła do mojej aplikacji dokument
  2. Zapisuję dokument u siebie, a następnie przesyłam go do zewnętrznego serwisu do dalszego procesowania (przetwarzanie asynchroniczne)
  3. Po przetworzeniu przez zewnętrzny serwis chcę pobrać z niego informację o wyniku przetwarzania - serwis nie udostępnia powiadomień/webhooków po swojej stronie, więc muszę zrobić polling. Po przetworzeniu przez zewnętrzny serwis chcę wykonać coś dodatkowego u siebie, poinformować użytkownika o zakończeniu procesowania itp.

To jakie dokumenty wysłałem do przetwarzania i nie dostałem jeszcze odpowiedzi mam zapisane w jakiejś bazie.

Zastanawiam się, w którym miejscu powinienem zrobić to odpytywanie.

Pierwsza myśl to w warstwie Application/Core (czyli UseCases na schemacie):
Zakładając że ta część nie ma zależności do frameworków to musiałbym zdefiniować sobie jakiś interfejs typu IBackgroundJob z polem określającym jak często taki Job ma się odpalać w tle i w warstwie infrastructure zaimplementować jakiegoś JobRunnera (czy to będzie pod spodem jakiś Hangfire, Quartz, czy zwykły BackgroundService z .NET Core to nie ma już znaczenia).
No i teraz w tej warstwie Core mogę sobie zaimplementować coś w stylu DocumentProcessingStatusChecker : IBackgroundJob który zgodnie z jakimś interwałem pobiera z bazy dokumenty dla których nie mam jeszcze wyników (i których nie odpytywałem o status w ostatnich x minutach) i odpytuje zewnętrzne api o status. Jak jest wynik to coś sobie aktualizuję na modelu i zapisuje do bazy.

Druga opcja to implementacja odpytywania bezpośrednio w warstwie Infrastructure:
Tutaj nie muszę się bawić w jakieś dodatkowe interfejsy tylko w najprostszym przypadku implementuję takiego joba bezpośrednio jako BackgroundService z .NET Core. W tej warstwie mam dostęp do DbContextu, czy tam implementacje innych repozytoriów jeśli mam jakiegoś NoSQLa więc też bezpośrednio czytam z bazy to co mi trzeba. Odpytałem zewnętrzny serwis i mam wynik procesowania. Teraz muszę to jakoś przekazać do warstwy Core, więc definiuję tam nowy UseCase na aktualizowanie statusu przetwarzania (albo CommandHandler jak używam mediatr) i go wywołuję z warstwy infrastructure.

W tym przypadku odpytywanie nie bardzo wiąże mi się z szeroko rozumianym Corem mojej aplikacji, bo to trochę taki szczegół implementacyjny - gdyby ich api obsługiwało powiadomienia to w Core miałbym tylko jakiś UseCase do aktualizacji statusu tak jak w scenariuszu 2 - więc może jednak druga opcja jest lepsza?

Z drugiej strony gdyby to był jakiś proces biznesowy, np. po otrzymaniu konkretnego statusu, użytkownik ma 7 dni na akceptację albo odrzucenie dokumentu i co jakiś czas chcę mu wysłać przypomnienie to wtedy chyba bym się skłaniał ku opcji 1, bo to jednak jest jakaś logika biznesowa.

A może jeszcze inaczej?

3

Wybieram opcję nr 2, dlatego że:

  • Jak często system odpytuje proces zewnętrzny, to nie są „business rules”
  • core powinien implementować listener na gotowy plik. Kto go odpali, to kwestia infrastruktury/dependency injection.
  • Pomyśl, co się zmieni, jeśli system zewnętrzny udostępni notyfikacje o gotowym pliku na przykład wysyłając event do kolejki, albo gdy skorzystasz z api S3 do nasłuchowania na zmiany.

Dobrze kombinujesz, że w przypadku faktycznych procesów biznesowych domain powinien deklaratywnie definiować schedulera, a infrastructure tłumaczyć to na konkretna technologię.

0

Pomyśl, co się zmieni, jeśli system zewnętrzny udostępni notyfikacje o gotowym pliku na przykład wysyłając event do kolejki, albo gdy skorzystasz z api S3 do nasłuchowania na zmiany. to co nie powinno, czyli Core :P Chociaż i tak Core definiowałby jakiś interfejs do sprawdzenia statusu danego dokumentu w tym zewnętrznym API i jak nagle by się zmienił sposób powiadamiania to teoretycznie można w infrastrukturze zrobić jakąś warstwę pośrednią, która zbuforuje to po mojej stronie i tylko podmienić implementację interfejsu z Core na nową, która odczyta status z tych danych.

4

@some_ONE: no przecież w tej sytuacji Core się nie zmieni, zmieni się Infrastructure. W Core będzie wciąż ten sam kod obsługujący dokładnie ten sam przypadek użycia z dokładnie tymi samymi danymi wejściowymi.
Logika przypadku użycia się jest taka sama, niezależnie od tego, czy inicjuje go wywoływanie z kontrolera API, z jakiegoś słuchacza Kafki czy innego service busa, czy jak w tym przypadku z poolera odpytującego jakieś źródło danych o coś.

0

@somekind: trochę tam namieszałem w odpowiedzi.

W scenariuszu gdzie Core wystawia jakiś UseCase, który jako dane wejściowe przyjmuje wynik przewarzania jak najbardziej nic się nie zmieni po stronie Core.

To co pisałem dotyczyło scenariusza 1 gdzie to Core robi pooling. Wtedy gdyby tam były jakieś sensowne abstrakcje na odczytywanie statusu/wyniku przetwarzania to też teoretycznie Core nie musiałby się zmienić, ale przy zmianie sposobu (np. z poolingu na kolejkę z powiadomieniami) wymagałoby to dodatkowych implementacji po warstwie infrastruktury żeby po swojej stronie przechować komunikaty z kolejki :P Raczej to słabe i tutaj mamy jasność, że odpytywanie to szczegół implementacyjny i Core powinien tylko mieć UseCase na przyjęcie wyniku i zrobienie z nim coś swojego.

A jak byś zrobił coś podobnego, ale w przypadku gdy chodzi o jakiś bardziej biznesowy przypadek? Tak jak wspominałem, np. reguła że użytkownik ma 7 dni na zatwierdzenie albo odrzucenie wyniku (wtedy poprawia parametry i procesuje jeszcze raz) i chcę wysyłać przypomnienia, albo rozpoczął proces i nie wrzucił dokumentu, więc bez jego akcji nic więcej się nie wydarzy - też co jakiś czas powiadomienie.

1

No ja niespecjalnie widzę różnicę. Po stronie Infrastructure jest coś, co generuje jakieś dane, wysyła je do Core, core robi swoją logikę, a potem (znowu za pomocą Infrastructure) np. wysyła powiadomienia tam, gdzie trzeba.

0

No im dłużej się nad tym zastanawiałem to też dochodziłem do podobnego wniosku.
Dopytuję, bo teraz się zastanawiam czy w ogóle w jakimś przypadku byłoby sensowne deklarowanie interfejsu do zadań cyklicznych, czy tam zadań w tle po stronie Core.

0

No wówczas Core musiałoby być inicjatorem operacji, czyli być samo swoim światem zewnętrznym. To chyba niemożliwe.

3
some_ONE napisał(a):

No im dłużej się nad tym zastanawiałem to też dochodziłem do podobnego wniosku.

Dopytuję, bo teraz się zastanawiam czy w ogóle w jakimś przypadku byłoby sensowne deklarowanie interfejsu do zadań cyklicznych, czy tam zadań w tle po stronie Core.

Nie powinno. W języku architektury hexagonalnej scheduler taki to będzie adapter sterujący.

1

Poza domeną to powinno być. Obecnie masz model pull (odpytujesz zewnętrznej usługi), a może kiedyś będziesz miał push (asynchroniczna reakcja na zdarzenie) - z punktu widzenia kodu domenowego nie powinno być ani jednej zmiany, ani jeden test nie powinien się wysypać po podmiance. Joby, Quartze i workery nie należą do języka ekspertów domenowych (w teorii).

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