Hopp til artikkelen
Raindance

Svaret forsvant. Ble meldingen sendt?

Idempotens, nye forsøk og betydningen av feil i distribuerte systemer.

Raindance

Markdown-versjon

Norsk oversettelse av den engelske originalartikkelen.

Innhold

Sammendrag

En forespørsel får tidsavbrudd. Grensesnittet tilbyr en knapp for å prøve igjen. På den andre siden av forbindelsen kan endringen allerede være utført. Denne artikkelen utvikler en praktisk modell for dette usikre tidsrommet: gi operasjonen en identitet, behold den mellom forsøkene, og lagre resultatet sammen med beviset som gjør det mulig å gjenkjenne et duplikat. Et egenutviklet eksempel med en brukerstøttemelding viser samtidighet, gjenbruk av svar, oppbevaring og grensen mellom databasetransaksjoner og eksterne effekter. Målet er en avgrenset, testbar garanti, ikke en universell påstand om utførelse nøyaktig én gang.

Skjemaet, protokollen, diagrammene og eksemplene nedenfor er foreslåtte undervisningsmodeller. De beskriver verken produksjonshendelser hos Raindance eller en tjeneste som er satt i drift. Kildehenvisningene skiller etablert protokoll- og databaseatferd fra valgene som gjøres i eksemplet.

1. Et tidsavbrudd beskriver kunnskap, ikke utfall

Tenk deg at en utvikler sender en melding til brukerstøtte. Nettleseren sender POST /messages, venter fire sekunder og viser en tilkoblingsfeil. Flere hendelsesforløp er mulige: Forespørselen nådde aldri tjenesten; tjenesten mottok den, men rullet transaksjonen tilbake; eller databasen lagret endringen og svaret forsvant. Et fjerde forløp er fortsatt i gang: Behandlingen fortsetter etter at nettleseren sluttet å vente.

Disse forløpene krever ulike tiltak, men det samme tidsavbruddet kan representere dem alle. MITs undervisningsnotater om RPC beskriver nettopp denne tvetydigheten: Et manglende svar forteller ikke klienten om serveren utførte forespørselen. Eksterne kall kan derfor ikke uten videre overta feilsemantikken fra lokale funksjonskall. [1]

For grensesnittet vårt er bekreftet, avvist og ubekreftet nyttige tilstander. «Mottok ikke bekreftelse» er mer presist enn «meldingen ble ikke sendt». Behold utkastet og operasjonsidentifikatoren så lenge utfallet er ubekreftet. Et nytt forsøk skal gjenoppta den samme innsendingen, ikke opprette en ny i det skjulte.

Klienten sender operasjon K. API-et lagrer meldingen og kvitteringen, men det første svaret forsvinner. Et nytt forsøk med nøkkel K henter den samme kvitteringen.Åpne diagrammet i full størrelse

Tidslinjen skiller databasens lagring fra leveringen av bekreftelsen. Det nye forsøket når tjenesten etter at den første transaksjonen er fullført. Ved å returnere den lagrede kvitteringen kan tjenesten avklare utfallet uten å opprette en ny melding. De to nettverksforsøkene representerer én tilsiktet innsending.

Dette skillet gjør også brukerstøttearbeidet enklere. En operasjonsidentifikator lar en utvikler undersøke en bestemt rekke forsøk uten å be brukeren sende på nytt. Identifikatoren viser imidlertid hvilken operasjon som skal undersøkes, ikke at den lyktes.

2. Definer garantien før du velger mekanismen

HTTP skiller mellom sikre og idempotente metoder. Sikkerhet handler her om hvorvidt klienten ber om en tilstandsendring. Idempotens handler om den tilsiktede effekten av å gjenta en identisk forespørsel. PUT, DELETE og sikre metoder er idempotente etter HTTP-semantikken. Svarene trenger ikke være identiske, og sideeffekter som logging kan fortsatt forekomme. POST har ingen generell idempotensgaranti, men en applikasjon kan definere en for en bestemt operasjon. [2]

Kontrakten vi foreslår, er snevrere og mer nyttig enn «nøyaktig én gang»:

For en autorisert konto, en operasjonstype og en forespørselsnøkkel som fortsatt er lagret, skal nye forsøk med samme godkjente innhold gi høyst én lagret melding og returnere samme applikasjonskvittering.

Hvert forbehold har betydning. Kontoen inngår i navnerommet. Operasjonstypen hindrer kollisjoner mellom ulike endepunkter. Oppbevaringstiden begrenser hvor lenge løftet gjelder. Kravet om likt innhold hindrer at en gjenbrukt nøkkel skjuler en annen hensikt. «Lagret melding» angir effekten som beskyttes; levering av e-post inngår ikke.

Tenk på to identiske meldinger som bevisst sendes med ett minutts mellomrom. En sammenligning av teksten kan ikke avgjøre om den andre er et duplikat. Klienten må skille en ny hensikt fra et nytt forsøk på å fullføre en eksisterende. AWS dokumenterer klientgenererte forespørselsidentifikatorer og kontroll av avvikende parametere som del av sin tilnærming til idempotente API-er. Eksemplet vårt bruker det samme uttrykkelige skillet. [3]

Garantien er betinget. Hvis klienten mister nøkkelen, tjenesten mister kvitteringen eller et nytt forsøk kommer etter det støttede tidsvinduet, gjelder ikke nødvendigvis denne beskyttelsen mot duplikater. Å dokumentere vilkårene er en del av API-utformingen.

3. Gi hensikten en varig identitet

Lag en tilstrekkelig uforutsigbar nøkkel når brukeren først bekrefter innsendingen. Lagre den sammen med det nøyaktige forespørselsinnholdet før nettverksaktiviteten begynner. Følgende nettlesereksempel viser livsløpet. Applikasjonens utkastlager må passe til opplysningenes sensitivitet og kravene til oppbevaring.

typescript
type Submission = {
  key: string;
  body: string;
  createdAt: string;
};

const submission: Submission = {
  key: crypto.randomUUID(),
  body: JSON.stringify({ text: draftText }),
  createdAt: new Date().toISOString(),
};

await draftStore.save(submission); // Before the first attempt.

// Every retry loads this object; it does not generate a new key.
await fetch("/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": submission.key,
  },
  body: submission.body,
  signal: AbortSignal.timeout(4000),
});

Dette er ikke en fullstendig klient for nye forsøk: Validering av svar, policy for legitimasjon og mekanismer for gjenoppretting er utelatt. Eksemplet skal vise at forespørselens identitet finnes utenfor løkken som prøver igjen. At klienten avbryter ventingen, må ikke tolkes som en bekreftet tilbakerulling på serveren.

Serveren må utlede kontotilhørigheten fra verifisert autentisering, ikke fra et JSON-felt brukeren sender inn. Kontroller autorisasjon både ved første utførelse og ved gjenbruk av et tidligere svar. En idempotensnøkkel skal ikke bli en innehaverlegitimasjon som gir tilgang til andres kvitteringer.

Lagre et fingeravtrykk av den godkjente operasjonen sammen med nøkkelen. I dette eksemplet med ett felt defineres fingeravtrykket ut fra en versjonert, kanonisk representasjon av den validerte text-strengen. Angi om forskjeller i mellomrom og Unicode skal telle. Ikke hash vilkårlige JSON-representasjoner uten videre: Feltenes rekkefølge eller standardverdier kan variere selv om operasjonen er den samme.

Unngå samtidig normalisering som fjerner meningsfulle forskjeller. To meldinger med forskjellig mellomrom i et sitat kan være ulike innsendinger. Fingeravtrykket er en likhetskontroll etter API-ets dokumenterte regler, ikke en generell vurdering av semantisk likhet. Hashing anonymiserer heller ikke forutsigbare personopplysninger.

4. Hold meldingen og kvitteringen innenfor samme grense

