Skip to content

Group-commit durable publishing + dedicated 'msgflux' schema - #2

Merged
imihalcea merged 8 commits into
mainfrom
feat/group-commit-durable-buffer
Jun 28, 2026
Merged

imihalcea merged 8 commits into
mainfrom
feat/group-commit-durable-buffer

Conversation

@imihalcea

Copy link
Copy Markdown
Owner

Résumé

Corrige une fenêtre de perte at-least-once dans le DurableBuffer et déplace la table durable dans un schéma Postgres dédié.

1. Group-commit durable publishing (fenêtre de perte fermée)

Auparavant, un publish at-least-once était acquitté dès la mise en buffer mémoire, alors que le flush vers le store n'avait lieu que par intervalle/seuil. Un crash (ou un shutdown non ordonné vis-à-vis du store) dans cette fenêtre perdait silencieusement des messages déjà signalés comme publiés.

  • AddAsync ne se complète qu'après la persistance effective du batch (group-commit).
  • Les publishes concurrents sont coalescés en un seul batch → le débit du batching est conservé, sans fenêtre de perte.
  • Sur échec de persistance, l'erreur remonte à l'appelant (qui republie) — pas de retry interne silencieux.
  • Backpressure bornée : MaxBufferedMessages (défaut 1000). Au-delà, le publish attend → mémoire bornée, plus de risque d'OOM (le buffer était non borné).
  • Flush de shutdown ordonné : DurableBuffer devient IHostedService, son flush final s'exécute pendant l'arrêt du host, tant que le store est vivant.

2. Schéma Postgres dédié msgflux

La table passe de public.msgflux_messages à msgflux.messages (schéma créé via CREATE SCHEMA IF NOT EXISTS, requêtes qualifiées). Suppression du schema.sql mort/divergent.

Tests

  • Unitaires (group-commit, coalescing, backpressure, propagation d'échec).
  • Intégration de bout en bout contre un vrai Postgres : ack seulement après commit, batch consommé exactly-once, flush au shutdown (le scénario d'origine).
  • Core 44/44, Postgres 26/26, Integration 7/7, build clean.

⚠️ BREAKING — migration des déploiements existants

CREATE SCHEMA IF NOT EXISTS msgflux;
ALTER TABLE public.msgflux_messages SET SCHEMA msgflux;
ALTER TABLE msgflux.msgflux_messages RENAME TO messages;

API : ajout de WithMaxBufferedMessages(int) ; suppression déjà actée de WithPollingBatchSize reflétée dans la doc.

Ionut added 8 commits June 23, 2026 10:27
… window

DurableBuffer previously acked the publisher as soon as a message was buffered
in memory, while the actual flush to the store happened only every interval or
every N messages. A crash (or an unordered shutdown vs. the store) in that
window silently dropped messages already reported as published — violating
at-least-once.

Switch to a group-commit model: AddAsync only completes once the batch
containing its messages is durably persisted. Concurrent publishes are coalesced
into a single batch (one flush in flight at a time), so batching throughput is
preserved without a loss window. On persist failure the error surfaces to the
caller (no silent internal retry). A bounded number of in-flight messages
(MaxBufferedMessages, default 1000) applies backpressure instead of buffering
unbounded. The final flush now runs via IHostedService during the host's ordered
shutdown, while the store is still alive.
Three end-to-end integration tests over Testcontainers PostgreSQL:
- a buffered publish is acknowledged only after its row is committed (group-commit);
- a batched burst is persisted and each message is consumed exactly once;
- messages still in the buffer at shutdown are flushed, not silently dropped
  (the scenario that motivated the change).
The inbox table now lives at msgflux.messages instead of public.msgflux_messages:
the schema is created (CREATE SCHEMA IF NOT EXISTS msgflux) and every query is
schema-qualified. Indexes live in the schema with the table; the COPY temp table
(_msgflux_bulk) and the purge advisory-lock key are unaffected.

Also realigned the unused schema.sql, which had drifted from the real DDL
(message_id was TEXT instead of UUID), to the source-of-truth schema.

BREAKING: existing deployments must migrate. For an existing table:
  CREATE SCHEMA IF NOT EXISTS msgflux;
  ALTER TABLE public.msgflux_messages SET SCHEMA msgflux;
  ALTER TABLE msgflux.msgflux_messages RENAME TO messages;
The DDL is owned by SchemaInitializer.SchemaSql; this file was never read at
build or runtime (no embedded resource, no file read) and only drifted from the
real schema. Deleting it removes a misleading duplicate.
- replace the 'No WAL / buffered messages are lost' trade-off with the
  group-commit guarantee (durable publish acked only after the batch is
  committed) and its real trade-off (bounded backpressure);
- note group-commit + coalescing in the durable-delivery flow;
- document the new MaxBufferedMessages option;
- drop the removed PollingBatchSize from the options table and example.
@imihalcea
imihalcea merged commit dc3355e into main Jun 28, 2026
1 check passed
@imihalcea
imihalcea deleted the feat/group-commit-durable-buffer branch June 28, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant