CodeGym /Kursy /ChatGPT Apps /Zadania asynchroniczne: kolejki, workery, ponowne próby (...

Zadania asynchroniczne: kolejki, workery, ponowne próby (retry)

ChatGPT Apps
Poziom 13 , Lekcja 3
Dostępny

1. Po co w ogóle zadania asynchroniczne w ChatGPT App

Gdyby świat był idealny, każde Twoje narzędzie MCP wyrabiałoby się w kilkaset milisekund. W praktyce wszystko, co najciekawsze — bywa długie i ciężkie:

  • parsowanie dużego pliku CSV z historią zakupów użytkownika;
  • agregacja danych z kilku zewnętrznych API, z których każde raz „śpi”, raz odpowiada 503;
  • budowanie złożonych rekomendacji z masą kroków pośrednich;
  • generowanie obszernych raportów i prezentacji.

Jeśli spróbujesz wcisnąć to w jedno synchroniczne wywołanie narzędzia (tool‑call), natkniesz się na trzy problemy.

Po pierwsze, timeouty. Sesja ChatGPT, infrastruktura HTTP, klient MCP — to wszystko nie jest przygotowane na odpowiedzi „za pięć minut”. Serwer, który utrzymuje połączenie zbyt długo, będzie wyglądał na „zawieszony” zarówno dla ChatGPT, jak i dla użytkownika.

Po drugie, zarządzanie obciążeniem. Jeśli stu użytkowników jednocześnie uruchomi „super‑analizę prezentów na Nowy Rok”, nie chcesz, by serwer MCP synchronicznie trzymał sto długich zadań bezpośrednio w strumieniach HTTP. Potrzebujesz warstwy pośredniej, która potrafi przyjąć skoki ruchu, rozłożyć zadania do kolejki i obsługiwać je przez kilka workerów.

Po trzecie, UX. Użytkownik kliknął przycisk w widżecie GiftGenius i wpatruje się w jeden spinner przez 40 sekund — odczucie jak w starych bankach internetowych. Model „szybka odpowiedź + postęp + możliwość anulowania” jest dużo przyjemniejszy.

Te problemy rozwiązuje wspólny schemat: „start → kolejka → tło → zdarzenia”.

2. Podstawowa architektura async‑job w kontekście MCP

Weźmy nasz GiftGenius. Załóżmy, że pojawił się nowy ciężki scenariusz: „Głęboka analiza preferencji na podstawie historii zakupów i mediów społecznościowych znajomego”. Taka operacja może trwać kilka minut, dlatego:

  1. Narzędzie MCP (tool) przyjmuje parametry żądania od modelu.
  2. Zamiast od razu wszystko liczyć, tworzy rekord Job w bazie.
  3. Umieszcza zadanie w kolejce.
  4. Błyskawicznie odpowiada ChatGPT: „Analiza uruchomiona, oto jobId”.
  5. Worker w tle pobiera zadanie z kolejki, wykonuje ciężką pracę, po drodze wysyła zdarzenia MCP job.progress i job.partial, a na końcu — job.completed lub job.failed.

Z punktu widzenia architektury wygląda to mniej więcej tak:

flowchart LR
    subgraph ChatGPT
      U[Użytkownik] --> GPT[Model + ChatGPT UI]
    end

    GPT -->|call_tool analyze_preferences| MCP[MCP-serwer]

    subgraph Backend
      MCP -->|utworzyć Job| DB[(Baza zadań)]
      MCP -->|enqueue| Q[Kolejka]
      W[Worker] -->|take job| Q
      W -->|update status/progress| DB
      W -->|MCP events: job.progress/job.completed| MCP
    end

    MCP -->|SSE events| GPT

Ważna myśl: serwer MCP nie musi być monolitem. Często pełni rolę fasady nad Twoją wewnętrzną infrastrukturą asynchroniczną: przyjmuje wywołania narzędzi (tool‑calls), tworzy joby i wysyła zdarzenia, a ciężką pracę wykonują osobne procesy‑workery.

3. Model danych dla zadania asynchronicznego

Zaczniemy od prostego modelu Job. Użyjemy TypeScriptu i przykładowego serwera Node/MCP, żeby od razu było widać, jak to wpisuje się w stos technologiczny.

Najprostszy model w pamięci/bazie może wyglądać tak:

// openai/jobs/model.ts
export type JobStatus =
  | 'pending'
  | 'in_progress'
  | 'completed'
  | 'failed'
  | 'canceled';

export interface GiftJob {
  id: string;                // jobId
  type: 'deep_gift_analysis';
  status: JobStatus;
  payload: {
    recipientProfile: string;  // tekst/ID profilu
    budget: number;
  };
  result?: unknown;          // końcowe rekomendacje
  error?: string;            // przyczyna błędu
  attempts: number;          // ile razy próbowano wykonać
  createdAt: Date;
  updatedAt: Date;
}

W realnym projekcie będziesz przechowywać GiftJob w Postgresie, DynamoDB, Firestore lub gdzie indziej, ale dla tej lekcji istotne są pola:

  • status — bieżący stan zadania, który będzie odzwierciedlony i w zdarzeniach, i w UX;
  • attempts — licznik dla retry;
  • error — do logów i debugowania;
  • payload — dane wejściowe, które worker wykorzystuje do przetwarzania.

4. Narzędzie MCP, które tworzy async‑job

Wyobraźmy sobie narzędzie start_deep_analysis. Kiedyś wykonywałoby wszystko synchronicznie, a teraz tylko wkłada zadanie do kolejki i zwraca jobId.

// openai/tools/startDeepAnalysis.ts
import { v4 as uuid } from 'uuid';
import { createJobAndEnqueue } from '../jobs/queue';

// Pseudotypy dla MCP SDK
type StartDeepAnalysisInput = {
  recipientProfile: string;
  budget: number;
};

type StartDeepAnalysisOutput = {
  jobId: string;
  message: string;
};

export async function startDeepAnalysisTool(
  input: StartDeepAnalysisInput
): Promise<StartDeepAnalysisOutput> {
  const jobId = uuid();

  await createJobAndEnqueue({
    id: jobId,
    type: 'deep_gift_analysis',
    status: 'pending',
    payload: {
      recipientProfile: input.recipientProfile,
      budget: input.budget,
    },
    attempts: 0,
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  return {
    jobId,
    message: `Rozpoczęto głęboką analizę. ID zadania: ${jobId}. Będę przesyłać aktualizacje w miarę postępów.`,
  };
}

Ważne jest to, że:

  • narzędzie MCP działa szybko: maksymalnie kilka zapytań do bazy/kolejki;
  • zwraca ustrukturyzowaną odpowiedź z jobId, której ChatGPT może użyć w „wyjaśnieniu dla użytkownika” i którą widżet GiftGenius może zapisać w swoim widgetState.

Twoje JSON Schema dla tego narzędzia po prostu opisuje jobId jako string i message jako tekst czytelny dla człowieka — model zrozumie, że to identyfikator zadania i będzie mógł się do niego odwoływać w kolejnych krokach dialogu.

5. Prosta kolejka i worker: wersja dydaktyczna

Aby nie ciągnąć teraz Redisa, RabbitMQ i całej reszty, zrobimy uproszczoną kolejkę w pamięci. W prawdziwej produkcji będzie to oczywiście osobna usługa (SQS/BullMQ/Cloud Tasks itd.), ale logika pozostanie taka sama.

Najpierw szkic kolejki:

// openai/jobs/queue.ts
import type { GiftJob } from './model';

const jobs = new Map<string, GiftJob>();   // "BD" w pamięci
export const queue: string[] = [];         // uproszczona kolejka po id

export async function createJobAndEnqueue(job: GiftJob) {
  jobs.set(job.id, job);
  queue.push(job.id);
}

export function getJob(id: string): GiftJob | undefined {
  return jobs.get(id);
}

export function updateJob(id: string, patch: Partial<GiftJob>) {
  const job = jobs.get(id);
  if (!job) return;
  const updated: GiftJob = { ...job, ...patch, updatedAt: new Date() };
  jobs.set(id, updated);
}

Teraz prymitywny worker, który okresowo zagląda do kolejki, bierze job i go przetwarza:

// openai/jobs/worker.ts
import { getJob, updateJob } from './queue';
import { emitJobEvent } from './events';

async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  updateJob(jobId, { status: 'in_progress' });
  await emitJobEvent(jobId, 'job.started', {});

  try {
    // Tutaj wywołujemy długą logikę biznesową
    const result = await doDeepGiftAnalysis(job.id, job.payload);

    updateJob(jobId, { status: 'completed', result });
    await emitJobEvent(jobId, 'job.completed', { resultSummary: summarize(result) });
  } catch (err) {
    updateJob(jobId, {
      status: 'failed',
      error: (err as Error).message,
    });
    await emitJobEvent(jobId, 'job.failed', { error: 'Internal error' });
  }
}

I sam „cykliczny” worker, który można uruchomić gdzieś przy starcie aplikacji:

// openai/jobs/workerLoop.ts
import { queue } from './queue';
import { processJob } from './worker';

export function startWorkerLoop() {
  setInterval(async () => {
    const jobId = queue.shift(); // docelowo potrzebna ochrona przed warunkami wyścigu
    if (!jobId) return;

    await processJob(jobId);
  }, 1000); // sprawdzamy kolejkę co sekundę
}

To przykład dydaktyczny. W prawdziwym świecie zamiast setInterval będzie normalna kolejka, która sama „budzi” workera, gdy pojawia się nowe wiadomości. Ale ogólna idea jest jasna: worker jest oddzielony od narzędzia MCP, działa w tle i komunikuje się z serwerem MCP poprzez zdarzenia.

6. Generowanie zdarzeń MCP z workera

W poprzednich lekcjach widziałeś już format zdarzeń MCP: typ (type), unikalny event_id, timestamp, job_id i payload. Teraz pokażemy, jak worker może wywoływać helper emitJobEvent, a ten — dostarczać zdarzenia do ChatGPT przez kanał SSE serwera MCP.

Przykładowy prosty helper:

// openai/jobs/events.ts
import { randomUUID } from 'crypto';
import { sendMcpEvent } from '../mcp/eventBus';

export async function emitJobEvent(
  jobId: string,
  type: 'job.started' | 'job.progress' | 'job.completed' | 'job.failed',
  payload: unknown
) {
  const event = {
    event_id: randomUUID(),
    type,
    job_id: jobId,
    timestamp: new Date().toISOString(),
    payload,
  };

  await sendMcpEvent(event);
}

A sendMcpEvent wewnątrz serwera MCP już wie, jak dokładnie wcisnąć to zdarzenie do SSEServerTransport z MCP SDK: na przykład przez lokalną szynę zdarzeń lub Redis Pub/Sub, jak omawialiśmy w module 12.

Kluczowa myśl: worker nie komunikuje się z ChatGPT bezpośrednio. Rozmawia z serwerem MCP, a ten utrzymuje połączenia SSE i przekazuje zdarzenia do klientów.

7. Postęp i częściowe wyniki (partial results) z workera

Teraz najciekawsze: postęp i częściowe wyniki. W GiftGenius długą analizę można podzielić na etapy:

  • zbieranie i normalizacja danych;
  • budowa podstawowych segmentów;
  • generowanie wstępnych pomysłów na prezenty;
  • finalny ranking i tekstowe objaśnienie.

Na każdym etapie możemy wysyłać job.progress i czasem — job.partial, aby UI mógł już pokazać pierwsze prezenty.

Przykładowy worker:

async function doDeepGiftAnalysis(jobId: string, payload: GiftJob['payload']) {
  await emitJobEvent(jobId, 'job.progress', { step: 1, totalSteps: 4 });

  const normalized = await collectAndNormalizeData(payload);
  await emitJobEvent(jobId, 'job.progress', { step: 2, totalSteps: 4 });

  const roughGifts = await generateInitialGifts(normalized);
  await emitJobEvent(jobId, 'job.partial', { gifts: roughGifts.slice(0, 3) });

  await emitJobEvent(jobId, 'job.progress', { step: 3, totalSteps: 4 });

  const finalGifts = await rerankAndBeautify(roughGifts);
  await emitJobEvent(jobId, 'job.progress', { step: 4, totalSteps: 4 });

  return finalGifts;
}

Widżet, słuchając zdarzeń, może najpierw pokazać 3 „robocze” prezenty z adnotacją „Jeszcze doprecyzowujemy”, a po job.completed — zaktualizować listę i usunąć wskaźnik ładowania. Wszystko to idealnie wpisuje się w wzorce UX, o których mówiliśmy w lekcji 3.

8. Logika retry dla workerów

Teraz najbardziej newralgiczne miejsce: błędy i ponowne próby.

Wyobraź sobie, że worker podczas przetwarzania zadania wywołuje zewnętrzne API listy produktów, a to okresowo odpowiada 500 albo 429. Porzucać zadanie po pierwszym błędzie — to dziwne. Ale i restartować w nieskończoność nie wolno: w ten sposób zrobisz DDoS samemu sobie albo zewnętrznej usłudze.

Potrzebujemy strategii retry z wykładniczym opóźnieniem i ograniczeniem liczby prób.

Zacznijmy od klasyfikacji błędów, która przyda się nam dalej w kursie:

  • tymczasowe (transient) — timeouty, 500, 503, 429;
  • stałe (permanent) — błędne wejście, nieistniejący zasób;
  • fatalne (bug) — błędy w kodzie, TypeError, nieoczekiwany wyjątek.

Powtarzać ma sens tylko błędy tymczasowe. Pozostałe należy uczciwie oznaczać jako 'failed'.

Uprośćmy i zróbmy helper:

// openai/jobs/retry.ts
export function shouldRetry(error: unknown): boolean {
  if (!(error instanceof Error)) return false;
  // Umownie: HTTP 5xx lub 429
  return /5\d\d|429/.test(error.message);
}

export function getDelayMs(base: number, attempt: number): number {
  const jitter = Math.random() * 100;   // niewielki jitter
  return base * 2 ** attempt + jitter; // wykładniczy backoff
}

Teraz zaktualizujemy workera tak, aby uwzględniał attempts w GiftJob:

// openai/jobs/worker.ts
import { getJob, updateJob } from './queue';
import { emitJobEvent } from './events';
import { shouldRetry, getDelayMs } from './retry';

const MAX_ATTEMPTS = 5;

export async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  updateJob(jobId, { status: 'in_progress' });

  try {
    const result = await doDeepGiftAnalysis(job.id, job.payload);

    updateJob(jobId, { status: 'completed', result });
    await emitJobEvent(jobId, 'job.completed', {
      resultSummary: summarize(result),
    });
  } catch (err) {
    const attempts = job.attempts + 1;
    const error = err as Error;

    if (attempts <= MAX_ATTEMPTS && shouldRetry(error)) {
      const delay = getDelayMs(1000, attempts); // 1s,2s,4s...

      updateJob(jobId, { attempts, status: 'pending', error: error.message });

      setTimeout(() => {
        // W prawdziwej kolejce "reenqueue'owałbyś" job z opóźnieniem
        processJob(jobId);
      }, delay);

      await emitJobEvent(jobId, 'job.progress', {
        retry: attempts,
        nextAttemptInMs: delay,
      });
    } else {
      updateJob(jobId, { status: 'failed', error: error.message });
      await emitJobEvent(jobId, 'job.failed', {
        error: 'Nie udało się zakończyć analizy po kilku próbach',
      });
    }
  }
}

Tu ważnych jest kilka kwestii.

Po pierwsze, attempts jest przechowywane w samym zadaniu — to wygodne zarówno do logowania, jak i do obserwowalności (na wykresie będzie ładnie widać, ile zadań przechodzi z retry).

Po drugie, przy każdym retry wysyłamy job.progress z wyraźną informacją, że to próba nr N. Model może wykorzystać tę informację, by wyjaśnić użytkownikowi, że „serwer prezentów odpowiada niestabilnie, próbuję ponownie”.

Po trzecie, gwarantujemy, że w każdym przypadku wyśle się albo job.completed, albo job.failed. Żadnych wiszących zadań „ani żywe, ani martwe”.

Anulowanie ('canceled') — to kolejny ważny status. W przykładach dydaktycznych go nie implementujemy, ale w produkcji zwykle ustawiany jest z inicjatywy użytkownika (przycisk „Anuluj” w widżecie) lub przez timeout. W takim przypadku worker przy następnym pobraniu zadania z kolejki widzi status: 'canceled', nie uruchamia przetwarzania, a serwer MCP wysyła końcowe zdarzenie job.canceled.

9. Idempotentność i retry: nie wdepnij dwa razy w tę samą pułapkę

Gdy wprowadzasz retry, od razu pojawia się ryzyko „zrobienia tego samego dwa razy”. W modułach commerce to krytyczne (np. podwójne obciążenie karty), ale i w GiftGenius są scenariusze, gdzie powtórka jest zła: wysłanie dwóch identycznych maili do znajomego, zduplikowanie wpisu w wewnętrznej analityce itp.

Dlatego kieruj się dwoma zasadami.

Po pierwsze: handler zadania powinien być idempotentny.

Jeśli wywołujesz go z tym samym jobId kilka razy (w ramach retry lub przez pomyłkę), świat nie powinien się „psuć”. W tym celu:

  • wszystkie efekty uboczne (zapis do bazy, wysyłka maili, tworzenie zamówień) powinny być powiązane z jobId lub innym naturalnym identyfikatorem, aby w kodzie można było szybko sprawdzić, czy dany krok był już wykonany;
  • jeśli job.status to już 'completed' albo 'failed', ponowne wywołanie można zignorować albo po prostu zwrócić gotowy wynik.

Przykład prostej ochrony:

export async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  if (job.status === 'completed' || job.status === 'failed') {
    // Zadanie jest już pomyślnie lub ostatecznie zakończone
    return;
  }

  // ... reszta kodu
}

