14 — Odporność operacyjna i zewnętrzne zależności
Przykazania III, V, VI w warstwie runtime: prod żyje w zawodnym świecie. Sieć rwie, provider blokuje porty, Scraping — Automatyczne zbieranie danych ze stron internetowych przez program zamiast ręcznego kopiowania. Potężne do pozyskiwania danych, ale wymaga kultury: szanuj cudze zasady (robots.txt), nie przeciążaj serwera. pada w połowie, płatne API — Umówiony sposób, w jaki dwa programy gadają ze sobą — jeden prosi, drugi odpowiada w ustalonym formacie. Przez API Twoja aplikacja łączy się z cudzymi usługami (płatności, mapy, AI). Klucz API traktuj jak hasło. kosztuje przy każdym żądaniu. Doktryna z rozdziałów 03–05 dba o kod i wdrożenie; ten rozdział o tym, co dzieje się po — gdy realni użytkownicy i niezależne systemy uderzają w działający serwis.
Te lekcje są drogo opłacone: prawie każda to incydent na prodzie, nie teoria. Wspólny mianownik — najgorsze bugi nie krzyczą, tylko cicho zawieszają, wylogowują albo drenują budżet.
1. Jeden zły request nie może położyć procesu
Niezłapany throw/rejection w handlerze async potrafi ubić cały serwer (Node/Express:
rejection → exit procesu → pętla restartów pm2 — Menedżer, który pilnuje, żeby aplikacja (Node) ciągle działała — restartuje ją po awarii. Bez tego po crashu albo restarcie serwera aplikacja po prostu stoi. pm2 trzyma ją „przy życiu”.). Zbuduj siatkę na poziomie procesu:
- Globalne łapacze:
process.on('unhandledRejection')i'uncaughtException')— logują i kontrolowanie kończą, nie zostawiają zombie-procesu. - Wrapper async-route przekazujący błąd do
next(err)zamiast gubić go w niezłapanym promise. - Middleware 500, który nie wycieka stack trace użytkownikowi (→ 09).
- Defensywa na danych z brzegu: konto OAuth bez hasła, pole
nulltam gdzie kod zakłada string — to realne wejścia, gdy wpuścisz prawdziwych userów (często ujawnia się po swapie bazy, → 05).
Reguła: throw w jednym żądaniu degraduje to żądanie, nie serwis. Otestuj regresyjnie (→ 03).
2. Długie zadania: wznawialne i odpięte od sesji agenta
Scraper/ETL „zbierz wszystko → zapisz raz” traci 100% pracy przy każdym crashu. Pisz batchami:
- Checkpoint zrobionych jednostek (plik/tabela z URL-ami/ID) — restart wznawia od miejsca, nie od zera.
- Zapis co N, nie na końcu — crash kosztuje ostatni batch, nie cały run.
- Dopasuj runner do czasu życia zadania. Tło agenta jest trwałe w obrębie sesji — przeżywa tury i raportuje, gdy skończy, więc korzystaj z niego (→ 16) — ale umiera razem z sesją: teardown hosta sesji ubija proces w połowie (to nie anti-bot, to znikający runner). Cokolwiek ma przeżyć rozmowę, idzie do procesu odpiętego (detached) albo schedulera OS (→ § 7), nigdy do tła sesji.
- Idempotencja — Cecha operacji, którą można uruchomić wiele razy, a wynik jest zawsze taki sam — bez dublowania. Kluczowa przy skryptach i zdarzeniach: ponowne uruchomienie nie psuje danych. Jak włącznik „ON”. całości (→ 04): wznowiony job nie dubluje już zapisanych danych.
3. Zewnętrzne źródła traktuj jak wrogie
Cudze API i strony rwą połączenia, rate-limitują (429), zwracają 200 z HTML-em błędu. Zakładaj zawodność:
- Timeout na każde wywołanie — bez niego socket wisi w nieskończoność (cichy zawis, nie błąd).
- Retry z exponential backoff (np. 10/20/30 s), z górnym limitem prób.
- Rotacja sesji i User-Agent przy masowym I/O — nowe
Session(świeże TCP/cookies) co batch, UA z puli realnych przeglądarek (część hostów rwie po ~kilkuset żądaniach z jednej sesji). - Waliduj odpowiedź, nie status — „200 + strona błędu” to porażka; sprawdź kształt danych.
4. Infrastruktura providera narzuca limity — weryfikuj end-to-end
Hosting ma własne reguły sieciowe, które kładą „działający” kod dopiero na prodzie:
- Porty bywają blokowane. Np. wyjściowy SMTP 465 potrafi być zamknięty → użyj 587 + STARTTLS.
secure:truena zablokowanym porcie zawiesza każdą wysyłkę aż do timeoutu — ustaw twarde connection/greeting timeouts, żeby błąd był głośny. - Deliverability poczty to nie „wysłałem”. Zweryfikowana domena nadawcy (SPF/DKIM), realne wysłanie testowe, opt-in zgodny z RODO / GDPR — Unijne prawo o ochronie danych osobowych — jak wolno zbierać, trzymać i usuwać dane użytkowników. Dotyczy każdej aplikacji z danymi ludzi. Lepiej wpisać zgody i retencję od początku niż płacić kary później. (→ 09). E-mail, który „wyszedł”, a wylądował w spamie/nigdzie, to bug.
- Sprawdź to na prodzie/stagingu providera, nie lokalnie — lokalna sieć nie ma tych blokad (→ 03).
5. Każdy płatny zasób za twardą kwotą
Endpoint — Pojedynczy „adres” w API, pod który wysyłasz zapytanie po konkretną rzecz (np. listę zamówień). Aplikacje rozmawiają przez endpointy — jeden endpoint = jedna funkcja, którą udostępniasz. wołający płatne API (LLM, geokodowanie, e-mail) bez limitu to otwarty rachunek i wektor nadużyć:
- Kwota per użytkownik (okno miesięczne/dobowe) z jasnym komunikatem i datą resetu z góry.
- Różnicuj per tier (free vs premium), egzekwuj po stronie serwera.
- Rate-limit + nagłówki bezpieczeństwa (helmet/limiter) jako stały element stacku (→ 08).
- Powiąż z prawem i regulaminem: limit i jego komunikacja to też ochrona przed abuse (→ 09).
6. Wiedz, że prod żyje — observability
Powyższe obrony chronią proda przed śmiercią; observability mówi Ci, że żyje — po zamknięciu okna deployu, zanim user napisze maila. „Weryfikuj, nie deklaruj” (→ 03) zastosowane do działającego systemu:
- Logi strukturalne z poziomami.
error/warn/info(nieprintwszędzie). Błędy niosą kontekst (request id, user, co padło) — ale nigdy sekretów ani pełnego PII. Grepujesz logi o 2 w nocy; zrób je grepowalne. - Endpoint zdrowia.
/healthzzwracający 200 + tani check (baza osiągalna, wersja) — dla load balancera i monitora uptime. Zewnętrzny ping uptime powie Ci o awarii zanim powie klient. - Alertuj na to, co boli, nie na szum: skoki 5xx, nieudane płatności/maile, job który nie wystartował, drift selektora / „200 + strona błędu” (→ § 3). Jeden działający alert bije sto dashboardów.
- Kilka realnych metryk zamiast vanity: error rate, p95 latency, głębokość kolejki, dzienny koszt płatnych API (→ § 5). Mierz, zanim optymalizujesz (→ 13).
- Błędy → tam, gdzie je zobaczysz (log drain / error tracker), nie tylko stdout, który ucieka.
Zacznij malutko: logi z poziomami + health check + jeden ping uptime łapią większość wartości. Rozwijaj dopiero, gdy realny incydent pokaże lukę (zapisz lekcję w runbooku → 05).
7. Zadania cykliczne: pilnuj wyniku, nie runnera
Nocny job to ta część proda, która pada cicho z założenia — nikt nie patrzy o 4 rano, a awaria wygląda dokładnie jak spokojna noc. Trzy reguły, każda opłacona incydentem:
- Watchdog — Niezależny dozorca, który sam sprawdza, czy dane są świeże / aplikacja żyje, i alarmuje, gdy coś stoi. Przy zadaniach cyklicznych mierz skutek (wiek najświeższego rekordu), nie kod wyjścia joba — zielony raport potrafi kłamać. mierzy dane, nie job. Kod wyjścia 0, log, który urósł, „26 scraperów, 0 błędów” — każde z
nich może być prawdą, gdy dane stoją zamrożone. Zamiast tego sprawdzaj wiek najświeższego rekordu per
źródło (
MAX(updated_at)grupowane po sklepie/feedzie/tenancie). To przeżywa job, który się odpalił i nic nie pobrał, ingest, który padł po udanym pobraniu, i źródło dodane za miesiąc — czego żaden check skanujący logi nie przeżyje. W projekcie referencyjnym zielony nocny raport chodził trzy dni obok 44% zamrożonego katalogu: raport nie kłamał, po prostu nic nie wiedział o drugiej ścieżce danych. - Zielony raport pokrywa tylko to, o czym wie. Gdy część pipeline’u chodzi gdzie indziej — inna maszyna, inny harmonogram, krok ręczny — milczenie głównego raportu o tym nie jest dobrą wiadomością. Albo każda ścieżka kończy się własnym dowodem, albo jeden watchdog widzi je wszystkie.
- Config schedulera to produkcja i żyje poza repo. Tamten incydent nie był bugiem kodu: zadanie
zaplanowane miało wyłączone „uruchom jak najszybciej po przegapionym starcie” i warunek „nie startuj na
baterii”, więc śpiący laptop znaczył pominiętą noc, której nigdy nie ponowiono. Żaden test, review ani
git logby tego nie złapał. Wpisz do runbooka ustawienia, które mają znaczenie (retry-po-przegapieniu, warunki zasilania/sieci, konto, na którym chodzi, katalog roboczy → 05) i re-sprawdzaj je po każdej zmianie OS, sprzętu czy konta.
Anty-wzorce
- 🚫 Brak globalnego łapacza błędów — jeden zły request restartuje serwis dla wszystkich.
- 🚫 Scraper bez checkpointów odpalany z tła sesji — crash = run od zera, znikający runner = wieczna porażka.
- 🚫 Zewnętrzne wywołanie bez timeoutu i backoffu — cichy zawis albo ban po serii 429.
- 🚫 „Wysłałem maila” ≠ dostarczyłem — brak weryfikacji portu/domeny/dostarczalności.
- 🚫 Płatne API bez kwoty — niespodziewany rachunek i otwarty wektor nadużyć.
- 🚫 Sesje w pamięci procesu — każdy deploy wylogowuje wszystkich (→ 05).
- 🚫 Brak logów/alertów — o awarii proda dowiadujesz się od usera, nie od systemu; „działa” bez sposobu, by to wiedzieć.
- 🚫 Job cykliczny pilnowany własnym kodem wyjścia — melduje sukces przez tydzień, gdy dane stoją w miejscu.
- 🚫 Ustawienia schedulera, których nikt nie zapisał — run jest „zautomatyzowany” aż do nocy, gdy cicho nie jest.
- 🚫 Sekrety/PII w logach — log drain staje się wyciekiem.
Dla nowych projektów
Wpisz do Dnia 0 (→ 07) zanim pojawi się pierwszy realny user: globalny error-handler + 500 bez wycieku, timeouty i backoff na każdym zewnętrznym I/O, kwoty na płatnych endpointach, trwały store sesji w osobnym pliku. To tańsze teraz niż jako incydent o 2 w nocy. Każdy incydent prod → wpis w runbooku z datą i ponumerowaną lekcją (→ 05, 06).