CodeGym /Kurse /ChatGPT Apps /Asynchrone Aufgaben: Warteschlangen, Worker, Wiederholver...

Asynchrone Aufgaben: Warteschlangen, Worker, Wiederholversuche (Retry)

ChatGPT Apps
Level 13 , Lektion 3
Verfügbar

1. Warum überhaupt asynchrone Aufgaben in der ChatGPT App

Wäre die Welt ideal, würde jedes MCP‑Tool in ein paar hundert Millisekunden fertig werden. In der Realität ist jedoch alles Interessante – langwierig und schwer:

  • Parsing einer großen CSV mit der Einkaufshistorie des Nutzers;
  • Datenaggregation aus mehreren externen APIs, von denen jede mal schläft und mal mit 503 antwortet;
  • Erstellung komplexer Empfehlungen mit vielen Zwischenschritten;
  • Generierung großer Reports und Präsentationen.

Versucht man, das alles in einen einzigen synchronen Tool‑Call zu pressen, stößt man auf drei Probleme.

Erstens: Timeouts. ChatGPT‑Session, HTTP‑Infrastruktur, MCP‑Client – all das ist nicht für Antworten „in fünf Minuten“ gemacht. Ein Server, der die Verbindung zu lange hält, wirkt sowohl für ChatGPT als auch für die Nutzer wie „eingefroren“.

Zweitens: Laststeuerung. Wenn hundert Nutzer gleichzeitig den „Super‑Analyse‑Workflow für Weihnachtsgeschenke“ starten, wollen Sie nicht, dass der MCP‑Server hundert lange Aufgaben synchron in HTTP‑Threads hält. Sie brauchen eine Zwischenschicht, die Lastspitzen aufnehmen, Aufgaben in eine Warteschlange legen und sie von mehreren Workern verarbeiten kann.

Drittens: UX. Der Nutzer klickt im GiftGenius‑Widget auf einen Button und starrt 40 Sekunden lang auf einen einzigen Spinner – das fühlt sich an wie alte Online‑Banking‑Zeiten. Deutlich angenehmer ist das Modell „schnelle Antwort + Fortschritt + Möglichkeit zum Abbrechen“.

Diese Probleme löst man mit einem allgemeinen Schema: „Start → Queue → Hintergrund → Events“.

2. Grundarchitektur eines Async‑Jobs im MCP‑Kontext

Nehmen wir GiftGenius. Angenommen, wir haben ein neues, schweres Szenario: „Tiefgehende Analyse der Präferenzen anhand der Einkaufshistorie und der sozialen Netzwerke eines Freundes“. Das kann mehrere Minuten laufen, daher:

  1. Das MCP‑Tool nimmt die Parameter der Anfrage vom Modell entgegen.
  2. Statt sofort alles zu berechnen, erstellt es einen Eintrag Job in der Datenbank.
  3. Es legt die Aufgabe in eine Warteschlange.
  4. Es antwortet ChatGPT sofort: „Analyse gestartet, hier ist jobId“.
  5. Ein Hintergrund‑Worker holt die Aufgabe aus der Queue, führt die schwere Arbeit aus, sendet dabei MCP‑Events job.progress und job.partial und am Ende – job.completed oder job.failed.

Aus Architektursicht sieht das ungefähr so aus:

flowchart LR
    subgraph ChatGPT
      U[Benutzer] --> GPT[Modell + ChatGPT UI]
    end

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

    subgraph Backend
      MCP -->|Job erstellen| DB[(Job-DB)]
      MCP -->|enqueue| Q[Warteschlange]
      W[Worker] -->|take job| Q
      W -->|Status/Fortschritt aktualisieren| DB
      W -->|MCP events: job.progress/job.completed| MCP
    end

    MCP -->|SSE events| GPT

Wichtiger Gedanke: Der MCP‑Server ist nicht zwingend ein Monolith. Oft fungiert er als Fassade über Ihrer internen asynchronen Infrastruktur: Er nimmt Tool‑Calls entgegen, erstellt Jobs und sendet Events, während die schwere Arbeit separate Worker‑Prozesse erledigen.

3. Datenmodell für eine asynchrone Aufgabe

Starten wir mit einem einfachen Job‑Modell. Wir verwenden TypeScript und einen fiktiven Node/MCP‑Server, damit Sie direkt sehen, wie das in Ihren Stack passt.

Ein einfaches In‑Memory/DB‑Modell könnte so aussehen:

