Skip to content

feat(dlr): replace MVStore with PostgreSQL persistence - #309

Draft
lykakis wants to merge 20 commits into
mainfrom
feature/remove-mvstore
Draft

feat(dlr): replace MVStore with PostgreSQL persistence#309
lykakis wants to merge 20 commits into
mainfrom
feature/remove-mvstore

Conversation

@lykakis

@lykakis lykakis commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What

This PR replaces Sendium's MVStore-based DLR persistence with a fail-closed PostgreSQL lifecycle for provider correlation and downstream receipt delivery.

It introduces:

  • One sendium_dlr.dlr_message row per gateway message for ingress metadata, terminal provider outcome, downstream HTTP/SMPP payload, delivery status, retry schedule, and fenced attempt number.
  • Provider-scoped correlations keyed by the exact (provider_name, provider_message_id) pair.
  • Final-only provider receipt handling: valid ACCEPTD and ENROUTE receipts are acknowledged without consuming correlation, while the first terminal receipt consumes every correlation for the gateway message.
  • Durable HTTP callback attempts with scheduled PostgreSQL polling, five-second request timeouts, no redirects, 120-second retry delays, and a 10-attempt limit.
  • Acknowledgement-driven downstream SMPP delivery that deletes state only after every multipart deliver_sm_resp succeeds and replays pending rows when the same system_id binds again.
  • Attempt-number fencing so stale completion, retry, and failure callbacks cannot mutate a newer attempt.

The broader persistence replacement also:

  • Uses a versioned Flyway migration and named Agroal datasource.
  • Persists required state before HTTP routing or successful SMPP acknowledgement.
  • Supports shared SMSC namespaces through msg.hash.prefix while allowing different providers to reuse the same message ID independently.
  • Preserves last-write-wins behavior when one provider reuses an ID, with transaction-scoped advisory locks and canonical gateway-row lock ordering.
  • Rejects submissions with retryable protocol responses when PostgreSQL cannot persist required state.
  • Returns SMPP STATUS_SYSERR when a provider-originated terminal receipt cannot be resolved durably.
  • Retains PostgreSQL readiness, storage-operation metrics, retention cleanup, and sanitized diagnostics.
  • Adds PostgreSQL to Quick Start with a private service, persistent volume, generated credentials, health-gated startup, and safe password preservation across regeneration.
  • Supports externally managed PostgreSQL through explicit JDBC configuration.
  • Removes MVStore, H2, backend selectors, local-file configuration, and volatile persistence fallback.
  • Keeps Sendium-owned DLR persistence disabled by default in sendium-core and enables it explicitly in sendium-app.
  • Renames the default tracker and SMPP server store to StandardMessageTracker and StandardSmppServerMessageStore; InMemoryQueueProvider remains because router and worker queues are still process-local.

Downstream delivery metrics and a new structured DLR event schema are intentionally deferred to a separate observability design and PR. This PR retains the existing PostgreSQL storage metrics and existing message.deliver.* events.

Why

Sendium must retain DLR state long enough to correlate provider receipts with the original gateway message and complete downstream HTTP or SMPP delivery after process restarts and transient failures.

The previous local persistence model tied state to one application filesystem and could fall back to volatile storage. It also removed resolved state before downstream delivery was acknowledged, leaving a crash window where a provider receipt could be accepted and then lost before reaching the originating client.

PostgreSQL provides an externally managed durability boundary with transactional transitions, versioned migrations, standard backup and monitoring options, and consistent behavior across application restarts.

Final Lifecycle

  1. Persist the initial gateway message and downstream delivery target before HTTP routing or successful SMPP acknowledgement.
  2. Link each upstream submit_sm_resp ID to the exact provider namespace and gateway message.
  3. Acknowledge intermediate provider receipts without consuming correlation.
  4. On the first terminal provider receipt, atomically store its exact outcome, consume all correlations, and retain the lifecycle row when HTTP or SMPP delivery is required.
  5. Delete the lifecycle row only after a successful HTTP response or successful deliver_sm_resp PDUs for every SMPP receipt part.
  6. Persist HTTP retry scheduling and leave failed SMPP attempts pending for bind-driven replay.

The reusable sendium-core module does not activate this infrastructure automatically. Applications that own or do not use Sendium's DLR persistence lifecycle can embed the core without requiring a DLR database, while the standalone application retains a coherent fail-closed PostgreSQL contract.

Important Boundaries

  • Delivery is at-least-once, not exactly-once. A crash or storage failure after an HTTP receiver accepts a callback or an SMPP client acknowledges a receipt can cause redelivery.
  • Multipart SMPP replay can repeat parts that were acknowledged before another part failed.
  • SMPP replay is bind-driven rather than periodic. A failed attempt remains pending until the matching client binds again or retention removes it.
  • HTTP checks are scheduled every second in non-overlapping serial batches of up to 100. A slow batch delays later due callbacks.
  • HTTP attempts 1 through 9 retry after 120 seconds. Attempt 10 becomes FAILED and remains until retention cleanup.
  • PostgreSQL makes DLR lifecycle state durable; it does not make router queues, worker queues, or multipart submission assembly durable.
  • Provider namespaces must remain stable while correlations are outstanding. Changing msg.hash.prefix or renaming a worker using the default namespace makes earlier receipts unresolvable.
  • Reusing the same message ID within one provider transfers ownership to the newest gateway message. Different providers remain isolated.
  • Active-attempt exclusion is process-local. Multiple active Sendium replicas sharing one database can start duplicate deliveries.
  • The V1 Flyway migration is edited directly because PostgreSQL DLR persistence has not shipped or been applied to a production installation.
  • Existing MVStore files are not imported.

Verification

  • mvnw -pl sendium-core verify
  • 254 unit tests passed.
  • 35 integration tests passed, including PostgreSQL migration, storage, outage, recovery, retention, provider isolation, and concurrency coverage.
  • 18 focused HTTP dispatcher and SMPP output-task tests passed.
  • All 16 Quick Start tests passed, including local/external/local PostgreSQL password preservation and Docker Compose parsing.
  • Packaged JVM PostgreSQL outage and restart recovery passed during the branch acceptance gates.
  • Container-built native PostgreSQL restart recovery passed during the branch acceptance gates.
  • External-consumer startup with Sendium-owned persistence disabled passed.
  • Checkstyle and git diff --check pass.

Review Map

The branch is organized into reviewable stages. Compatibility wiring introduced in earlier stages is removed by finalization stages; the final PR state is PostgreSQL-only.

Pass Focus Commit range
1 Schema, storage boundary, PostgreSQL message state, correlation, and receipt storage 8c85e0f through e3685b3
2 Runtime wiring, storage metrics, health, and fail-closed HTTP/SMPP submission behavior c71c45b through 93aeb46
3 Quick Start deployment, JVM/native restart testing, and operational documentation 1965fe6 through 062a308
4 PostgreSQL default, MVStore removal, and reusable-core persistence boundary ba2c582 through 91d1f54
5 Provider-scoped correlation hardening, adapter naming, and architecture flow 81f58af through 92a75cc
6 Unified durable lifecycle, HTTP retries, SMPP acknowledgement, and final documentation c18ab9e through 24f08ad

Suggested Review Areas

  • Unified lifecycle schema, provider correlation consumption, retention, and fenced transitions.
  • Advisory/message lock ordering and concurrent same-provider correlation rebinding.
  • Final-only provider receipt handling and STATUS_SYSERR retry behavior.
  • HTTP retry, interruption, terminal failure, and at-least-once semantics.
  • SMPP multipart acknowledgement, failure release, disconnect handling, and bind-driven replay.
  • Persistence-before-acceptance behavior for HTTP and SMPP submissions.
  • Quick Start credentials, volume lifecycle, password regeneration, and external database configuration.
  • Final removal of MVStore and optional persistence behavior in sendium-core.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 19, 2026
Comment thread sendium-core/src/test/java/utils/NativeE2eSmoke.java Fixed
storage.linkProviderMessageId(state.getGatewayMsgId(), PROVIDER, providerMessageId);
}

private Optional<MessageState> resolve(MessageState state, String providerMessageId) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant