Architecture

Projektuj pod webhook, który przychodzi dwa razy

Druga kopia zdarzenia płatności nie jest nową pracą. Unikalny klucz, skrzynka wyjściowa i unieważnienie, które nie edytuje przeszłości.

Design for the webhook that arrives twice

Niebezpieczne żądanie to nie pierwsze. To to samo żądanie, które przychodzi znów, bo nadawca nie usłyszał pierwszej odpowiedzi albo usłyszał i i tak spróbował ponownie. Dostawcy płatności tak robią. Kolejki tak robią. Przeglądarka tak robi, gdy ktoś naciśnie przycisk dwa razy. Jeśli system traktuje drugą kopię jak nową pracę, pobierasz dwa razy, wysyłasz dwa razy albo zapisujesz dwie linie księgi za jeden zakład.

Zakładaj duplikat

Projektuję obsługę tak, by drugie dostarczenie było sukcesem, który nic nie robi. Pierwsze wykonuje pracę i zapisuje klucz: id zdarzenia dostawcy albo klucz wysłany przez klienta, unikalny w bazie. Drugie znajduje klucz i zwraca ten sam wynik. Bez drugiej opłaty. Bez drugiego maila. Bez drugiego wiersza.

Ograniczenie unikalności to część, której ufam. Sprawdzenie w kodzie aplikacji, chwilę przed insertem, przegrywa wyścig, gdy dwóch workerów budzi się naraz. Baza tego wyścigu nie przegrywa. Przegrany dostaje konflikt i zamienia go w „już zrobione”.

Pierwsze przyjścieDrugie przyjście
WyszukanieKlucz jest nowyKlucz jest już zapisany
PisaćJeden wiersz, jeden skutek ubocznyŻaden nowy wiersz
OdpowiedziećPrzyjęteTen sam już przyjęty
Jeśli proces umrze w połowieSkrzynka wyjściowa wciąż winna jest wiadomośćNie jest winne drugiego

Wiadomość wychodzi razem z zapisem, albo wcale

Wysłanie maila albo komunikatu kolejki wewnątrz żądania i przed commitem to sposób, by dostać wiadomość o transakcji, która się cofnęła. Zapisuję wiadomość wychodzącą w tej samej transakcji co wiersz biznesowy, a osobny worker ją publikuje. Jeśli proces umrze, wiersz i wiadomość są oba albo nie ma żadnego. Worker może powtórzyć publikację. Obsługa nie może powtórzyć obciążenia.

Unieważnienie to nowy zapis, nie edycja

Gdy coś trzeba cofnąć, nie nadpisuję pierwotnej linii. Zapis wyrównujący mówi, co się zmieniło, i zostawia pierwszą linię czytelną. Tydzień później wciąż możesz odpowiedzieć, w co wierzono o 14:02. Edycja wiersza odpowiada tylko na to, w co wierzy się teraz, i bez świadka.

Jeśli ponowienie może sprawić, że operacja zdarzy się dwa razy, operacja nie jest skończona.

Nazwij klucz, zanim napiszesz handler

Klucz to nie „hash całego żądania”, chyba że sprawdziłeś, co nadawca zmienia między ponowieniami. Dostawca płatności wyśle ponownie to samo id zdarzenia z nowym znacznikiem czasu dostarczenia. Jeśli znacznik jest w kluczu, drugie dostarczenie wygląda na nowe i zbudowałeś błąd, który chciałeś powstrzymać. Używam id, które nadawca obiecuje jako stałe. Jeśli nie obiecuje, wymagam, by wywołujący przysłał własne, i odrzucam wywołanie, które przychodzi bez niego.

Potem zapisuję porażkę w tym samym miejscu co sukces. Konflikt nie jest stroną błędu dla nadawcy. To oczekiwana odpowiedź na ponowienie. Nadawca powinien móc się zatrzymać. Jeśli odpowiem 500, spróbuje trzeci raz i czwarty, a incydent stanie się kolejką tej samej pracy.

Nie uważam tego za skończone, bo szczęśliwa ścieżka zwróciła 200. Test wysyła ten sam ładunek jeszcze raz i sprawdza jeden skutek uboczny. Potem zabija workera między commitem a publikacją i sprawdza, że wiadomość wciąż wychodzi, raz. Te dwa testy są krótsze niż incydent, który zastępują. Zaczynam tam.