ID: S202606091024
Status: imported

Tags: avans 2-4 LU1

AD: Hoe gebruiken wij queues?

Context and Problem Statement

De communicatiemodule bestaat uit drie zelfstandige .NET 10 Worker Services die elk in hun eigen Docker container draaien en alleen via gedeelde infrastructuur met elkaar communiceren. De vraag is hoe de asynchrone communicatie tussen deze containers en tussen de OpenMRS plugins en de inbound processor wordt ingericht.

Er zijn twee afzonderlijke queue-lagen in het systeem. De externe queue verbindt de OpenMRS plugins met de inbound processor (container 1). De interne queue verbindt de scheduler (container 2) met de notification worker (container 3). Beide lagen hebben verschillende producenten, consumers en betrouwbaarheidseisen.


Considered Options

Optie A: In-memory queue binnen de applicatie

Gebruik maken van een .NET Channel of ConcurrentQueue zonder externe broker.

Voordelen:

  • Geen extra infrastructuur nodig
  • Eenvoudig te implementeren

Nadelen:

  • Berichten gaan verloren bij herstart of crash
  • Werkt niet tussen separate containers
  • Geen dead-letter mechanisme
  • Onaanvaardbaar voor medische data

Optie B: RabbitMQ met RabbitMQ.Client (.NET) voor beide queue-lagen

Beide queue-lagen gebruiken RabbitMQ als broker. De externe queue verbindt OpenMRS plugins met container 1 via een topic exchange. De interne queue verbindt container 2 met container 3 via een directe queue. RabbitMQ.Client biedt de AMQP-integratie in alle drie .NET Worker Services.

Voordelen:

  • Één technologie voor beide queue-lagen, minder operationele complexiteit
  • Bewezen technologie met officiele .NET client library (RabbitMQ.Client)
  • Ingebouwde ondersteuning voor ACK/NACK en durable queues
  • Berichten gaan niet verloren bij herstart als queues durable zijn geconfigureerd
  • Competing consumers mogelijk voor horizontale schaalbaarheid van container 3
  • Beschikbaar als officiële Docker image met management UI

Nadelen:

  • Extra infrastructuurcomponent die beheerd moet worden
  • Vereist correcte configuratie van durable queues en ACK om berichtenverlies te voorkomen
  • Retry storms mogelijk bij grootschalige uitval als backoff niet correct geconfigureerd is

Optie C: Kafka voor de externe queue, RabbitMQ voor de interne queue

Kafka handelt de hoge-volume externe stroom van OpenMRS events af. RabbitMQ handelt de interne notificatiejobs af.

Voordelen:

  • Kafka is beter geschikt voor hoge-volume event streams met meerdere producers
  • Events blijven beschikbaar na verwerking voor replay

Nadelen:

  • Twee verschillende messaging technologieën verhogen de operationele en ontwikkelcomplexiteit aanzienlijk
  • Kafka is overkill voor het berichtenvolume van een afsprakensysteem
  • Vereist meer operationele kennis dan beschikbaar binnen het team

Decision Outcome

Gekozen: Optie B, RabbitMQ met RabbitMQ.Client (.NET) voor beide queue-lagen

Justification Één messaging technologie voor beide queue-lagen reduceert de operationele complexiteit. RabbitMQ met durable queues en expliciete ACK garandeert dat berichten niet verloren gaan bij een crash van een van de drie containers. Kafka biedt meer functionaliteit dan nodig is voor het berichtenvolume van dit systeem en zou de complexiteit onnodig verhogen.

De RabbitMQ.Client library programmeert tegen een interface in plaats van een implementatie. Alle drie .NET Worker Services gebruiken dezelfde AMQP-configuratiepatronen via de IMessageLoader en IMessageSender interfaces, wat de codebases consistent houdt.


Queue-ontwerp

Externe queue-laag (OpenMRS plugins naar container 1) De topic exchange heet appointment.exchange. Alle OpenMRS plugins publiceren hier naartoe met een routing key volgens het patroon appointment.{tenantId}.{eventType}, bijvoorbeeld appointment.hospital-amsterdam.CREATED of appointment.hospital-nairobi.CANCELLED.

