Blog

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ą:

  1. 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.
  2. 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.
  3. 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.
  4. 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ń.
  5. Ź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:

Happy path: scraper zapisuje do Postgres (nie do RabbitMQ). Notifier wykrywa nowe wiersze poll’em, potem wysyła mail przez kolejkę offer_notification.

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.