Alle artikels

Wanneer synchroon te traag is: een async inbox voor betrouwbare integratie

Een event-driven architectuur is snel, tot je een systeem moet aanroepen dat dat niet is. Dan krijg je een synchrone HTTP-call midden in een asynchrone keten, met een timeout als zwakste schakel.

Deze blog bekijkt zo'n situatie: een integratie waarbij een trage verwerking in het ERP kan leiden tot dubbele orders. En een patroon dat dat oplost en zich goed leent voor een herbruikbare library: de AsyncInbox.

Het probleem: timeout, retry en dubbele orders

Neem een klassieke opzet. Een warehouse-systeem publiceert een event op Kafka zodra een zending klaarstaat. Een consumer (bijvoorbeeld een anti-corruption layer) leest dat event en roept via HTTP de integratielaag van het ERP aan. Die maakt er een transportorder van.

Het probleem: de verwerking in het ERP duurt soms langer dan de HTTP-timeout van 90 seconden. De consumer ziet een timeout, gaat ervan uit dat het mislukt is en probeert opnieuw. Maar de eerste verwerking loopt gewoon door.

Het resultaat: twee transportorders voor één vrachtwagen. Een timeout zegt namelijk niets over de uitkomst. Hij zegt enkel dat je het antwoord niet hebt afgewacht.

Waarom de voor de hand liggende fixes niet werken

De eerste reflex is een grotere timeout. Dat verschuift het probleem alleen: er komt altijd een dag waarop de verwerking net iets langer duurt. En een thread die minutenlang wacht, is een slechte plek om state bij te houden.

De tweede reflex is idempotentie aan de ontvangende kant. Dat helpt, maar niet volledig. Een check op "bestaat deze order al?" werkt tegen een afgeronde verwerking. Twee runs die tegelijk lopen, zien elkaar niet.

De derde reflex is retries uitschakelen. Dan ruil je dubbele orders in voor verloren orders. Ook geen winst.

De echte oorzaak zit dieper: een langlopend proces wordt behandeld alsof het een request-response is.

De oplossing: async request-response met persist-before-ack

De oplossing is de synchrone call op te splitsen in twee asynchrone stappen: een bevestiging van ontvangst, en later een apart resultaat.

  1. De consumer leest het event van Kafka en slaat het eerst op in een eigen database. Pas daarna commit hij de offset (persist-before-ack). Zo gaat er niets verloren als de consumer crasht.
  2. De consumer stuurt het event via HTTP naar de integratielaag. Die bevestigt enkel de ontvangst, binnen ongeveer een seconde.
  3. De integratielaag bewaart het verzoek intern en verwerkt het asynchroon richting ERP.
  4. Het eindresultaat komt terug via een apart Kafka-topic. De consumer koppelt het aan het opgeslagen event en sluit het af.
Warehousepubliceert event Kafkaevents-topic Consumermet AsyncInbox Integratielaagbevestigt ontvangst ERPtrage verwerking Kafkaresultaat-topic Inboxstatus per event 2. verzoek ack < 1 s 1. persist-before-ack 3. async verwerking 4. resultaat sluit het event af
De HTTP-call bevestigt enkel de ontvangst. Het eindresultaat komt apart terug via Kafka en sluit het event in de inbox af.

De HTTP-call is nu kort en voorspelbaar. Een timeout betekent weer wat je zou verwachten: de ontvangst is niet bevestigd, dus opnieuw sturen is veilig.

Twee extra regels houden het geheel beheersbaar. De consumer begrenst het aantal openstaande verzoeken per topic, zodat het ERP niet overspoeld wordt. En hij bewaart de volgorde per key: een volgend event voor dezelfde zending wacht tot het vorige afgerond is.

Statussen en expiratie: wat als het antwoord nooit komt

Asynchroon werken verplaatst de vraag: niet "wat als het te lang duurt?", maar "wat als het resultaat nooit komt?". Elk opgeslagen event krijgt daarom een expliciete status.

Status Betekenis
QUEUED Opgeslagen, wacht op een vrije plaats
DISPATCHING Wordt naar de integratielaag gestuurd
AWAITING_RESULT Ontvangst bevestigd, wacht op het resultaat
COMPLETED Resultaat ontvangen en verwerkt
SKIPPED Bewust niet verwerkt
AWAITING_RESULT_EXPIRED Geen resultaat binnen de termijn

Voor uitblijvende resultaten zijn er twee onafhankelijke drempels. De eerste stuurt een alert naar support: dit duurt langer dan normaal. De tweede zet het event op AWAITING_RESULT_EXPIRED en geeft de plaats vrij, zodat de rest van de stroom niet blokkeert.

Bewust zonder automatische retry. Een verlopen event kan in het ERP alsnog verwerkt zijn. Opnieuw sturen zonder te kijken, brengt het oorspronkelijke probleem terug. Herstel is daarom manueel: eerst de staat in het ERP controleren, dan pas opnieuw versturen.

Van oplossing naar library: AsyncInbox

Wie dit patroon uitwerkt, merkt dat het sterk lijkt op een bekend patroon voor foutafhandeling. Een error inbox slaat mislukte events op, houdt een status bij en laat support ze opvolgen en opnieuw versturen.

De AsyncInbox gebruikt dezelfde basis. Opslag, statusbeheer, zoeken op business key en manuele replay zijn grotendeels gedeeld; het overgrote deel van de functionaliteit. Nieuw zijn vooral de koppeling met het resultaat-topic, de concurrency-limieten en de expiratie.

Dat maakt het patroon herbruikbaar. Elke integratie met een trage, langlopende verwerking kan het overnemen zonder het opnieuw uit te vinden.

Lessen

  • Een timeout is geen fout, het is onwetendheid. Behandel hem nooit als bewijs dat iets mislukt is.
  • Langlopend werk hoort niet in een synchrone call. Splits bevestiging en resultaat.
  • Persist-before-ack maakt je consumer veilig tegen crashes en herstarts.
  • Ontwerp voor het antwoord dat nooit komt. Expliciete statussen en expiratie maken het zichtbaar en opvolgbaar.
  • Automatische retry is niet altijd veilig. Waar de uitkomst onzeker is, is gecontroleerd manueel herstel de betere keuze.

De kern: een bestaand patroon lost met een kleine uitbreiding een hele klasse van integratieproblemen op.

Loop je tegen een gelijkaardig probleem aan? Ik denk graag mee.

Neem contact op