// 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;  // Text/ID des Profils
    budget: number;
  };
  result?: unknown;          // Endempfehlungen
  error?: string;            // Fehlerursache
  attempts: number;          // wie oft wurde versucht, auszuführen
  createdAt: Date;
  updatedAt: Date;
}

In einem realen Projekt speichern Sie GiftJob in Postgres, DynamoDB, Firestore oder anderswo. Für die Vorlesung sind jedoch diese Felder wichtig:

  • status – der aktuelle Zustand der Aufgabe, der sich sowohl in den Events als auch im UX widerspiegelt;
  • attempts – Zähler für Retry;
  • error – für Logs und Debugging;
  • payload – Eingabedaten, die der Worker für die Verarbeitung nutzt.

4. Ein MCP‑Tool, das einen Async‑Job erstellt

Stellen wir uns das Tool start_deep_analysis vor. Früher hätte es alles synchron erledigt, jetzt stellt es nur eine Aufgabe in die Queue und gibt jobId zurück.

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

// Pseudotypen für das 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: `Tiefenanalyse gestartet. Aufgaben-ID: ${jobId}. Ich sende Updates, sobald Fortschritt verfügbar ist.`,
  };
}

Wichtig ist hier:

  • Das MCP‑Tool arbeitet schnell: maximal ein paar DB/Queue‑Aufrufe;
  • es gibt eine strukturierte Antwort mit jobId zurück, die ChatGPT für seine „Erklärung für den Nutzer“ verwenden und die das GiftGenius‑Widget in seinem widgetState speichern kann.

Ihre JSON Schema für dieses Tool beschreibt einfach jobId als String und message als menschenlesbaren Text – das Modell versteht, dass es sich um eine Aufgabenkennung handelt, und kann in weiteren Dialogschritten darauf Bezug nehmen.

5. Einfache Queue und Worker: Lernversion

Um jetzt nicht Redis, RabbitMQ und alles andere mitzuschleppen, bauen wir eine vereinfachte In‑Memory‑Queue. Im Produktivbetrieb ist das natürlich ein separater Dienst (SQS/BullMQ/Cloud Tasks etc.), aber die Logik bleibt dieselbe.

Zunächst das Queue‑Grundgerüst:

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

const jobs = new Map<string, GiftJob>();   // "DB" im Speicher
export const queue: string[] = [];         // vereinfachte Queue nach 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);
}

Jetzt ein primitiver Worker, der periodisch in die Queue schaut, sich einen Job nimmt und ihn verarbeitet:

// 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 {
    // Hier rufen wir die lang laufende Business-Logik auf
    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' });
  }
}

Und der „zyklische“ Worker selbst, den man beim App‑Start irgendwo anwerfen kann:

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

export function startWorkerLoop() {
  setInterval(async () => {
    const jobId = queue.shift(); // eigentlich braucht man Schutz vor Race Conditions
    if (!jobId) return;

    await processJob(jobId);
  }, 1000); // einmal pro Sekunde die Queue prüfen
}

Das ist ein Lernbeispiel. In der Praxis ersetzt eine echte Queue setInterval und „weckt“ den Worker, sobald eine neue Nachricht eintrifft. Aber das Prinzip wird klar: Der Worker ist vom MCP‑Tool getrennt, läuft im Hintergrund und kommuniziert über Events mit dem MCP‑Server.

6. MCP‑Events aus dem Worker generieren

In früheren Vorlesungen haben Sie das MCP‑Event‑Format gesehen: Typ (type), eindeutige event_id, timestamp, job_id und payload. Jetzt zeigen wir, wie der Worker den Helper emitJobEvent aufruft, der die Events über den SSE‑Kanal des MCP‑Servers zu ChatGPT liefert.

Beispiel für einen einfachen 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);
}

Und sendMcpEvent im MCP‑Server weiß bereits, wie man dieses Event in SSEServerTransport aus dem MCP‑SDK einspeist: etwa über einen lokalen Event‑Bus oder Redis Pub/Sub, wie wir in Modul 12 besprochen haben.

Schlüsselerkenntnis: Der Worker spricht nicht direkt mit ChatGPT. Er spricht mit dem MCP‑Server, der die SSE‑Verbindungen hält und die Events an die Clients weiterleitet.

7. Fortschritt und Partial Results aus dem Worker

Jetzt zum Besten: Fortschritt und Teilresultate. In GiftGenius kann man die lange Analyse in Etappen aufteilen:

  • Datensammlung und Normalisierung;
  • Aufbau grundlegender Segmente;
  • Generierung erster Geschenkideen;
  • finales Ranking und textliche Erläuterung.

In jeder Etappe können wir job.progress senden und manchmal – job.partial, damit das UI schon erste Geschenke zeigt.

Ein fiktiver 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;
}

Das Widget kann beim Hören der Events zunächst 3 „vorläufige“ Geschenke mit dem Hinweis „Wir präzisieren noch“ zeigen und nach job.completed die Liste aktualisieren und den Ladeindikator entfernen. Das passt perfekt zu den UX‑Mustern, über die wir in Vorlesung 3 gesprochen haben.

8. Retry‑Logik für Worker

Jetzt zum heikelsten Teil: Fehler und Wiederholversuche.

Stellen Sie sich vor, der Worker ruft beim Verarbeiten einer Aufgabe eine externe Produktlisten‑API auf, die gelegentlich mit 500 oder 429 antwortet. Die Aufgabe nach dem ersten Fehler fallen zu lassen, ist unglücklich. Aber unendlich oft neu zu versuchen, geht ebenso wenig: So DDoSen Sie sich selbst oder den Fremdservice.

Wir brauchen eine Retry‑Strategie mit exponentiellem Backoff und einer Begrenzung der Versuchsanzahl.

Beginnen wir mit einer Fehlerklassifikation, die uns auch später im Kurs nützen wird:

  • vorübergehend (transient) – Timeouts, 500, 503, 429;
  • permanent – ungültige Eingaben, nicht existente Ressourcen;
  • fatal (Bug) – Code‑Fehler, TypeError, unerwartete Exceptions.

Nur vorübergehende Fehler sollte man wiederholen. Alle anderen markiert man fair als 'failed'.

Vereinfachen wir es und bauen einen Helper:

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

export function getDelayMs(base: number, attempt: number): number {
  const jitter = Math.random() * 100;   // kleines Jitter
  return base * 2 ** attempt + jitter; // exponentielles Backoff
}

Jetzt aktualisieren wir den Worker so, dass er attempts in GiftJob berücksichtigt:

// 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(() => {
        // In einer echten Queue würde man den Job mit Verzögerung erneut enqueuen
        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: 'Analyse konnte nach mehreren Versuchen nicht abgeschlossen werden',
      });
    }
  }
}

Hier sind mehrere Punkte wichtig.

Erstens: attempts wird in der Aufgabe selbst gespeichert – praktisch für Logging und Observability (auf dem Graphen sieht man schön, wie viele Aufgaben mit Retries durchlaufen).

Zweitens: Bei jedem Retry senden wir job.progress mit explizitem Hinweis, dass es sich um Versuch Nr. N handelt. Das Modell kann diese Information nutzen, um dem Nutzer zu erklären, dass „der Geschenkserver instabil antwortet, ich versuche es erneut“.

Drittens: Wir garantieren, dass in jedem Fall entweder job.completed oder job.failed gesendet wird. Keine hängenden Aufgaben „weder Fisch noch Fleisch“.

Abbrechen ('canceled') ist ein weiterer wichtiger Status. In den Lernbeispielen implementieren wir ihn nicht, aber im Produktivbetrieb wird er meist durch den Nutzer (Button „Abbrechen“ im Widget) oder per Timeout gesetzt. In diesem Fall sieht der Worker beim nächsten Aufnehmen der Aufgabe aus der Queue den status: 'canceled', startet die Verarbeitung nicht und der MCP‑Server sendet das finale Event job.canceled.

9. Idempotenz und Retry: nicht zweimal in dieselbe Falle tappen

Mit der Einführung von Retry entsteht das Risiko, „dasselbe zweimal zu tun“. In Commerce‑Modulen ist das kritisch (z. B. doppelte Abbuchung), aber auch bei GiftGenius gibt es Szenarien, in denen Doppelungen schlecht sind: zwei identische Mails an den Freund, doppelte Einträge in Ihrer internen Analytics usw.

Orientieren Sie sich daher an zwei Prinzipien.

Erstens: Der Job‑Handler sollte idempotent sein.

Wenn Sie ihn mehrfach mit derselben jobId aufrufen (im Rahmen von Retry oder versehentlich), darf die Welt nicht kaputtgehen. Dafür gilt:

  • alle Side‑Effects (DB‑Schreibvorgänge, E‑Mails, Order‑Erstellung) sollten an die jobId oder eine andere natürliche Kennung gebunden sein, sodass der Code schnell prüfen kann, ob dieser Schritt bereits ausgeführt wurde;
  • wenn job.status bereits 'completed' oder 'failed' ist, kann man den erneuten Aufruf ignorieren oder einfach das bereits vorliegende Ergebnis zurückgeben.

