Aller au contenu

Livraison outbox et rejeu

L'outbox MIC garantit une livraison at-least-once. Elle ne promet jamais exactly-once. L'ordre par agrégat existe, mais il doit être activé explicitement quand le consommateur ne peut pas accepter une inversion.

Mode de débit par défaut

Les stores lisent les événements en attente par priorité puis ordre d'arrivée. Le dispatcher traite le batch séquentiellement et marque chaque événement après publication. Un événement en échec peut donc être dépassé par le suivant.

Avec use_claim=True, un lease place l'événement en traitement. Les leases expirés sont récupérables après la mort d'un worker, et SKIP LOCKED permet aux workers SQL de réclamer des ensembles disjoints.

Un crash après publication mais avant mark_dispatched() peut produire un doublon. Le consommateur doit rester idempotent dans tous les modes.

Ordre par agrégat

Activez le mode ordonné sur le dispatcher :

from mic.outbox import AsyncOutboxDispatcher

dispatcher = AsyncOutboxDispatcher(
    store=store,
    dispatch_fn=publish,
    use_claim=True,
    lease_ttl_seconds=60,
    preserve_aggregate_order=True,
)

La clé d'ordre est le couple (aggregate_type, aggregate_id) déjà porté par OutboxEvent. En mode ordonné, seule la plus ancienne ligne non terminale de chaque agrégat est visible :

  • une tête PENDING en attente de retry bloque ses successeurs ;
  • une tête PROCESSING sous lease les bloque également ;
  • DISPATCHED et DLQ sont terminaux et libèrent la ligne suivante ;
  • les autres agrégats continuent en parallèle.

L'ordre est (created_at, id) dans un agrégat. Une priorité supérieure sur un événement plus récent ne lui permet pas de dépasser sa propre tête. Avec plusieurs workers, combinez toujours ce mode avec use_claim=True.

Cette garantie ne crée ni ordre global entre agrégats, ni exactly-once. Sur une table volumineuse, l'application doit aussi ajouter et mesurer un index adapté à son dialecte sur (aggregate_type, aggregate_id, status, created_at, id).

Rejeu temporel

Le rejeu reste séparé du dispatcher transactionnel. mic.event_replay lit une archive immuable JSON ou ClickHouse, filtre par topic et intervalle inclusif, puis republie dans l'ordre déterministe (created_at, original_id).

mic-events-replay --from-clickhouse http://clickhouse.svc:8123 \
  --topic resource.changed \
  --from 2026-09-29T08:00:00Z \
  --to 2026-09-29T10:00:00Z \
  --to-nats nats://nats-0:4222 --nats-stream events

Chaque publication vers le bus porte x-replay: true, un x-replay-batch-id commun au lot et x-original-event-id comme clé d'idempotence stable. Sans sink, la commande effectue un dry-run. Une erreur de publication interrompt le lot ; une reprise peut donc redélivrer son préfixe.

Choix opérationnel

Besoin Primitive
Publication fiable après transaction OutboxDispatcher + OutboxStore
Dispatch concurrent avec reprise après crash use_claim=True + leases
Ordre strict dans chaque agrégat preserve_aggregate_order=True + claim
Reconstruction depuis l'historique mic.event_replay + archive
Exactly-once ou ordre global Hors garantie MIC ; idempotence et topologie métier requises

Voir aussi Retry sans idempotence.