Po drugie: zdarzenia również powinny być idempotentne.

Już mówiliśmy o event_id i o tym, że klient może filtrować duplikaty, ale po stronie serwera też warto uważać: przy restarcie workera czy odtworzeniu z kolejki nie spamuj klienta tymi samymi job.progress bez potrzeby.

10. Gdzie w Twojej architekturze znajdują się kolejki i workery

Na schemacie wszystko wygląda pięknie, ale gdzie fizycznie działa worker? Jest kilka typowych wariantów.

Zintegrowany worker: serwer MCP i worker — to ten sam proces/deploy. Ten sam przyjmuje tool‑calls i uruchamia pętlę workera. Plus to prostota: mniej usług, prostszy deploy. Minus — skalowanie: aby dodać workery, trzeba skalować cały serwer MCP.

Wydzielony worker: serwer MCP — to jedna usługa, workery — druga. Pomiędzy nimi — kolejka i ewentualnie Pub/Sub dla zdarzeń. To właśnie o tym dużo się pisze w kontekście BullMQ/Redis i zdarzeń MCP: serwer MCP subskrybuje kanał Redisa 'mcp:events', workery publikują tam zdarzenia.

Wariant kombinowany: jeden instancja serwera MCP uruchamia też workera, pozostałe instancje — tylko HTTP/SSE. To może być użyteczne, jeśli wdrażasz na Vercel lub innej platformie serverless, gdzie stałe procesy w tle są „średnią przyjemnością”.

W naszym dydaktycznym GiftGenius na razie dopuszczamy pierwszy wariant: serwer MCP + jeden prosty worker w procesie. Gdy dojdziesz do modułów o produkcji i skalowaniu, możesz przenieść workery do osobnej usługi.

11. Przykład: pełny pipeline async w GiftGenius

Przeanalizujmy spójnie, co się dzieje, gdy użytkownik w czacie pisze:

„Potrzebuję złożonego doboru prezentu dla fana kosmosu, z uwzględnieniem jego wcześniejszych zakupów”.

  1. Model decyduje się wywołać narzędzie start_deep_analysis z parametrami profilu obdarowywanego i budżetu.
  2. Narzędzie tworzy GiftJob w bazie ze statusem 'pending', wkłada go do kolejki i zwraca jobId + wiadomość potwierdzającą.
  3. ChatGPT wyjaśnia użytkownikowi, że analiza została uruchomiona, i może przekazać jobId do widżetu GiftGenius.
  4. Widżet subskrybuje zdarzenia dla tego jobId przez SSE, pokazuje pasek postępu i status „Zbieramy i analizujemy dane”.
  5. Worker, widząc nowe zadanie w kolejce, aktualizuje status na 'in_progress' i wysyła job.started.
  6. W trakcie pracy wysyła kilka razy job.progress (etapy) oraz job.partial (pierwsze 23 prezenty).
  7. Jeśli po drodze padnie zewnętrzne API, worker próbuje ponownie z wykładniczym backoffem, aktualizując attempts i wysyłając zdarzenie z informacją o ponownej próbie.
  8. Na koniec wysyła albo job.completed z krótkim podsumowaniem i finalnymi rekomendacjami, albo job.failed z jasnym objaśnieniem.
  9. Widżet na podstawie tych zdarzeń aktualizuje UI, a ChatGPT może przygotować tekstową syntezę i zaproponować follow‑up: „Pokaż więcej pomysłów”, „Zawęź budżet”, „Zmień typ prezentu”.

Z punktu widzenia użytkownika to „żywy”, długi proces, ale pod kontrolą. Z punktu widzenia backendu — normalny pipeline asynchroniczny z kolejką, workerami i retry.

12. Małe ćwiczenie (dla samodzielnej praktyki)

Jeśli chcesz utrwalić materiał, spróbuj dla GiftGenius:

  • zaprojektować schemat tabeli jobs dla prawdziwej bazy: jakie indeksy będą potrzebne, które pola będą używane w filtracji (po użytkowniku, statusie, dacie utworzenia);
  • naszkicować typ TypeScript dla endpointu HTTP /api/jobs/:id, aby widżet mógł w ostateczności robić polling statusu, jeśli SSE jest niedostępne;
  • opisać politykę retry: ile prób, bazowe opóźnienie, co zrobić z zadaniami, które i tak upadły (prosta tabela dead‑letter czy logowanie + alert).

To zadanie przyda się później, gdy w modułach o produkcji i obserwowalności będziemy mówić o metrykach typu „ile zadań utknęło w statusie pending dłużej niż N minut”.

13. Typowe błędy przy pracy z zadaniami asynchronicznymi

Błąd nr 1: wszystko synchronicznie w tool‑call.
Najczęstsza pułapka — spróbować upchnąć całą ciężką pracę w jedno narzędzie MCP bez kolejki. Gdy zapytań jest mało, to niby działa. Gdy tylko rośnie obciążenie albo zewnętrzne API zaczynają zwalniać, łapiesz timeouty, zawieszanie czatu i bardzo nerwowy UX. Każdą operację, która potencjalnie może trwać dziesiątki sekund i dłużej, lepiej od razu projektować jako async‑job z jobId.

Błąd nr 2: brak wyraźnego modelu Job.
Czasem deweloperzy próbują obejść się „samymi wiadomościami w kolejce”, bez przechowywania stanu zadań w bazie. W efekcie trudno odpowiedzieć na podstawowe pytania: „jaki jest status zadania?”, „ile razy próbowaliśmy je wykonać?”, „dlaczego upadło?”. Jasny model Job z polami status, attempts, error, createdAt — to podstawa debugowania, monitoringu i UX.

Błąd nr 3: brak retry albo przeciwnie — nieskończone próby.
Jedni w ogóle nie robią retry i upadają przy pierwszym 500, inni piszą while (!success) i nie ograniczają liczby prób. W pierwszym przypadku tracisz mnóstwo zadań przez krótkotrwałe awarie, w drugim — wywołujesz „sztormy” obciążenia i ryzykujesz zablokowanie zewnętrznych API. Potrzebny jest rozsądny środek: ograniczona liczba prób + wykładnicze opóźnienie + rozróżnienie błędów tymczasowych i stałych.

Błąd nr 4: nieidempotentne handlery.
Jeśli przy każdej próbie, na przykład, tworzysz nowy wpis w systemie zewnętrznym bez sprawdzenia, realizujesz tę samą płatność albo wysyłasz ten sam e‑mail — retry szybko staje się problemem. Handler powinien rozpoznać, że zadanie z tym jobId zostało już doprowadzone do końca, i nie powtarzać niebezpiecznych efektów ubocznych.

