Die gefährliche Anfrage ist nicht die erste. Es ist dieselbe Anfrage, die noch einmal ankommt, weil der Absender die erste Antwort nicht gehört hat oder sie gehört und trotzdem erneut versucht hat. Zahlungsanbieter tun das. Warteschlangen tun das. Ein Browser tut das, wenn jemand den Knopf zweimal drückt. Behandelt das System die zweite Kopie als neue Arbeit, berechnen Sie zweimal, versenden Sie zweimal oder schreiben zwei Buchungszeilen für einen Einsatz.
Rechnen Sie mit dem Duplikat
Ich baue den Handler so, dass die zweite Zustellung ein Erfolg ist, der nichts tut. Die erste macht die Arbeit und speichert einen Schlüssel: die Event-Id des Anbieters oder einen Schlüssel, den der Client schickte, eindeutig in der Datenbank. Die zweite findet den Schlüssel und gibt dasselbe Ergebnis zurück. Keine zweite Abbuchung. Keine zweite Mail. Keine zweite Zeile.
Eine Unique-Constraint ist der Teil, dem ich traue. Eine Prüfung im Anwendungscode, einen Moment vor dem Insert, verliert das Rennen, wenn zwei Worker gleichzeitig aufwachen. Die Datenbank verliert dieses Rennen nicht. Der Verlierer bekommt einen Konflikt und macht daraus „schon erledigt“.
| Erste Ankunft | Zweite Ankunft | |
|---|---|---|
| Nachschlagen | Der Schlüssel ist neu | Der Schlüssel ist schon gespeichert |
| Schreiben | Eine Zeile, ein Nebeneffekt | Keine neue Zeile |
| Antworten | Angenommen | Dasselbe bereits angenommen |
| Wenn der Prozess mittendrin stirbt | Der Ausgangskasten schuldet die Nachricht noch | Es schuldet kein zweites |
Die Nachricht geht mit dem Schreiben, oder gar nicht
Die E-Mail oder die Warteschlangennachricht noch in der Anfrage und vor dem Commit zu senden, ist der Weg zu einer Nachricht für eine Transaktion, die zurückgerollt wurde. Ich schreibe die ausgehende Nachricht in derselben Transaktion wie die Geschäftszeile, und ein eigener Worker veröffentlicht sie. Stirbt der Prozess, sind Zeile und Nachricht entweder beide da oder beide weg. Der Worker darf das Veröffentlichen wiederholen. Der Handler darf die Abbuchung nicht wiederholen.
Eine Stornierung ist eine neue Buchung, keine Änderung
Wenn etwas rückgängig gemacht werden muss, überschreibe ich die ursprüngliche Zeile nicht. Eine Ausgleichsbuchung sagt, was sich geändert hat, und lässt die erste Zeile lesbar. Eine Woche später können Sie noch sagen, was um 14:02 geglaubt wurde. Die Zeile zu ändern sagt nur, was jetzt geglaubt wird, und das ohne Zeugen.
Wenn ein erneuter Versuch die Operation zweimal auslösen kann, ist die Operation nicht fertig.
Benennen Sie den Schlüssel, bevor Sie den Handler schreiben
Der Schlüssel ist nicht „ein Hash der ganzen Anfrage“, es sei denn, Sie haben geprüft, was der Absender zwischen den Versuchen ändert. Ein Zahlungsanbieter sendet dieselbe Event-Id mit einem neuen Zustellzeitstempel erneut. Steht der Zeitstempel im Schlüssel, sieht die zweite Zustellung neu aus, und Sie haben den Fehler gebaut, den Sie verhindern wollten. Ich nehme die Id, die der Absender als stabil verspricht. Verspricht er keine, verlange ich, dass der Aufrufer eine schickt, und ich weise den Aufruf zurück, der ohne ankommt.
Dann schreibe ich den Fehlschlag an dieselbe Stelle wie den Erfolg. Ein Konflikt ist für den Absender keine Fehlerseite. Er ist die erwartete Antwort auf einen erneuten Versuch. Der Absender muss aufhören können. Antworte ich mit 500, versuchen sie es ein drittes Mal und ein viertes, und der Vorfall wird eine Warteschlange gleicher Arbeit.
Ich halte das nicht für erledigt, weil der Glückspfad 200 zurückgab. Der Test schickt dieselbe Nutzlast noch einmal und erwartet genau eine Nebenwirkung. Dann beendet er den Worker zwischen Commit und Veröffentlichung und erwartet, dass die Nachricht trotzdem hinausgeht, einmal. Diese zwei Tests sind kürzer als der Vorfall, den sie ersetzen. Dort fange ich an.
