Architecture

Ontwerp voor de webhook die twee keer aankomt

De tweede kopie van een betalingsgebeurtenis is geen nieuw werk. Een unieke sleutel, een outbox en een void die het verleden niet bewerkt.

Design for the webhook that arrives twice

Het gevaarlijke verzoek is niet het eerste. Het is hetzelfde verzoek dat opnieuw aankomt, omdat de afzender het eerste antwoord niet hoorde, of het hoorde en toch opnieuw probeerde. Betaalaanbieders doen dit. Wachtrijen doen dit. Een browser doet dit als iemand twee keer op de knop drukt. Als het systeem de tweede kopie als nieuw werk behandelt, rekent u twee keer af, verstuurt u twee keer, of schrijft u twee boekregels voor één inzet.

Ga uit van het duplicaat

Ik ontwerp de handler zo dat de tweede aflevering een succes is dat niets doet. De eerste doet het werk en bewaart een sleutel: de event-id van de aanbieder, of een sleutel die de client stuurde, uniek in de database. De tweede vindt de sleutel en geeft hetzelfde resultaat terug. Geen tweede afschrijving. Geen tweede mail. Geen tweede rij.

Een unique constraint is het deel dat ik vertrouw. Een check in de applicatiecode, een moment voor de insert, verliest de race als twee workers tegelijk wakker worden. De database verliest die race niet. De verliezer krijgt een conflict en maakt er “al gedaan” van.

Eerste aankomstTweede aankomst
OpzoekenDe sleutel is nieuwDe sleutel is al opgeslagen
SchrijvenEén rij, één neveneffectGeen nieuwe rij
AntwoordenGeaccepteerdDezelfde al geaccepteerd
Als het proces halverwege sterftDe outbox is het bericht nog verschuldigdHet is geen tweede verschuldigd

Het bericht vertrekt met de schrijfactie, of helemaal niet

De mail, of het wachtrijbericht, binnen het verzoek en vóór de commit versturen is hoe u een bericht krijgt voor een transactie die is teruggedraaid. Ik schrijf het uitgaande bericht in dezelfde transactie als de bedrijfsrij, en een aparte worker publiceert het. Als het proces sterft, zijn de rij en het bericht er allebei of geen van beide. De worker mag het publiceren opnieuw proberen. De handler mag de afschrijving niet opnieuw proberen.

Een void is een nieuwe boeking, geen bewerking

Als iets ongedaan moet, overschrijf ik de oorspronkelijke regel niet. Een tegenboeking zegt wat er veranderde en laat de eerste regel leesbaar. Een week later kunt u nog zeggen wat er om 14:02 geloofd werd. De rij bewerken zegt alleen wat er nu geloofd wordt, en zonder getuige.

Als een nieuwe poging de bewerking twee keer kan laten gebeuren, is de bewerking niet af.

Noem de sleutel voordat u de handler schrijft

De sleutel is niet “een hash van het hele verzoek”, tenzij u hebt gecontroleerd wat de afzender tussen pogingen verandert. Een betaalaanbieder stuurt dezelfde event-id opnieuw met een nieuw aflevertijdstip. Zit dat tijdstip in de sleutel, dan lijkt de tweede aflevering nieuw, en hebt u de bug gebouwd die u wilde voorkomen. Ik gebruik de id die de afzender stabiel belooft. Beloofd die er geen, dan eis ik dat de aanroeper er een stuurt, en ik wijs de aanroep af die zonder aankomt.

Daarna schrijf ik de mislukking op dezelfde plek als het succes. Een conflict is voor de afzender geen foutpagina. Het is het verwachte antwoord op een nieuwe poging. De afzender moet kunnen stoppen. Als ik 500 antwoord, proberen ze het een derde keer, en een vierde, en het incident wordt een wachtrij van hetzelfde werk.

Ik noem dit niet af omdat het gelukkige pad 200 teruggaf. De test stuurt dezelfde payload nog eens en eist één neveneffect. Daarna doodt hij de worker tussen de commit en het publiceren en eist dat het bericht nog vertrekt, één keer. Die twee tests zijn korter dan het incident dat ze vervangen. Daar begin ik.