CodeGym /Kursy /ChatGPT Apps /Odporność systemu: timeouts, circuit breakers, bulkheads,...

Odporność systemu: timeouts, circuit breakers, bulkheads, ochrona przed sztormami webhooków

ChatGPT Apps
Poziom 16 , Lekcja 2
Dostępny

1. Dlaczego w ogóle myśleć o „odporności” w ChatGPT App

W zwykłej aplikacji webowej użytkownik przynajmniej widzi URL, spinner przeglądarki, może odświeżyć stronę. W ChatGPT użytkownik widzi jeden ekran: czat i waszą aplikację. Jeśli coś zwalnia, nie rozróżnia, kto jest winny – OpenAI, wasz Gateway, płatności czy sąsiedni mikrousług analityki. Dla niego to wszystko to „ChatGPT + wasza aplikacja”.

Gdy tool-call wisi 3060 sekund, model czeka, czeka… i w najlepszym razie przeprasza za opóźnienie. W najgorszym – halucynuje odpowiedź zamiast danych z waszego backendu. Dlatego odporność to nie tylko SRE i uptime, to także jakość odpowiedzi, ton zachowania modelu i metryki w Store.

W ekosystemie ChatGPT App mamy kilka niezależnych konturów:

  • ChatGPT ↔ MCP Gateway.
  • Gateway ↔ wasze usługi backendowe/REST (Gift REST API, Commerce REST API, Analytics Service itp.).
  • Wasze usługi ↔ zewnętrzne API (LLM, płatności, katalogi).
  • Przychodzące webhooki (ACP, Stripe, dowolne integracje) ↔ wasze handlery.

Problem w tym, że awaria w jednym miejscu może wywołać kaskadę: Gateway uczciwie czeka na zawieszony serwis, workerzy się zapychają, kończą się połączenia, klienci zaczynają robić ponowienia, i po kilku minutach macie klasyczny scenariusz: wszystko się pali i tonie jednocześnie. Przed tym chronią nas cztery wzorce, o których dziś mówimy:

  • Timeouts – nigdy nie czekamy w nieskończoność.
  • Circuit breaker – nie walimy w zamknięte drzwi.
  • Bulkheads – budujemy „przedziały” i nie pozwalamy zatonąć całemu statkowi.
  • Ochrona przed sztormami webhooków – uznajemy, że webhooki przychodzą z duplikatami, skokami i ponowieniami, i przygotowujemy się na to.

2. Timeouts: nie czekamy w nieskończoność

Co to jest timeout i dlaczego bez niego jest źle

Timeout to maksymalny czas, jaki wasz kod jest gotów czekać na odpowiedź zależności: bazy danych, serwera MCP, zewnętrznego HTTP API, modelu. Jeśli odpowiedź nie nadejdzie w zadanym czasie – uznajemy wywołanie za nieudane, zwalniamy zasoby i zwracamy zrozumiały błąd lub fallback.

Bez timeoutów żądania mogą:

  • wisieć w oczekiwaniu bez końca,
  • zajmować połączenia i pulę wątków,
  • blokować kolejne żądania,
  • powodować kaskadowe awarie.

Wzorzec jest prosty: „lepsza przewidywalna odmowa po 35 sekundach niż niezrozumiała cisza przez 5 minut”.

Warto pamiętać, że mamy timeouty na kilku poziomach:

  • na poziomie proxy/balancera (Cloudflare, Nginx),
  • na poziomie MCP Gateway (klienci HTTP do mikrousług),
  • w samych serwisach (wywołania do bazy danych, zewnętrznych API, LLM).

W ChatGPT rozsądnie jest dążyć do całkowitego czasu tool-call w zakresie 510 sekund dla zwykłych operacji i maksymalnie 2030 sekund dla szczególnie ciężkich. Wszystko, co dłuższe – to niemal gwarantowany zły UX.

Prosty fetchWithTimeout w TypeScript

