Wpisy
Soak test API + scraper + notifier — jak sprawdzić pipeline, którego krótkie CI nie złapie
Jak projektuję i odpalam soak / endurance na stacku API + scraper + notifier — gdy zielone CI nic nie mówi o tym, czy łańcuch danych wytrzyma kilka godzin.
Przy systemach, które buduję, wartość często nie kończy się na odpowiedzi HTTP. Typowy układ wygląda tak: API (rejestracja, subskrypcja, preferencje, odczyt danych), scraper (ciągłe ściąganie i normalizacja z zewnętrznych źródeł, zapis do bazy, sygnały do monitoringu) i notifier (dopasowanie nowych rekordów do preferencji użytkownika i wysyłka powiadomień).
Każda usługa żyje swoim życiem — inne zależności, inne awarie. Sens biznesowy pojawia się dopiero w łańcuchu: nowe dane → dopasowanie → powiadomienie, a API ma przy tym dalej działać. Unit, integracja i build w CI łapią regresje w kodzie. Nie odpowiadają jednak na pytanie, czy ten łańcuch wytrzyma kilka godzin pod obciążeniem i przy żywych źródłach na zewnątrz.
Poniżej opisuję, jak projektuję i odpalam soak / endurance na takiej architekturze — bez wchodzenia w szczegóły domeny produktu.
O co chodzi
Przy API + workerach w tle + asynchronicznych powiadomieniach pojawiają się ryzyka, których testy sekundowe po prostu nie widzą:
- Kontener żyje, a proces biznesowy — nie. Health scrapera może być zielony, a pętla scrapingu od dawna nic nie dokłada do pipeline’u.
- Notifier jest asynchroniczny. To, że API przyjęło filtr, nie znaczy, że mail dojdzie. Potrzebne są świeże dane ze scrapera i działające kolejki.
- Współdzielony stan z dewem. Ta sama baza i te same kolejki co lokalny development psują SLO: fałszywe błędy zapisu, zaległe wiadomości, rekordy już oznaczone jako przetworzone.
- Równolegli użytkownicy. Kilku wirtualnych userów naraz na jednym API szybko pokazuje zależności między scenariuszami — np. najpierw konto, potem subskrypcja, dopiero potem akcja wymagająca uprawnień.
- Źródło padło ≠ padła nasza baza. Niedostępność zewnętrznego źródła to nie to samo, co błąd zapisu u nas albo rosnąca DLQ.
Cel harnessu, który buduję: czytelny werdykt o stabilności całego stacku, z naciskiem na pipeline danych. I świadomie poza twardym gate’em CI — zbyt długo trwa, za dużo CPU, outbound i flaków z żywego internetu.
Architektura testu — nie „dłuższy E2E”
Harness w Go odpala dwa tory równolegle. Runtime (scraper → baza → notifier → mail oraz API) i nakładka soak, która to obserwuje i steruje E2E:
Observer pipeline’u (główne SLO)
Co kilkadziesiąt sekund sprawdzam:
- health scrapera i stan per źródło (w tym circuit breaker),
- eventy z logów i metryk: heartbeat, rate limit, niedostępność zewnętrzna, błędy batch save,
- backlog w bazie — rekordy czekające na notifier,
- głębokość kolejek w brokerze (retry, DLQ).
To jest priorytet werdyktu. Jeśli pipeline leży, soak pada — nawet gdy API dalej zwraca 200.
Workerzy API E2E (drugorzędne SLO)
N wirtualnych użytkowników odpala ważone scenariusze HTTP (bez UI):
- założenie konta,
- aktywacja subskrypcji (płatność testowa),
- CRUD preferencji,
- przeglądanie zasobów,
- pipeline_notify — ustawienie preferencji i czekanie na maila.
Ostatni scenariusz to test trzech usług naraz, nie samego API. Ma więc osobne reguły: warmup, dłuższy timeout — i nie powinien rządzić krótkimi smoke’ami.
Dwa SLO (pipeline / API) pozwalają czytać wynik jak produkcyjny incident: gdzie pękło — przy ingestcie, w kolejkach, czy na ścieżce użytkownika?
Problemy: przyczyny, skutki, przeciwdziałania
1. Soak na tym samym Compose’ie co lokalny dev
Przyczyna: odpalam soak na tym samym projekcie Compose co codzienny development — te same volumy, sieci, często ten sam sposób startu scrapera (ręczny proces vs osobny kontener).
Skutki: wynik miesza jakość kodu z historią mojej maszyny. Nie wiadomo, czy padło dlatego, że kod jest zły, czy dlatego, że wczoraj zostawiłem brudne kolejki.
Przeciwdziałanie: osobny projekt Compose, własne volumy, własna podsieć, inne porty na hoście, domyślny reset volumów przed runem. Migracje dają schemat i dane referencyjne; konta użytkowników powstają dopiero w trakcie E2E.
2. compose up to jeszcze nie działający pipeline
Przyczyna: kontenery wstają — i ktoś zakłada, że scraper już ciągnie dane, a notifier już consumuje kolejki.
Skutki: soak startuje „na nadziei”. Pierwsze minuty to szum albo fałszywy fail, zanim stack w ogóle zacznie pracować.
Przeciwdziałanie: osobny preflight przed soakiem — migracje, recreate/restart workerów, fail-fast na health, czyszczenie kolejek i skrzynki testowej, bramka aktywności scrapera (dowód heartbeat albo nowych rekordów). Bez tego soak się nie rusza.
3. Stan wirtualnego usera rozjeżdża się z API
Przyczyna: wirtualny użytkownik żyje przez wiele losowych scenariuszy. API wymaga kolejności uprawnień: konto → subskrypcja → akcje premium. Cache stanu w harnessie łatwo rozjeżdża się z tym, co naprawdę wie serwer.
Skutki: flaky asercje, scenariusze premium na koncie bez subskrypcji, fail, który wygląda jak bug produktu, a jest bugiem testu.
Przeciwdziałanie: traktuję stan VU jak stan produkcyjny — przed flow wymagającym subskrypcji synchronizuję się z API; przy nowym koncie resetuję sesję. To jest problem projektu testu, nie tylko „zła asercja”.
4. Scenariusz notify to test całego łańcucha
Przyczyna: API może być zielone przy zerowym success rate powiadomień — scraper nie dokłada świeżych danych albo notifier nie nadąża. Krótki timeout E2E myli to z awarią produktu.
Skutki: albo ciągłe false positive na notify, albo sztucznie obniżony próg, który chowa prawdziwe problemy pipeline’u.
Przeciwdziałanie: rozdzielam SLO (core API vs notify), daję warmup, przed czekaniem na maila sprawdzam w bazie, czy w ogóle jest backlog do przetworzenia. Mierzę to, co architektura obiecuje: preferencja + nowe dane → wiadomość.
5. Awaria na zewnątrz ≠ awaria u nas
Przyczyna: scraper siedzi na żywych źródłach i Chromium. Pusta odpowiedź źródła to nie to samo, co błąd zapisu do naszej bazy.
Skutki: soak pada przy lekkiej degradacji internetu tak samo jak przy awarii własnej infrastruktury. Wynik przestaje się nadawać do decyzji.
Przeciwdziałanie: w observerze rozróżniam sygnały zewnętrzne (warning / pass z ostrzeżeniami) od wewnętrznych (fail głównego SLO: brak heartbeatu, błędy batch save, rosnąca DLQ). Soak ma być użyteczny przy częściowej degradacji, a nie tylko „wszystko albo nic”.
Jak to odpalam
./bin/run-endurance --duration 20m --vus 3
./bin/run-endurance --no-reset --duration 20m --vus 3 # świadomie na istniejącym stanie
./bin/run-endurance --down-volumes # sprzątanie
Stack soak: API + scraper + notifier + reverse proxy + Postgres + Redis + RabbitMQ + skrzynka testowa + Loki/Grafana — blisko produkcji, odcięty od codziennego dewu.
Wynik: raport JSON + exit code (pass / fail pipeline / pass z ostrzeżeniami zewnętrznymi / fail API E2E).
Co z tego wychodzi
Na czystym, izolowanym stacku soak pokazuje to, czego oczekiwałem od architektury:
- scraper (główne SLO) — stabilny heartbeat, bez wewnętrznych naruszeń wynikających z „brudu” środowiska,
- API E2E (drugorzędne) — ścieżki użytkownika na poziomie ~99%+,
- pipeline_notify — działa end-to-end; jako najwolniejszy i najbardziej zależny od świata zewnętrznego potrzebuje dłuższych runów i ostrożniejszych progów,
- CI — soak świadomie poza twardym gate’em (ręcznie / nie blokuje merge); gate zostaje przy lint, unit, integracji i buildzie.
To nie jest „więcej asercji w pipeline”. To osobna ścieżka sprawdzania, czy system rozproszony zachowuje się sensownie w czasie.
Co z tego biorę do kolejnych projektów
Przy systemach z workerami w tle zakładam, że:
- zielony health ≠ działający biznes — aktywność pipeline’u trzeba udowodnić osobno,
- asynchroniczne powiadomienia testuje się jako pipeline, nie jako jeden endpoint,
- izolacja i reset stanu są częścią jakości testu, tak samo jak dobra asercja,
- SLO warto dzielić na warstwy (ingest / kolejki / API), żeby wynik dało się czytać jak diagnozę,
- ciężkie soak’i nie należą do twardego CI — ich wartość jest w świadomym odpaleniu przed release albo po zmianach w workerach, nie w blokowaniu każdego MR.
Technicznie opieram to na Compose z osobnym projektem, harnessie w Go, logach i eventach, brokerze wiadomości oraz headless Chromium po stronie scrapera — tym samym zestawie, którego używa produkcja, tylko w kontrolowanym, powtarzalnym rytuale.
Podsumowanie
Soak dla API + scraper + notifier zasypuje lukę między zielonym CI a pewnością, że pipeline działa godzinami. Klucz nie leży w liczbie scenariuszy HTTP, tylko w architekturze testu: izolacja od dewu, preflight z dowodem pracy scrapera, dwa SLO oraz scenariusz powiadomień traktowany jako test całego łańcucha. Tak sprawdzam systemy, w których wartość powstaje asynchronicznie — a nie tylko w odpowiedzi API.