Timeout mówi o czekaniu, nie o wyniku
Klikasz „Zarezerwuj”. Po kilku sekundach dostajesz . Czy możesz bezpiecznie kliknąć jeszcze raz? Serwer mógł niczego nie zapisać, ale mógł też zakończyć zapis i zgubić odpowiedź po drodze. Te sytuacje wyglądają identycznie dla klienta.
Zanim dodasz , odpowiedz na inne pytanie: jak rozpoznasz tę samą intencję użytkownika? Samo ponowienie żądania nie odpowiada na to pytanie. Dla operacji tworzącej rezerwację nowy przy każdej próbie może oznaczać nową rezerwację.
Rysowanie diagramu…
Tekst diagramu
sequenceDiagram
participant C as Klient
participant A as API
C->>A: Rezerwacja, klucz attempt-42
A->>A: Zapis wyniku
A--xC: Odpowiedź utracona
C->>A: Ponowienie, ten sam klucz
A->>C: Ten sam wynikMały przykład, który można uruchomić
oznacza tutaj, że powtórzenie tej samej operacji nie tworzy kolejnego skutku. Zapisz kod jako retry.py i uruchom python3 retry.py ( 3.11+). Nie potrzebujesz bazy ani sieci. To celowo jednoprocesowy model reguły, a nie gotowy mechanizm płatności lub rezerwacji.
responses: dict[str, tuple[int, str]] = {}
created: list[str] = []
def create_booking(key: str, seats: int) -> str:
if not key or type(seats) is not int or seats < 1:
raise ValueError("invalid input")
if key in responses:
previous_seats, booking_id = responses[key]
if previous_seats != seats:
raise ValueError("key reused with different input")
return booking_id
booking_id = f"booking-{len(created) + 1}"
created.append(booking_id)
responses[key] = (seats, booking_id)
return booking_id
first = create_booking("attempt-42", 2) # Imagine this response was lost.
second = create_booking("attempt-42", 2)
assert first == second
assert len(created) == 1
print(second, len(created))
try:
create_booking("attempt-42", 3)
except ValueError as error:
print(error)
else:
raise AssertionError("Conflicting input must be rejected")Oczekiwany wynik:
booking-1 1
key reused with different inputPierwsze wywołanie tworzy wynik i zapamiętuje go pod kluczem. Drugie zwraca ten wynik. Gdy ten sam klucz przychodzi z inną liczbą miejsc, odrzucamy sprzeczne dane: nie udajemy, że są ponowieniem poprzedniej intencji.
Zmień jedną rzecz i zobacz różnicę
W drugim wywołaniu zamień attempt-42 na attempt-43. first == second ma się nie udać. Właśnie pokazałeś różnicę między ponowieniem operacji a nową operacją. Przywróć klucz. Następnie zastanów się, kiedy aplikacja kliencka powinna go utworzyć i jak zachować go między próbami.
Klucz nie zastępuje . W rzeczywistym systemie jego zakres musi uwzględniać użytkownika lub oraz rodzaj operacji. Cudzy klucz nie może umożliwiać odczytania cudzej rezerwacji.
Gdzie ten przykład przestaje wystarczać
Słownik w pamięci znika po restarcie. Dwa procesy mają dwa słowniki. Dwa współbieżne żądania mogą zobaczyć brak wpisu i oba wykonać skutek. Dlatego produkcyjna implementacja potrzebuje trwałego zapisu i atomowej ochrony przed konfliktem, a nie tylko instrukcji if.
Oddziel też zapis lokalny od wywołania zewnętrznego dostawcy. w Twojej bazie nie wycofa już pobranej płatności. Musisz znać dostawcy, sposób sprawdzania stanu i zasady ponawiania. Nie zakładaj, że każdy nagłówek z kluczem ma takie same gwarancje.
Krótka lista decyzji przed wdrożeniem
- Co identyfikuje intencję i kto zachowuje klucz między próbami?
- Co zrobisz z tym samym kluczem i innymi danymi?
- Co stanie się przy dwóch równoczesnych żądaniach oraz restarcie?
- Jak długo przechowasz wynik i co klient zrobi po tym czasie?
- Jaki jest limit ponowień i jak rozpoznasz nieznany wynik?
Sprawdź te pytania na jednej operacji, zanim dodasz ogólny mechanizm retry do całej aplikacji. Najważniejszym dowodem jest pojedynczy skutek, nie liczba udanych odpowiedzi HTTP.
Źródła
SPRAWDŹ SWOJĄ INTUICJĘ