I denne modellen inneholder én PostgreSQL-transaksjon både selve skrivingen og oppføringen som gjør gjenbruk av svaret mulig. PostgreSQL-transaksjoner grupperer operasjoner slik at alle fullføres eller alle rulles tilbake; mellomtilstander vises ikke som fullførte resultater. [4]

API-et bruker én PostgreSQL-transaksjon for en idempotenskvittering, en melding og en valgfri utboksoppføring. En ekstern e-postleverandør er utenfor transaksjonen.Åpne diagrammet i full størrelse

Grensen omfatter både beviset på at innsendingen er akseptert og selve meldingen. Et prosesskrasj før commit etterlater ingen av delene som lagret resultat. Et tapt svar etter commit kan etterlate begge, selv om API-behandleren ikke kan bekrefte suksess. En eventuell varslingsjobb hører hjemme i transaksjonen som en varig registrert hensikt; den eksterne leveringen gjør det ikke.

Her er et bevisst lite kvitteringsskjema. payload_hash representerer det avtalte fingeravtrykket. Primærnøkkelen lar databasen håndheve unikhet på tvers av det tredelte identifikasjonsområdet. PostgreSQL støtter slike sammensatte begrensninger. [5]

sql
CREATE TABLE operation_receipts (
  account_id uuid NOT NULL,
  operation text NOT NULL,
  request_key uuid NOT NULL,
  payload_hash bytea NOT NULL,
  response_status smallint,
  response_body jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (account_id, operation, request_key),
  CHECK (
    (response_status IS NULL AND response_body IS NULL)
    OR
    (response_status IS NOT NULL AND response_body IS NOT NULL)
  )
);

At svaret kan være null, gjør det mulig å reservere nøkkelen før meldingen opprettes. Det er ikke grunnlag for å fullføre transaksjonen med en uferdig kvittering. Transaksjonshjelperen må rulle tilbake ved ethvert unntak og bare utføre commit når kvitteringen er komplett. Skjemaet alene håndhever ikke denne applikasjonsinvarianten. En produksjonsløsning bør begrense databasetilgangen og teste alle måter transaksjonen kan avsluttes på.

Lagre en liten applikasjonskvittering, for eksempel en meldingsidentifikator og tidspunktet for aksept. Unngå å gjenbruke vilkårlige HTTP-hoder, utløpende sesjonskapsler eller genererte hemmeligheter. Bygg transporthodene på nytt etter gjeldende sikkerhetspolicy. Hvis svarets innhold er privat, kreves fortsatt autorisasjon selv om kvitteringen ikke kan endres.

5. La samtidige duplikater møtes i databasen

En innledende SELECT etterfulgt av en ubeskyttet innsetting er ikke nok: To behandlere kan begge se at oppføringen mangler. I modellen vår prøver konkurrerende forespørsler først å sette inn kvitteringsnøkkelen med ON CONFLICT DO NOTHING. Bare transaksjonen som fikk reservasjonen, kan opprette meldingen.

Pseudokode
BEGIN transaction at READ COMMITTED

INSERT receipt(scope, key, fingerprint)
  ON CONFLICT DO NOTHING
  RETURNING key

if this transaction inserted the receipt:
    insert message
    construct receipt from stored message id and acceptance time
    update reserved receipt with response status and body
    assert exactly one complete receipt was written
else:
    SELECT existing receipt in a NEW statement
    if missing: ROLLBACK and enter bounded reconciliation
    if fingerprint differs: ROLLBACK and return conflict
    if receipt incomplete: ROLLBACK and report invariant failure
    use existing receipt

COMMIT
return receipt only after commit acknowledgement

Den separate lesingen er bevisst. Ved PostgreSQL-isolasjonsnivået Read Committed kan INSERT ... ON CONFLICT DO NOTHING møte en konflikt med en rad som ikke var synlig i SQL-setningens øyeblikksbilde. En senere setning får et nytt øyeblikksbilde og kan se den lagrede raden. Dette må ikke forenkles til en antatt databaseoppskrift som fungerer likt overalt. [6]