Ein einfaches Schutzbeispiel:

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

  if (job.status === 'completed' || job.status === 'failed') {
    // Aufgabe bereits erfolgreich oder endgültig abgeschlossen
    return;
  }

  // ... restlicher Code
}

Zweitens: Auch Events sollten idempotent sein.

Wir haben bereits über event_id gesprochen und darüber, dass der Client Duplikate filtern kann, aber auch Serverseitig sollte man vorsichtig sein: Beim Neustart des Workers oder bei der Wiederherstellung aus der Queue nicht unnötig mit identischen job.progress‑Events spammen.

10. Wo sich Queues und Worker in Ihrer Architektur befinden

Auf dem Bild ist alles schön – aber wo läuft der Worker physisch? Es gibt einige typische Varianten.

Integrierter Worker: MCP‑Server und Worker sind derselbe Prozess/Deploy. Er nimmt Tool‑Calls entgegen und startet auch die Worker‑Schleife. Pluspunkt: Einfachheit – weniger Services, einfacher zu deployen. Minus: Skalierung – um mehr Worker hinzuzufügen, muss man den gesamten MCP‑Server skalieren.

Dedizierter Worker: Der MCP‑Server ist ein Service, die Worker ein anderer. Dazwischen: eine Queue und ggf. Pub/Sub für Events. Darüber wird viel im Kontext von BullMQ/Redis und MCP‑Events geschrieben: Der MCP‑Server ist auf den Redis‑Kanal 'mcp:events' subscribed, Worker publizieren dorthin Events.

Kombinierte Variante: Eine Instanz des MCP‑Servers läuft mit Worker, die anderen Instanzen sind nur HTTP/SSE. Das kann nützlich sein, wenn Sie auf Vercel oder einer anderen Serverless‑Plattform deployen, wo dauerhafte Hintergrundprozesse eher mühsam sind.

In unserem Lern‑GiftGenius reicht zunächst die erste Variante: MCP‑Server + ein einfacher Worker im Prozess. Wenn Sie die Module zu Produktion und Skalierung erreicht haben, können Sie Worker in einen separaten Service migrieren.

11. Beispiel: kompletter Async‑Pipeline von GiftGenius

Sprechen wir zusammenhängend durch, was passiert, wenn der Nutzer im Chat schreibt:

„Ich brauche eine komplexe Geschenkauswahl für einen Weltraum‑Fan, unter Berücksichtigung seiner früheren Einkäufe.“

  1. Das Modell entscheidet, das Tool start_deep_analysis mit den Parametern für Empfängerprofil und Budget aufzurufen.
  2. Das Tool erstellt GiftJob in der DB mit dem Status 'pending', legt ihn in die Queue und gibt jobId + eine Bestätigungsnachricht zurück.
  3. ChatGPT erklärt dem Nutzer, dass die Analyse gestartet wurde, und kann die jobId an das GiftGenius‑Widget übergeben.
  4. Das Widget abonniert Events für diese jobId über SSE, zeigt einen Fortschrittsbalken und den Status „Wir sammeln und analysieren Daten“.
  5. Der Worker sieht den neuen Job in der Queue, setzt den Status auf 'in_progress' und sendet job.started.
  6. Während der Verarbeitung sendet er mehrfach job.progress (Etappen) und job.partial (die ersten 23 Geschenke).
  7. Fällt unterwegs eine externe API aus, versucht der Worker es mit exponentiellem Backoff erneut, aktualisiert attempts und sendet ein Event mit Informationen zum Wiederholversuch.
  8. Am Ende sendet er entweder job.completed mit kurzer Zusammenfassung und finalen Empfehlungen oder job.failed mit einer verständlichen Erklärung.
  9. Das Widget aktualisiert basierend auf diesen Events das UI, und ChatGPT kann eine Textzusammenfassung bilden und einen Follow‑up vorschlagen: „Mehr Ideen anzeigen“, „Budget eingrenzen“, „Geschenktyp ändern“.

Aus Nutzersicht ist das ein „lebendiger“ langer Prozess unter Kontrolle. Aus Backendsicht – eine normale Async‑Pipeline mit Queue, Workern und Retry.

12. Kleine Übung (für die eigene Praxis)