Błąd nr 5: brak zdarzeń przy błędach.
Bywa, że worker pada z nieoczekiwanym wyjątkiem, loguje go do konsoli i to wszystko. Użytkownik siedzi i czeka na job.completed, nie wiedząc, że wszystko dawno umarło. Każda ścieżka, w której przetwarzanie zakończyło się błędem, powinna finalnie prowadzić do job.failed i aktualizacji statusu Job w bazie. Bez tego Twoje strumienie MCP zmieniają się w jednostronną „czarną skrzynkę”.

Błąd nr 6: zbyt częste zdarzenia postępu.
Chęć „bycia uczciwym” i wysyłania job.progress przy każdym jednym procencie wykonania prowadzi do przeciążenia sieci, klienta i serwera MCP. Lepiej wysyłać postęp przy zmianie etapu lub większej delcie (np. co 10 %), a resztę trzymać tylko w wewnętrznych logach.

Błąd nr 7: używanie kolejki w pamięci w produkcji.
Przykład dydaktyczny z queue: string[] i Map — dobry do zrozumienia architektury, ale w prawdziwym systemie produkcyjnym rozpadnie się przy pierwszym restarcie procesu czy padzie serwera. Do poważnej eksploatacji potrzebne są zewnętrzne kolejki i magazyny: SQS, Pub/Sub, RabbitMQ, Redis Streams itd. Warianty in‑memory nadają się wyłącznie do lokalnego developmentu i prostych dem.

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