Sett grenser for venting på låser og kjøring av SQL-setninger. Hvis en annen forespørsel er aktiv lenger enn ventebudsjettet tillater, returnerer API-et et dokumentert, ubekreftet utfall som kan forsøkes avklart på nytt. Det starter ikke en ny skriving av meldingen. Vranglåser og midlertidige databasefeil krever gjenoppretting på transaksjonsnivå, ikke at man fortsetter midt i en avbrutt transaksjon.

Opprydding av kvitteringer gir enda en kappløpssituasjon: Raden som ga konflikten, kan bli slettet før den påfølgende lesingen. Behandle dette som et behov for avklaring, ikke som tillatelse til å konstruere suksess. Den enkleste undervisningsmodellen utelater opprydding i testperioden. En løsning som skal settes i drift, må samordne oppbevaring med aktive operasjoner og beskrive oppførselen ved grensen.

Til slutt er også en manglende bekreftelse på COMMIT tvetydig. Koble til igjen og avklar utfallet med samme nøkkel. Anta aldri at et unntak fra databasedriveren beviser at ingen transaksjon ble lagret.

6. Velg hva et gjentatt svar betyr

Kvitteringen vår betyr «meldingen ble akseptert», ikke «meldingen finnes fortsatt i sin opprinnelige form». Meldingen kan senere bli arkivert eller fjernet. Å returnere akseptkvitteringen på nytt skal ikke gjenopprette den. Skill historikken om aksept fra oppslag i nåværende tilstand.

Ulike API-er velger ulike regler for gjenbruk av svar. Dokumentasjonen for Stripe API v1 sier at den lagrer første status og svarinnhold etter at utførelsen har startet, også ved 500-svar. Den beskriver også parametersammenligning og sletting av nøkler etter minst 24 timer. Dette er konkrete kontraktsvalg, ikke universelle følger av idempotens. [7]

I vår hypotetiske meldingstjeneste kommer validering og autorisasjon før reservasjon. En vellykket aksept er varig. En transaksjon som med sikkerhet ble rullet tilbake, etterlater ingen suksesskvittering. En usikker commit må avklares. Uventede interne feil skal ikke uten videre lagres som suksess eller gi grunnlag for å bytte nøkkel.

Bruk maskinlesbare utfallskoder sammen med forståelige forklaringer. For eksempel betyr payload_mismatch at samme nøkkel nå beskriver annet innhold, outcome_unconfirmed at bekreftelse ikke er tilgjengelig, og accepted inneholder den lagrede identifikatoren. Dette er foreslåtte applikasjonskoder, ikke standardiserte HTTP-felt.

Klientgrensesnittet må være like presist. «Prøv å bekrefte igjen» beholder operasjonen. «Send en ny melding» oppretter en ny etter en uttrykkelig beslutning. En automatisert klient trenger det samme skillet: Å lage en helt ny plan etter et tidsavbrudd må ikke i det skjulte gi forespørselen en ny identitet.

7. Gjør nye forsøk innenfor et budsjett

Riktig håndtering av duplikater gjør ikke ubegrenset trafikk ufarlig. AWS beskriver hvordan nye forsøk kan forsterke overbelastning, og hvordan tilfeldig variasjon, eller jitter, fordeler forsøk som ellers ville kommet samtidig. De anbefaler også å begrense antall forsøk og unngå at uavhengige tjenestelag multipliserer dem. [8]

Definer en samlet tidsfrist, maksimalt antall forsøk, tidsgrense per forsøk og hvilke utfall som tillater et nytt forsøk. Et avvist innhold skal ikke inn i samme løkke som en avbrutt forbindelse. En utløpt autorisasjonskontekst krever egen håndtering. Hvis API-et angir Retry-After, må forsinkelsen ses sammen med tiden som er igjen, fremfor å sende før fristen. HTTP tillater både en dato og en relativ ventetid. [2]

Følgende funksjon illustrerer bare full jitter. Den avgjør ikke om det er trygt å gjenta en forespørsel:

typescript
function retryDelayMs(attempt: number): number {
  const baseMs = 250;
  const capMs = 4000;
  const ceiling = Math.min(capMs, baseMs * 2 ** attempt);
  return Math.floor(Math.random() * ceiling);
}

Tallene er eksempler, ikke målte anbefalinger. Velg verdier ut fra tjenestens svartider, belastning og krav til brukeropplevelse. Kontroller avbrudd og gjenværende tid både før ventingen og før sending. Gjenbruk det lagrede innholdet og nøkkelen. Når budsjettet er brukt opp, behold den ubekreftede operasjonen og tilby avklaring fremfor å erklære at den ikke finnes.

Avklar hvilket lag som har ansvar for nye forsøk. Hvis nettleser, gateway, SDK og worker alle prøver igjen, kan det samlede antallet bli langt større enn noe enkelt lag forventer. Logg nok ufølsomme korrelasjonsopplysninger til å knytte forsøkene sammen, uten å kopiere forespørselsinnholdet inn i vanlige driftslogger.

8. Oppbevaring er del av korrekthetskontrakten

En kvittering opptar plass og kan inneholde personopplysninger. Det er ikke automatisk ønskelig å lagre alle nøkler for alltid. Eksemplet trenger derfor et oppgitt tidsvindu for nye forsøk, en faktisk slettepolicy og en måte å avklare eldre, usikre operasjoner på.

Anta at kontrakten lover gjenkjenning i sju dager. En bærbar datamaskin som kobler seg til etter ti dager, kan ikke anta at den gamle nøkkelen fortsatt beskytter mot duplikater. Når serveren har slettet kvitteringen, kan en tidligere brukt nøkkel se ny ut. Et tidsstempel i en upålitelig forespørsel løser ikke dette alene.

I eksemplet stopper klientene automatiske innsendinger før det støttede tidsvinduet er slutt. Eldre usikkerhet sendes til et autorisert historikkoppslag eller menneskelig avklaring. Hvis produktet må kunne avvise alle utløpte operasjoner pålitelig, kreves ytterligere tilstand eller verifiserbare opplysninger om utstedelse, med egne livsløp. Dette endrer modellen; det er ingen gratis egenskap ved UUID-er.

Skill oppbevaring av kvitteringer fra oppbevaring av meldinger. Å slette den ene kan påvirke gjenoppretting uten å slette den andre. Sikkerhetskopier, eksporter og observasjonslagre trenger også forenlige regler. Beskriv hvilken klokke som starter tidsvinduet, om nye forsøk forlenger det, og hva som skjer ved grensen.

9. Eksterne effekter trenger sin egen avtale

Legg nå til et e-postvarsel. Sender man det før commit, kan mottakeren bli varslet om en melding som senere rulles tilbake. Sender man etter commit, oppstår et annet gap: Et krasj kan hindre varslingen, eller et usikkert leveringsresultat kan føre til at den sendes igjen.

En utvidelse av eksemplet bruker en utboksoppføring som lagres i samme transaksjon som meldingen. En separat worker leser oppføringene og sender varslinger. Hensikten om å varsle blir dermed varig sammen med aksepten. E-postleverandøren blir ikke en del av databasetransaksjonen.

Bruk en stabil varslingsidentifikator på tvers av leveringsforsøk. Hvis leverandøren støtter egnet deduplisering, samordne nøkkelområdet og oppbevaringen med workerens gjenopprettingsvindu. Hvis den ikke gjør det, må muligheten for doble varsler anerkjennes, og innholdet bør være ufarlig å motta flere ganger. Ikke kall hele arbeidsflyten «nøyaktig én gang» bare fordi første databaseskriving er beskyttet.

Den samme analysen gjelder når man belaster et kort, oppretter en skyressurs eller skriver til en annen database. Tegn hver grense der en bekreftelse må krysse mellom systemer. Spør ved hver overgang hva som overlever et krasj, hvem som eier operasjonsidentifikatoren og hvordan usikkerhet avklares. En pålitelig lokal transaksjon er et fundament, ikke en erstatning for disse separate avtalene.

