Odpowiedź na pytanie
Wzorzec CQRS (Separacja Odpowiedzialności Komendy i Zapytania) powstał z praktyk projektowania opartego na domenie, aby rozwiązać problemy ze skalowalnością w scenariuszach o dużym odczycie, poprzez oddzielanie modeli zoptymalizowanych do zapisu (PostgreSQL, Oracle) od projekcji zoptymalizowanych do odczytu (Elasticsearch, MongoDB). Ta architektoniczna bifurkacja tworzy wewnętrzną lukę czasową między trwałością polecenia a dostępnością zapytań, ponieważ asynchroniczne procesory zdarzeń muszą denormalizować dane wzdłuż granic sieci, zanim modele odczytu odzwierciedlą zmiany stanu.
Podstawowy problem w automatyzacji tych systemów wynika z warunków wyścigu między wątkami wykonania testu a pracownikami projekcji w tle, gdzie asercje dotyczące modeli odczytu zaraz po złożeniu polecenia niepowodzą się nieprzewidywalnie z powodu opóźnienia w przetwarzaniu. Tradcionalne rozwiązania polegają na arbitralnych opóźnieniach lub naiwnym polling, co albo spowalnia potoki do nieakceptowalnych prędkości, albo powoduje fałszywe negatywy pod stresem infrastrukturalnym.
Solidne rozwiązanie implementuje śledzenie pozycji zdarzenia, wykorzystując przesunięcia strumieni lub tokeny przechwytywania danych zmian (Debezium, grupy konsumentów Kafka), aby ustalić deterministyczną barierę synchronizacji. Ramy testowe przechwytują pozycję ostatnio wydanego zdarzenia domenowego i dokonują polling metadanych modelu odczytu, aż potwierdzą konsumpcję tej konkretnej pozycji, wykorzystując wykładnicze opóźnienie powrotu z czasami wyłączania obwodu, aby zapobiec nieograniczonemu blokowaniu, jednocześnie utrzymując precyzję synchronizacji poniżej sekundy.
Sytuacja z życia
Podczas projektowania automatyzacji testów dla platformy handlowej o wysokiej częstotliwości, nasz zespół napotkał krytyczną niestabilność w testach wyceny portfela, które wykorzystywały PostgreSQL do trwałości zleceń handlowych i Elasticsearch do bieżących zapytań o salda. Testy wykonujące polecenia kupna/sprzedaży i od razu zapytujące o punkty końcowe portfela otrzymywały przestarzałe salda przedtransakcyjne, ponieważ projekcje Kafka Connect wymagały 300-800ms na indeksowanie aktualizacji, co powodowało, że 35% budów CI niepowodowało się błędnie.
Nasze pierwsze rozważane rozwiązanie wprowadziło stałe Thread.sleep(2000) po każdej operacji zapisu, zapewniając zakończenie indeksowania Elasticsearch przed asercjami. To podejście tymczasowo ustabilizowało wyniki, ale zwiększyło czas wykonania suite o 400%, stworzyło delikatne zależności czasowe od wydajności sprzętu i pozostało wrażliwe na przerwy w zbieraniu śmieci lub zator sieciowy, które czasami przekraczały stałe opóźnienie.
Drugie oceniane podejście implementowało ogólny polling z wykładniczym opóźnieniem powrotu na końcowym punkcie zapytań, powtarzając asercje, aż oczekiwane wartości się pojawiły lub upłynął czas oczekiwania. Choć lepsze od stałych opóźnień, ta metoda cierpiała na niejasność między stanami "jeszcze nie zaktualizowany" a "nieprawidłowa wartość", i nie mogła obsługiwać równoległych scenariuszy testowych, w których wiele uruchomień modyfikowało identyczne agregaty jednocześnie, co prowadziło do zanieczyszczenia testów i fałszywych pozytywów.
Ostatecznie wybraliśmy trzecie podejście, polegające na instrumentacji warstwy projekcji, aby eksponować ostatnio przetworzone offsety Kafka w metadanych dokumentów Elasticsearch. Nasz zestaw testowy przechwytywał offset zdarzenia opublikowanego jako polecenie i używał specjalizowanego narzędzia oczekiwania, które wykonało polling modelu odczytu, aż jego metadane wskazały, że offset został skonsumowany, co zapewniło spójność bez czasowego zgadywania. To zmniejszyło średni czas wykonania testów z 52 sekund do 14 sekund i całkowicie wyeliminowało fałszywe negatywy, przekształcając asynchroniczną niepewność w deterministyczne punkty synchronizacji.
Co często pomijają kandydaci
Jak zapobiegasz interferencji danych testowych, gdy wiele równoległych runnerów CI jednocześnie aktualizuje agregaty, które dzielą projekcje modelu odczytu, nie wprowadzając mechanizmów blokowania, które naruszałyby asynchroniczną naturę CQRS?
Odpowiedź: Zastosuj logiczną izolację najemców funkcjonującą przy użyciu identyfikatorów agregatów z sufiksem UUID i identyfikatorów korelacji uruchomienia testu osadzonych w metadanych zdarzeń. Skonfiguruj indeksy modeli odczytu, aby uwzględniały identyfikator uruchomienia testu jako klucz trasowania lub parametr filtra, zapewniając, że zapytania projekcji zwracają tylko dokumenty istotne dla konkretnego kontekstu wykonania testu. To pozwala na równoległe wykonywanie testów bez fizycznych blokad bazy danych, jednocześnie utrzymując rygorystyczną segregację danych między równoległymi instancjami potoku.
Jaka jest podstawowa różnica architektoniczna między walidacją zachowania modelu zapisu a zachowaniem modelu odczytu w CQRS i dlaczego ta różnica wymaga różnych strategii asercji?
Odpowiedź: Walidacja modelu zapisu koncentruje się na atomowości transakcyjnej, egzekwowaniu inwariantów biznesowych i poprawności emisji zdarzeń domenowych, zazwyczaj wykorzystując możliwości wycofania transakcji bazy danych, aby utrzymać izolację testu. Walidacja modelu odczytu zajmuje się dokładnością denormalizacji, czasem odpowiedzi zapytań SLA oraz zgodnością okna ostatecznej spójności, wymagając asercji, które uwzględniają opóźnienia przetwarzania asynchronicznego i weryfikują, że projekcje obsługują duplikaty zdarzeń lub dostawy w nieodpowiedniej kolejności idempotentnie.
Jak zaprojektowałbyś automatyczne testy, aby zweryfikować, że modele odczytu poprawnie obsługują dostawy zdarzeń w nieodpowiedniej kolejności lub przetwarzanie zdarzeń duplikatów, nie naruszając integralności danych, zwłaszcza gdy projekcje implementują kontrolę konkurencji optymistycznej?
Odpowiedź: Zbuduj zestaw testów do wstrzykiwania błędów, który celowo publikuje zdarzenia w złej kolejności, korzystając z ponownego przydzielania partycji Kafka lub manipulacji znacznikami czasowymi, a następnie asertuj, że model odczytu kolejkuje i porządkuje zdarzenia za pomocą zegarów wektorowych lub stosuje idempotentne aktualizacje na podstawie numerów wersji agregatów. Zweryfikuj, że projekcja utrzymuje monotoniczną spójność, sprawdzając, że numery sekwencji nigdy się nie zmniejszają i że ponownie dostarczane zdarzenia (symulowane przez ręczne zresetowanie offsetu) nie tworzą fałszywych rekordów ani nie inkrementują liczników wielokrotnie w magazynie zapytań.