De inbound queue appointment.inbound is gebonden aan het patroon appointment.# en ontvangt alle binnenkomende appointment events van alle tenants. Container 1 consumeert exclusief van deze queue, valideert het event, en schrijft de afspraakdata naar de gedeelde MariaDB database.

Interne queue-laag (container 2 naar container 3) De directe queue notification.queue ontvangt notificatiejobs van container 2 op de geplande verzendmomenten (24 uur en 1 uur voor de afspraak). Container 3 consumeert van deze queue en roept de juiste provider adapter aan. Omdat container 3 horizontaal schaalt (meerdere instanties mogelijk) gebruiken alle instanties competing consumers op dezelfde queue.

De dead-letter queue notification.dlq is beschreven als toekomstig vangnet voor berichten die na drie verwerkingspogingen door container 3 nog steeds falen. (Niet geïmplementeerd in de huidige versie.)

Cancellatie-queue De directe queue cancellation.queue voor annuleringsverzoeken via provider webhooks is beschreven als toekomstige uitbreiding. (Niet geïmplementeerd in de huidige versie; VOIDED events worden verwerkt via de normale externe queue.)

ACK en retry gedrag Elke container bevestigt een bericht pas met ACK nadat de verwerking succesvol is afgerond. Bij een mislukte verwerking wordt NACK gestuurd. Container 3 implementeert applicatie-level exponential backoff met maximaal 3 pogingen bij provider-aanroepen (1s, 2s, 4s). RabbitMQ-level dead-lettering is nog niet geconfigureerd.


Consequences

Good, because:

  • Berichten gaan niet verloren bij een crash of herstart van een van de drie containers
  • De drie containers zijn volledig ontkoppeld in tijd en gedrag en communiceren alleen via de queues en de gedeelde database
  • Container 3 kan horizontaal schalen door meerdere instanties als competing consumers op notification.queue te zetten
  • Één messaging technologie voor beide queue-lagen houdt de operationele complexiteit beheersbaar

Neutraal, because:

  • De routing key conventie van de externe queue moet consistent zijn tussen de OpenMRS plugin en container 1; dit wordt gedocumenteerd in de technische beheerdershandleiding
  • Alle drie containers moeten idempotent zijn: hetzelfde bericht twee keer verwerken mag niet leiden tot dubbele acties

Bad, because:

  • Als de externe RabbitMQ tijdelijk niet bereikbaar is vanuit een OpenMRS-instantie kunnen events verloren gaan; de plugin implementeert retry-logica maar kan geen onbeperkte hoeveelheid events bufferen
  • Bij een storing waarbij alle berichten tegelijk opnieuw worden aangeboden bestaat het risico op een retry storm; dit wordt gemitigeerd door exponential backoff in alle consumers
  • RabbitMQ is een gedeeld infrastructuurcomponent waarvan alle drie containers afhankelijk zijn; downtime van RabbitMQ raakt het volledige systeem

More information

Implementatieaandachtspunten:

  • Er zijn twee afzonderlijke RabbitMQ instanties: één extern bereikbaar voor OpenMRS plugins en één intern alleen beschikbaar binnen het Docker netwerk
  • De RabbitMQ Prometheus plugin is ingeschakeld op beide brokers voor metrics monitoring
  • Queue- en exchange-namen worden geconfigureerd via environment variabelen zodat de drie containers zonder codewijzigingen op verschillende omgevingen draaien
  • Alle drie containers starten pas nadat RabbitMQ gezond is via de Docker Compose health check volgorde
  • Zie ADR 3 voor de motivatie achter de event-driven integratie met OpenMRS
  • Zie ADR 5 voor de plugin architectuur die berichten publiceert naar de externe queue
  • Zie ADR 6 voor de drie-container architectuur beslissing
  • Zie ADR8 voor de beveiligingseisen rondom de RabbitMQ infrastructuur