10. Test usikkerheten, ikke bare normalforløpet

Den mest nyttige testen avbryter svaret etter en vellykket commit. Prøv igjen med samme nøkkel og kontroller at det finnes én melding og at samme kvittering returneres. Det tester brukerens opprinnelige problem direkte.

Utvid testen til en liten matrise av feil:

Inngrep Påkrevd observasjon i denne modellen
Samtidige, identiske forespørsler Én lagret melding; like kvitteringer
Samme nøkkel med endret tekst Konflikt; ingen ny melding
Krasj før commit Ingen delvis lagret aksept
Tapt bekreftelse på commit Avklaringen beholder den opprinnelige nøkkelen
En annen autentisert konto Ingen kvitteringer utleveres på tvers av kontoer
Klienten startes på nytt Lagret nøkkel og innhold overlever sammen
Grensen for sletting av kvitteringer Dokumentert gjenoppretting; ingen falsk garanti
Bekreftelse på varsling forsvinner Separat leveringspolicy testes

Kjør samtidighetstestene mot den faktiske databasemotoren og det aktuelle isolasjonsnivået. Et simulert datalag som alltid svarer sekvensielt, kan ikke fastslå at kappløp håndteres. Fremprovoser feil bevisst og kontroller varig tilstand, ikke bare HTTP-status. Test også avbrudd, ugyldige fingeravtrykk, oppbrukte budsjetter og tilbakekalt tilgang før gjenbruk av svar.

Gi testoppsettet en tydelig invariant: Innenfor det støttede tidsvinduet skal hver aksepterte operasjon ha høyst én lagret melding, og hver suksesskvittering skal peke på denne meldingen. Registrer den faktiske hendelsesrekkefølgen slik at feil kan gjenskapes. Testen kan da variere forsinkelser rundt reservasjon, innsetting, commit og svarlevering uten å avhenge av én heldig rekkefølge. Skill tilgjengelighet fra korrekthet: Hvis alle forespørsler får tidsavbrudd, kan unikheten fortsatt være bevart samtidig som produktet er ubrukelig. Mål både bekreftet fullføring og antall uavklarte operasjoner.

Da kan driftsspørsmålet besvares: ikke «Lyktes nettverksforespørselen?», men «Hva vet vi om denne identifiserte operasjonen, og hvilken trygg handling reduserer usikkerheten som gjenstår?» Det gir utviklere, grensesnitt og automatiserte klienter et felles grunnlag for gjenoppretting.

Referanser

Primærkildene ble kontrollert 19. oktober 2026. Arkitekturen og koden er egne forklarende eksempler; de presenteres ikke som produksjonsmålinger.

  1. MIT 6.5840: Threads and RPC, 2026. Forklarer hvorfor manglende RPC-svar gir usikkerhet om utførelsen.
  2. IETF RFC 9110: HTTP Semantics. Definerer sikre metoder, idempotens og Retry-After.
  3. Malcolm Featonby, AWS: Making retries safe with idempotent APIs. Førstehåndskilde om forespørselsidentitet, samsvarende hensikt og gjenbruk av svar.
  4. PostgreSQL 18: Transactions. Beskriver den lokale atomisitetsgrensen som eksemplet bruker.
  5. PostgreSQL 18: Constraints. Dokumenterer sammensatt unikhet og deklarative integritetskontroller.
  6. PostgreSQL 18: Transaction Isolation. Spesifiserer Read Committed-øyeblikksbilder og konfliktatferd.
  7. Stripe API: Idempotent requests. Viser en konkret API v1-kontrakt for gjenbruk av svar og oppbevaring.
  8. Marc Brooker, AWS: Timeouts, retries, and backoff with jitter. Forklarer budsjetter for nye forsøk, belastningsforsterkning og jitter.

Mer fra Innsikt

Minne er ikke fullmakt.