← Artykuły

Timeout po zapisie: jak ponowić operację bez podwójnego skutku

Odpowiedź zginęła, ale rezerwacja mogła powstać. Prześledź klucz idempotencji, konflikt danych i granice małego przykładu.

Podkreślone pojęcia mają objaśnienia. Kod możesz skopiować i uruchomić u siebie.

Spis treści i pojęcia

Pojęcia pod ręką

Kliknij termin, aby zobaczyć definicję.

Cały słownik →

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ę.

Diagram

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 wynik

Mał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.

python
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:

text
booking-1 1
key reused with different input

Pierwsze 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Ę

Odpowiedź zginęła. Jaki klucz wysłać dla tej samej intencji?

TERAZ TWÓJ KOD

Zrozumienie sprawdź w praktyce.

W module Java przeanalizujesz płatność, nieznany wynik i kompensację.

Otwórz materiał i ćwiczenia →