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ście | Drugie przyjście | |
|---|---|---|
| Wyszukanie | Klucz jest nowy | Klucz jest już zapisany |
| Pisać | Jeden wiersz, jeden skutek uboczny | Żaden nowy wiersz |
| Odpowiedzieć | Przyjęte | Ten sam już przyjęty |
| Jeśli proces umrze w połowie | Skrzynka 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.