Wenn Sie den Stoff festigen wollen, versuchen Sie für GiftGenius Folgendes:

  • Entwerfen Sie ein Tabellenschema jobs für eine echte DB: welche Indizes Sie brauchen, welche Felder an der Filterung beteiligt sind (nach Nutzer, nach Status, nach Erstellungsdatum);
  • skizzieren Sie einen TypeScript‑Typ für den HTTP‑Endpoint /api/jobs/:id, damit das Widget im Notfall Status‑Polling machen kann, falls SSE nicht verfügbar ist;
  • beschreiben Sie die Retry‑Policy: wie viele Versuche, Basis‑Verzögerung, was mit Aufgaben passiert, die trotzdem fehlschlagen (einfache Dead‑Letter‑Tabelle oder Logging + Alert).

Diese Aufgabe wird Ihnen später nützen, wenn wir in den Modulen zu Produktion und Observability über Metriken sprechen wie „wie viele Aufgaben hängen länger als N Minuten im Status pending“.

13. Typische Fehler bei der Arbeit mit asynchronen Aufgaben

Fehler Nr. 1: alles synchron im Tool‑Call erledigen.
Die häufigste Falle: Man versucht, die gesamte schwere Arbeit in ein einziges MCP‑Tool ohne Queue zu quetschen. Solange wenige Anfragen anfallen, scheint es zu funktionieren. Sobald die Last steigt oder externe APIs langsamer werden, kassieren Sie Timeouts, ein hängendes Chat‑UI und sehr nerviges UX. Jede Operation, die potenziell Dutzende Sekunden oder länger dauert, sollten Sie direkt als Async‑Job mit jobId entwerfen.

Fehler Nr. 2: kein explizites Job‑Modell.
Manche Entwickler versuchen, mit „nur Nachrichten in der Queue“ auszukommen, ohne den Aufgabenstatus in einer DB zu speichern. Dann wird es schwer, Basisfragen zu beantworten: „Wie ist der Status der Aufgabe?“, „Wie oft haben wir versucht, sie auszuführen?“, „Warum ist sie gefallen?“. Ein klares Job‑Modell mit den Feldern status, attempts, error, createdAt ist die Grundlage für Debugging, Monitoring und UX.

Fehler Nr. 3: kein Retry oder umgekehrt endlose Wiederholungen.
Die einen machen gar kein Retry und fallen beim ersten 500, die anderen schreiben while (!success) und begrenzen die Versuche nicht. Im ersten Fall verlieren Sie viele Aufgaben wegen kurzfristiger Störungen, im zweiten erzeugen Sie „Last‑Stürme“ und riskieren, externe APIs zu blockieren. Nötig ist eine vernünftige Mitte: begrenzte Anzahl von Versuchen + exponentielles Backoff + Trennung von vorübergehenden und permanenten Fehlern.

Fehler Nr. 4: nicht‑idempotente Handler.
Wenn Sie bei jedem Versuch z. B. einen neuen Eintrag in einem Fremdsystem ohne Prüfung erzeugen, dieselbe Zahlung erneut ausführen oder dieselbe E‑Mail noch einmal senden – wird Retry schnell zum Problem. Der Handler muss erkennen können, dass die Aufgabe mit dieser jobId bereits erfolgreich abgeschlossen wurde, und gefährliche Side‑Effects nicht wiederholen.

Fehler Nr. 5: keine Events bei Fehlern.
Es passiert, dass der Worker mit einer unerwarteten Exception fällt, sie in die Konsole loggt – und das war’s. Der Nutzer wartet ewig auf job.completed, ohne zu wissen, dass alles längst gestorben ist. Jeder Pfad, auf dem die Verarbeitung mit einem Fehler endet, muss letztlich zu job.failed führen und den Status des Job in der DB aktualisieren. Ohne das werden Ihre MCP‑Flows zur einseitigen „Black Box“.

Fehler Nr. 6: zu häufige Fortschrittsevents.
Der Wunsch, „ehrlich zu sein“, und job.progress bei jedem einzelnen Prozentpunkt zu senden, überlädt Netzwerk, Client und MCP‑Server. Besser ist es, Fortschritt bei Phasenwechseln oder grober Delta zu senden (z. B. alle 10 %), und alles andere nur in internen Logs zu halten.

Fehler Nr. 7: In‑Memory‑Queue im Produktivbetrieb.
Das Lernbeispiel mit queue: string[] und Map ist gut für das Verständnis der Architektur, aber in einer echten Produktionsumgebung bricht es beim ersten Prozessneustart oder Server‑Crash zusammen. Für ernsthaften Betrieb braucht man externe Queues und Speicher: SQS, Pub/Sub, RabbitMQ, Redis Streams etc. In‑Memory‑Varianten taugen nur für lokale Entwicklung und einfache Demos.

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