Zacznijmy od praktyki. W GiftGenius MCP Gateway mamy pomocniczego klienta HTTP, który chodzi do selektora prezentów (gift), do usługi commerce, do analityki. Obleczemy standardowy fetch w funkcję z timeoutem:

// src/gateway/httpClient.ts
export async function fetchWithTimeout(
  url: string,
  opts: RequestInit & { timeoutMs?: number } = {}
) {
  const { timeoutMs = 5000, ...rest } = opts;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await fetch(url, { ...rest, signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

Teraz w kodzie Gateway nigdy nie robimy „gołego” fetch, tylko przez tego helpera:

// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";

export async function callGiftService(path: string) {
  const res = await fetchWithTimeout(
    process.env.GIFT_SERVICE_URL + path,
    { timeoutMs: 4000 }
  );

  if (!res.ok) {
    throw new Error(`gift_service_${res.status}`);
  }
  return res.json();
}

Takie podejście gwarantuje, że nawet jeśli usługa gift się zawiesi, po 4 sekundach przerwiemy połączenie i będziemy mogli zwrócić błąd MCP do ChatGPT, zamiast trzymać połączenie do oporu.

Gdzie dokładnie ustawiać timeouty w GiftGenius

W naszym przykładzie GiftGenius:

  • Na poziomie Gateway: timeouty na wywołania Gift REST API, Commerce REST API, Analytics Service / REST API.
  • Wewnątrz tych serwisów: timeouty na wywołania do bazy danych, ACP/bramek płatniczych, zewnętrznych API rekomendacyjnych.
  • Na wejściu do Gateway: ogólny timeout żądania z ChatGPT, aby tool-call nie zamieniał się w „wieczny spinner”.

Ważne, aby czas oczekiwania na najwyższym poziomie był nieco większy niż na wewnętrznych. Na przykład, jeśli Gateway czeka na backend 5 sekund, a backend czeka na bazę danych 3 sekundy, to mamy zapas na przetworzenie i serializację wyniku.

Jak tłumaczyć timeouty modelowi ChatGPT

Dla ChatGPT ważne jest zwracanie semantycznych błędów, a nie ciche zrywanie połączeń. Zamiast abstrakcyjnego 500 lepiej zwrócić strukturalny błąd MCP, który model będzie mógł zakomunikować użytkownikowi: „Usługa doboru prezentów jest teraz przeciążona, spróbuj jeszcze raz za chwilę” i tak dalej.

To oznacza, że w Gateway przy timeoutach należy:

  1. Wyłapać AbortError albo nasz timeout_….
  2. Uformować odpowiedź MCP z sensownym kodem i krótkim opisem.
  3. Dać modelowi możliwość zdecydowania, jak to wyjaśnić człowiekowi.

Timeouty rozwiązują problem wiszących żądań, ale jeśli zależność zaczęła masowo padać, nie chronią przed lawiną identycznych nieudanych prób. Tu potrzebujemy kolejnej warstwy ochrony – circuit breaker.

3. Circuit breaker: „automat” przeciw umierającym serwisom

Intuicja: dlaczego sam timeout nie wystarcza

Nauczyliśmy się już ograniczać czas oczekiwania pojedynczych wywołań za pomocą timeoutów. Timeout chroni jedno konkretne wywołanie. Ale jeśli zależność „padła na twardo” (np. usługa commerce sypie się przez OOM (Out Of Memory) przy każdym żądaniu), będziemy do niej nadal chodzić, za każdym razem czekać 35 sekund, łapać błąd, obciążać sieć i CPU – i znów czekać.

Circuit breaker (automat) dodaje pamięć: śledzi błędy i timeouty i gdy jest ich zbyt wiele, przestaje w ogóle wysyłać żądania do tego serwisu. Zamiast tego od razu zwraca szybką odmowę lub fallback. Po pewnym czasie ostrożnie próbuje ponownie w trybie half-open.

Klasyczne stany automatu:

  • Closed – wszystko OK, żądania idą.
  • Open – serwis uznany za „martwy”, żądania nie idą, od razu błąd.
  • Half-open – próbujemy ograniczoną liczbę żądań; jeśli się udają – wracamy do closed, jeśli znów padają – ponownie open.

Prosty schemat circuit breaker

Mały diagram:

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: zbyt wiele błędów
    Open --> HalfOpen: upłynął cooldown
    HalfOpen --> Closed: kilka sukcesów z rzędu
    HalfOpen --> Open: znów błędy
    Open --> Open: szybka odmowa

Mini-implementacja circuit breaker w TypeScript

W produkcji zwykle używa się gotowych bibliotek (dla Node.js są np. opossum lub lekkie własne rozwiązania), ale żeby zrozumieć mechanikę, wystarczy kompaktowa klasa.

Przykład skrajnie uproszczonego breakera wokół wywołania modułu commerce:

// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";

export class CircuitBreaker {
    private state: State = "closed";
    private failureCount = 0;
    private nextAttemptAt = 0;

    constructor(
        private readonly failureThreshold = 5,
        private readonly cooldownMs = 30_000
    ) {}

    async call<T>(fn: () => Promise<T>): Promise<T> {
        const now = Date.now();

        if (this.state === "open") {
            if (now < this.nextAttemptAt) {
                throw new Error("circuit_open");
            }
            this.state = "half-open";
        }

        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (err) {
            this.onFailure();
            throw err;
        }
    }

    private onSuccess() {
        this.failureCount = 0;
        this.state = "closed";
    }

    private onFailure() {
        this.failureCount++;
        if (this.failureCount >= this.failureThreshold) {
            this.state = "open";
            this.nextAttemptAt = Date.now() + this.cooldownMs;
        }
    }
}

I użycie w kliencie usługi commerce:

// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);

export async function callCommerce(path: string) {
    return commerceBreaker.call(async () => {
        const res = await fetchWithTimeout(
            process.env.COMMERCE_URL + path,
            { timeoutMs: 3000 }
        );
        if (!res.ok) throw new Error(`commerce_${res.status}`);
        return res.json();
    });
}

Tutaj, gdy commerce zaczyna masowo odpowiadać błędami lub nie wyrabia się przed timeoutem, po kilku niepowodzeniach breaker przechodzi w open. W tym stanie przez cooldownMs w ogóle nie próbujemy chodzić do serwisu i od razu zwracamy błąd circuit_open.

Co powinno widzieć ChatGPT, gdy breaker „odciął” serwis

Z punktu widzenia ChatGPT najlepiej, jeśli:

  • Szybko odpowiadacie błędem MCP „commerce_unavailable” lub „gift_service_overloaded”.
  • Dodajecie zrozumiały opis: „Usługa płatności jest tymczasowo niedostępna, spróbujmy później”.
  • Nie ukrywacie błędu za nieskończonymi ponowieniami.

To właśnie ten przypadek, kiedy „szybka, uczciwa odmowa” jest lepsza niż długie zawieszanie. Zwłaszcza w checkout: użytkownik prędzej zaakceptuje jasny komunikat niż będzie 40 sekund patrzył na spinner i dostanie „coś poszło nie tak”.

Timeouty i breaker chronią nas przed „złymi” lub leżącymi zależnościami, ale nie rozwiązują problemu, gdy jeden typ obciążenia zjada wszystkie zasoby i zaczyna dusić pozostałe części systemu. Do tego potrzebna jest jeszcze jedna warstwa – bulkheads.

4. Bulkheads: izolacja „przedziałów”, aby jeden nie zatopił całego statku

Analogia ze statkiem

Wzorzec bulkhead nazwano od przegrodowych „grodzi” w statku: jeśli w jednym przedziale jest rozszczelnienie, woda nie rozleje się po całym statku. W architekturze oznacza to: podzielić zasoby między różne kierunki pracy, aby jeden przeciążony serwis nie zjadł wszystkiego – CPU, połączeń, pul – i nie położył krytycznych ścieżek.

W mikrousługach zwykle robi się to przez oddzielne:

  • pule połączeń HTTP,
  • pule wątków/workerów,
  • kolejki/topic’i,
  • a nawet oddzielne klastry baz danych dla operacji krytycznych.

Chodzi o to, że jeśli serwis rekomendacji prezentów zacznie działać wolniej i przycinać, wyczerpie tylko swoje zasoby, ale nie złamie checkoutu i autoryzacji.

Bulkheads w świecie Node.js i MCP Gateway

W Node.js nie mamy wątków w klasycznym sensie (jest pętla zdarzeń i workery), ale możemy ograniczać liczbę równoległych zadań dla każdego kierunku.

Przykład: w Gateway są trzy zewnętrzne zależności:

  • Usługa Gift (dobór prezentów, ciężkie wywołania LLM).
  • Usługa Commerce (checkout, ACP).
  • Usługa Analytics (logowanie zdarzeń).

Możemy wprowadzić proste limity na równoczesne żądania do każdej z nich.

Na przykład mały „semafor” do ograniczania równoległości:

// src/gateway/bulkhead.ts
export class Bulkhead {
    private active = 0;
    private queue: (() => void)[] = [];

    constructor(private readonly maxConcurrent: number) {}

    async run<T>(fn: () => Promise<T>): Promise<T> {
        if (this.active >= this.maxConcurrent) {
            await new Promise<void>((resolve) => this.queue.push(resolve));
        }
        this.active++;

        try {
            return await fn();
        } finally {
            this.active--;
            const next = this.queue.shift();
            if (next) next();
        }
    }
}

I użycie dla serwisów:

// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";

const giftBulkhead = new Bulkhead(10);      // do 10 równoległych
const commerceBulkhead = new Bulkhead(3);   // checkout mocno ograniczony
const analyticsBulkhead = new Bulkhead(50); // może być dużo

export async function callGiftWithBulkhead(fn: () => Promise<any>) {
    return giftBulkhead.run(fn);
}

export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
    return commerceBulkhead.run(fn);
}

Dzięki temu nawet jeśli GPT zdecyduje się masowo pytać „zrób mi 30 złożonych doborów prezentów”, będą wykonywane maksymalnie po 10 jednocześnie, a checkout będzie mógł dalej działać, używając własnego limitu.

GiftGenius: jakie „przedziały” chcemy

W GiftGenius rozsądnie jest zrobić oddzielne przedziały dla:

  • Doboru prezentów (ciężkie LLM, mniej krytyczne, mogą zwalniać).
  • Checkout/ACP (superkrytyczne, trzeba chronić maksymalnie).
  • Analityki/logów (ważne, ale mogą trochę poczekać).

W bardziej zaawansowanej architekturze wdrażacie je też jako różne klastry z oddzielnymi zasobami, ale w ramach tego wykładu ważna jest idea: nie pozwalać funkcjom drugorzędnym „zjeść” całego tlenu.

Te trzy wzorce – timeouty, circuit breaker i bulkheads – dotyczą tego, jak wychodzicie na zewnątrz, do swoich zależności. Jest jednak jeszcze jedna klasa zagrożeń dla odporności: strumienie zdarzeń przychodzących, które mogą was zasypać nawet przy idealnie ustawionych wywołaniach wychodzących. Najbardziej typowy przykład – sztormy webhooków.

5. Sztormy webhooków: gdy świat wysyła do was zdarzenia częściej, niż jesteście gotowi

Jak webhooki zachowują się w rzeczywistości

Czwartym źródłem problemów z odpornością są zdarzenia przychodzące: webhooki od ACP, Stripe i innych systemów. To one potrafią urządzić prawdziwy „sztorm”, nawet jeśli macie już ustawione timeouty, circuit breakery i bulkheady.

Webhooki to nie żądania HTTP „na żądanie”, lecz zdarzenia „push” od zewnętrznych systemów (Stripe, ACP, zewnętrzne sklepy itd.). Mają kilka nieprzyjemnych cech:

  • Dostawa co najmniej raz (at-least-once) – czyli duplikaty są nieuniknione.
  • Kolejność dostawy nie jest gwarantowana.
  • Przy błędach lubią ponawiać (retry): najpierw po sekundzie, potem po 10, potem po minucie… aż odpowiecie 2xx.
  • W szczycie (np. podczas wyprzedaży) przychodzą paczkami, tworząc „sztorm”.

Jeśli wasz handler nie jest idempotentny i działa zbyt długo, staje się wąskim gardłem, cała kolejka się zapycha, a ponowienia tylko wzmacniają sztorm. W rezultacie możecie położyć bazę danych, kolejkę, pule workerów – i po łańcuszku resztę systemu.

Podstawowe zasady ochrony przed sztormami

Jest kilka idei, które znacząco zwiększają szanse przetrwania sztormu:

Po pierwsze, queue-first, process-later. W idealnym scenariuszu przychodzący webhook nie powinien wykonywać ciężkiej pracy synchronicznie. Zamiast tego jak najszybciej weryfikuje podpis/format, odkłada zadanie do kolejki i odpowiada 200 OK. Przetwarzanie idzie asynchronicznie w workerze. Jeśli potrzebujecie „szybkiego potwierdzenia” dla ChatGPT, możecie mieć oddzielny tor notyfikacji.

Po drugie, idempotencja handlera. Powtórny webhook dla tej samej operacji nie powinien „utworzyć zamówienia jeszcze raz” ani „pobrać pieniędzy podwójnie”. Zwykle rozwiązuje się to przechowywaniem idempotency key lub eventId i sprawdzaniem, czy to zdarzenie już przetwarzaliśmy.

Po trzecie, rate limiting i circuit breaker po stronie odbiornika. Nawet jeśli nadawca sztormuje, wy możecie:

  • ograniczyć RPS per IP/subskrypcję/endpoint,
  • tymczasowo zwracać 429 lub 503, by spowolnić ponowienia,
  • użyć breakera, by nie lać strumienia w zepsuty downstream (np. bazę zamówień).

Przykład handlera webhooka w Next.js w GiftGenius

Załóżmy, że mamy ACP/płatności, które wysyłają webhook o statusie zamówienia na POST /api/commerce/webhook. Chcemy:

  • szybko przyjąć zdarzenie i odłożyć je do kolejki,
  • nie przetwarzać go synchronicznie,
  • nie psuć się od duplikatów.

Uproszczony przykład (bez weryfikacji podpisu i prawdziwej kolejki – to będzie w modułach o bezpieczeństwie i kolejkach):

// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";

// W tym miejscu mógłby być Redis/kolejka; na razie imitujemy tablicę
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // idempotencja (na potrzeby demo)

export async function POST(req: NextRequest) {
    const event = await req.json();

    const eventId = event.id as string;
    if (processedEvents.has(eventId)) {
        return NextResponse.json({ ok: true, duplicate: true });
    }

    // W rzeczywistości tutaj będzie weryfikacja podpisu i schemy

    inMemoryQueue.push(event); // wrzucamy do kolejki do przetworzenia w tle
    // Pracownik w tle później przetworzy i oznaczy ID jako obsłużone
    return NextResponse.json({ ok: true });
}

Na razie to pseudo-implementacja, ale ważne są dwa punkty:

  1. Część synchroniczna jest maksymalnie lekka.
  2. Zakładamy idempotencję wokół event.id.

W praktyce będziecie:

  • używać zewnętrznej kolejki (SQS, RabbitMQ, Kafka),
  • przechowywać przetworzone zdarzenia w bazie danych,
  • weryfikować podpis webhooka i wersję payloadu,
  • być może stosować oddzielny Bulkhead/Breaker wokół handlera.

Jak to wygląda w kontekście GiftGenius

Dla GiftGenius zintegrowanego z ACP/Stripe przez webhooki, ochrona przed sztormami jest szczególnie ważna w sezonach szczytu (Nowy Rok, Black Friday). Tam jest dużo zdarzeń:

  • tworzenie intentów,
  • potwierdzanie płatności,
  • anulacje,
  • zwroty.

Jeśli wasz handler zacznie się „wydłużać” (np. przez wywołania do zewnętrznego API), grozi wam, że:

  • ACP zacznie ponawiać,
  • zdarzenia przyjdą paczkami,
  • baza zamówień i pula workerów zostaną zapchane.

Wzorzec „queue first” + idempotencja + rate limiting na wejściu właśnie temu służy jako asekuracja.

6. Jak te wzorce działają razem

Złóżmy teraz wszystkie wzorce w jeden scenariusz i zobaczmy, jak działają w realnym flow „Dobierz prezent i od razu złóż zamówienie”.

Rozważmy łańcuch „ChatGPT → Gateway → Gift Service → Commerce → webhooki” na przykładzie scenariusza:

Użytkownik w czacie mówi: „Dobierz prezent i od razu złóż zamówienie”.

  1. Model decyduje się wywołać wasze narzędzie suggest_and_checkout.
  2. Gateway wywołuje usługę gift przez fetchWithTimeout i bulkhead usługi gift.
  3. Jeśli usługa gift wisi – zadziała timeout; breaker wokół niej po pewnej liczbie błędów przejdzie w open, a kolejne żądania będą od razu dostawały błąd MCP „gift_service_unavailable”.
  4. Jeśli gift odpowiada, Gateway wywołuje usługę commerce (znów z timeoutem i osobnym bulkheadem).
  5. Każde problemy z commerce uruchamiają oddzielny circuit breaker, ustawiony ostrzej niż w gift (bo checkout jest krytyczny).
  6. Udane zamówienie skutkuje webhookiem od ACP na wasz /api/commerce/webhook, który odkłada zdarzenie do kolejki i odpowiada szybko; worker w tle przetwarza płatność, a powtórne webhooki z tym samym eventId są ignorowane jako duplikaty.

W efekcie:

  • Wisząca usługa doboru nie kładzie checkoutu.
  • Wiszący commerce nie zamienia wszystkich tool-calls w minutowy spinner – ChatGPT szybko dostaje sensowny błąd.
  • Sztormy webhooków nie łamią waszego głównego toru HTTP.
  • Kontrolujecie miejsca degradacji: lepiej tymczasowo wyłączyć spersonalizowane rekomendacje niż położyć płatności.

7. Mały praktyczny checklist dla waszej aplikacji (w formie narracyjnej)

Jeśli uogólnić, w typowej aplikacji ChatGPT z MCP/Gateway warto przejść kolejno przez następujące pytania.

Najpierw sprawdzacie, czy są timeouty na wszystkich wywołaniach zewnętrznych. Cały kod fetch, zapytania do bazy danych i do LLM powinny używać obudowy w stylu fetchWithTimeout z właściwymi wartościami. Ważne, by nie było miejsc, gdzie żądanie może wisieć w nieskończoność.

Dalej identyfikujecie najbardziej kruche zależności. Zwykle to płatności, ACP, duże zewnętrzne API i czasem wasza baza zamówień. Wokół nich warto dodać circuit breaker, aby ochronić się przed lawiną powtórzeń w oczywiście martwy serwis. Jednocześnie od razu decydujecie, jak ChatGPT ma się zachować, gdy breaker jest w stanie open.

Potem patrzycie na swoje zasoby jak na „przedziały”. Czy wszystko idzie przez jedną pulę połączeń i jedną pulę workerów, czy operacje krytyczne (logowanie, checkout) mają swoje limity równoległości, niezależne od serwisu rekomendacji i analityki. Jeśli nie – dodajecie najprostszą implementację bulkheadów, choćby jako surowy limit zadań równoległych.

Na koniec robicie audyt wszystkich przychodzących webhooków. Sprawdzacie, czy mają idempotency key lub eventId, czy nie próbujecie wykonywać ciężkiej pracy synchronicznie w handlerze HTTP i czy umiecie przetrwać falę ponowień, jeśli wasz downstream tymczasowo padnie. Jeśli nie – przenosicie logikę do kolejki i workerów w tle.

Taka sekwencja kroków daje bardzo solidny wzrost odporności nawet bez super złożonej infrastruktury.

8. Typowe błędy przy pracy z timeouts, circuit breakers, bulkheads i sztormami webhooków

Błąd nr 1: brak timeoutów „gdzieś na dole”.
Programiści często ustawiają timeout tylko na Gateway albo tylko na frontendzie, zapominając, że wewnątrz backendu są jeszcze baza danych, zewnętrzne API i LLM. W efekcie zewnętrzne żądanie niby ma timeout 5 sekund, ale wewnątrz jedno wywołanie do bazy lub płatności może wisieć minutami, blokując pulę połączeń i powodując kaskadowe awarie.

Błąd nr 2: gigantyczne timeouty „na wszelki wypadek”.
Czasem ustawia się timeout 60120 sekund: „niech już dociągnie”. W kontekście ChatGPT to prawie zawsze złe. Użytkownik odchodzi, model zaczyna halucynować, a wasze zasoby są cały czas zablokowane. Znacznie lepsza jest uczciwa odmowa po 510 sekundach z jasnym opisem.

Błąd nr 3: circuit breaker bez przemyślanego UX.
Czasem breaker dodaje się „na odczepne”, ale przy jego zadziałaniu użytkownikowi lub modelowi wpada niezrozumiały 500, „ECONNREFUSED” albo „axios error”. W efekcie GPT nie potrafi adekwatnie wyjaśnić, co się dzieje, i zaczyna zmyślać. Warto od razu przemyśleć sformułowania błędów, które będą zrozumiałe i dla ludzi, i dla modelu.

Błąd nr 4: mieszanie zasobów bez podejścia bulkhead.
Klasyczny scenariusz: jeden serwis rekomendacji (albo analityki) zaczyna spowalniać, zjada całą pulę połączeń do bazy lub thread-pool i w ślad za nim padają checkout i logowanie. Wszystko dlatego, że zasoby nie są rozdzielone. Brak choćby elementarnego podejścia bulkhead powoduje, że funkcja drugorzędna może położyć cały produkcyjny system.

Błąd nr 5: traktowanie webhooków jak zwykłych żądań.
Nowicjusze często piszą handler webhooka tak jak zwykły kontroler: długa logika biznesowa, wywołania do zewnętrznych API, brak idempotencji. W warunkach ponowień i duplikatów prowadzi to do podwójnego przetwarzania zdarzeń, dziwnych stanów zamówień i padów pod obciążeniem podczas sztormu.

Błąd nr 6: ignorowanie idempotencji w scenariuszach commerce.
Szczególnie niebezpieczne, gdy webhook płatności może jeszcze raz utworzyć zamówienie albo ponownie zmienić jego stan. Bez sprawdzania idempotency key i przechowywania statusu przetworzenia zdarzenia prędzej czy później dostaniecie podwójne obciążenia albo dziwne duplikaty zamówień.

Błąd nr 7: próba naprawy wszystkiego setTimeoutami i „magicznymi opóźnieniami”.
Czasem chce się ominąć race condition i problemy sztormu przez „poczekaj 100 ms i będzie OK”. W praktyce czyni to zachowanie jeszcze bardziej niestabilnym i wcale nie chroni przed realnymi awariami. Właściwa droga to jawne timeouty, circuit breaker, kolejki i idempotencja, a nie szamanienie z opóźnieniami.

Błąd nr 8: brak priorytetyzacji ścieżek krytycznych.
Gdy checkout i logowanie żyją w tych samych limitach co analityka albo logika rekomendacyjna, każde przeciążenie może tak samo położyć i krytyczne, i drugorzędne części. W odpornym projekcie checkout i auth to „święte krowy”: mają oddzielne zasoby, oddzielne limity, oddzielne alerty i SLO.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION