Kontext
Die bisherigen Stories drehten sich um Arbeit, die in unserem eigenen AWS-Konto läuft: eine Queue, ein Scheduler und eigene Mutationen als Einstiegspunkt. Auf der produktiven Plattform, an der ich bei Formilo arbeite, passiert ein Teil der schweren Arbeit aber außerhalb unseres Systems: Externe Job-Anbieter übernehmen die Suche über Karten, das Erzeugen von E-Mails und das Scoring.
Auch diese Jobs müssen in denselben Ablauf passen:
Befehl (Mutation) → dauerhafter Task angelegt → externer Job gestartet → Status-Update → Zustand in der Datenbank aktualisiert → Oberfläche reagiert live
Problem
Läuft die Arbeit woanders, muss uns der Anbieter mitteilen, wenn sich etwas ändert. Webhooks sind dafür der natürliche Kanal, bringen aber eigene Fehlerfälle mit:
- dasselbe Event kann mehrfach ankommen,
- Events können verspätet oder in falscher Reihenfolge eintreffen,
- ein Event kann sich auf einen Task beziehen, den es nicht mehr gibt, oder auf einen Job, der schon fertig ist,
- antworten wir mit einem Fehler, wiederholt der Anbieter,
- und manchmal kommt ein Webhook einfach nie an.
Lösung
Der Ablauf
- Eine eigene Mutation prüft die Anfrage, legt die Task-Einträge an und startet den externen Job.
- DynamoDB hält die dauerhaften Task- oder Batch-Einträge: Zustand, Fortschritt, Zeitstempel, Fehler. Die Job-ID des Anbieters wird am Task gespeichert.
- Der externe Dienst verarbeitet den Job.
- Er sendet Status-Updates an einen Webhook-Endpunkt: REST-Endpunkte in API Gateway, einer pro Jobart.
- Ein Webhook-Handler als Lambda prüft die Nutzlast, bildet den Status des Anbieters auf unseren Task-Zustand ab, aktualisiert die Task-Einträge und lädt bei Bedarf Ergebnisse.
- Die Oberfläche reagiert live über
onUpdate- undobserveQuery-Subscriptions.
Erst benachrichtigen, dann abholen
Meldet der Anbieter einen Job als completed, behandelt der Handler den Webhook als Benachrichtigung, nicht als Ergebnis. Danach holt er das Ergebnis selbst über die API des Anbieters ab. Der Webhook muss nur sagen: „Es hat sich etwas geändert.“
Idempotente Antworten
Der Handler antwortet mit 2xx auf alles, was er verstanden hat, auch auf Events, die er ignoriert. Er gibt dann ausdrücklich „ignoriert“ mit einem Grund zurück:
task_not_found,already_completed,duplicate_completed,invalid_transition.
Veraltete, abgeschlossene und fehlgeschlagene Events werden alle mit 2xx bestätigt, damit der Anbieter sie nicht wiederholt. Ein doppelter oder verspäteter Webhook wird so zur Nulloperation statt zu einer Fehlerschleife, und ein Task wandert nie rückwärts, nur weil ein altes Event zuletzt ankam.
Batch-Jobs melden sich in einer Nutzlast mit batch_id und einer Liste jobs[], sodass eine Anfrage viele Tasks aktualisieren kann.
Ein Sicherheitsnetz für verpasste Webhooks
Webhooks sind keine Garantie. Für Batch-Jobs läuft jede Minute ein Polling-Fallback, ausdrücklich für Probleme bei der Webhook-Zustellung: Er prüft die externen Jobs über die API des Anbieters und repariert Abweichungen durch verpasste Webhooks, fehlgeschlagene Zustellungen oder Events in falscher Reihenfolge.
Abwägungen
- Ein zusätzlicher API-Aufruf pro Abschluss. „Erst benachrichtigen, dann abholen“ kostet eine Anfrage mehr, dafür gibt es einen klaren Weg, ein Ergebnis zu laden.
- 2xx heißt nicht „übernommen“. Weil auch ignorierte Events bestätigt werden, kann der Anbieter nicht erkennen, ob wir ein Update verwendet haben. Diese Information müssen der zurückgegebene Grund und unsere eigenen Logs tragen.
- Polling ist nicht kostenlos. Der Fallback läuft jede Minute. Er ist günstig, existiert aber nur, weil Webhooks allein nicht gereicht haben.
- Mehr Endpunkte, die abgesichert werden müssen. Jeder Anbieter bekommt einen öffentlichen Endpunkt, der eine eigene Prüfung braucht.
Ergebnis
- Externe Jobs nutzen dasselbe Task-Modell und denselben Lebenszyklus wie interne; der Oberfläche ist egal, wo die Arbeit lief.
- Doppelte, veraltete und ungeordnete Webhooks werden bestätigt und ignoriert, statt den Zustand zu verfälschen oder Retries auszulösen.
- Verpasste Webhooks fängt der Polling-Fallback innerhalb von Minuten auf.
Damit war der Kreis geschlossen: Ob die Arbeit in einer Queue, einem Scheduler oder einem externen Dienst läuft – der Einstieg ist ein Befehl, der Zustand liegt in DynamoDB, und die Oberfläche reagiert auf Zustandswechsel.