feat(dlr): replace MVStore with PostgreSQL persistence - #309
Draft
lykakis wants to merge 23 commits into
Draft
Conversation
lykakis
force-pushed
the
feature/remove-mvstore
branch
from
September 3, 2026 12:59
24f08ad to
8504860
Compare
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
lykakis
force-pushed
the
feature/remove-mvstore
branch
from
September 4, 2026 13:07
e742c13 to
04a18b1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR replaces Sendium's MVStore-based DLR persistence with a PostgreSQL-backed lifecycle for provider correlation and downstream HTTP/SMPP receipt delivery. Startup and HTTP ingress remain fail-closed; downstream SMPP uses early UUID acceptance with internal persistence retries.
It adds:
(provider_name, provider_message_id)pair.It removes MVStore, H2, backend selectors, local-file configuration, and volatile persistence fallback. Sendium-owned DLR persistence remains disabled by default in reusable
sendium-coreand is enabled explicitly bysendium-app.Downstream delivery metrics and a new structured DLR event schema are intentionally deferred to a separate observability design and PR. Existing PostgreSQL storage metrics and
message.deliver.*events remain.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.
Architecture
Component Responsibilities
DlrMessageStorage/DlrStorageDlrServiceManagedDlrStoragePostgresqlDlrStorageMessageStateStandardMessageTrackerForwardDlrServiceStandardSmppServerMessageStoreDlrDeliveryBatchdeliver_smparts belonging to one delivery attempt.DlrStorageReadinessCheckPersistence is a build-time application boundary controlled by
sendium.dlr.persistence.enabled. It is enabled in standalonesendium-app; applications embeddingsendium-coremust opt in and provide the named datasource and Flyway configuration.Persisted Model
PostgreSQL contains two main tables:
sendium_dlr.dlr_messagestores one lifecycle row per Sendium gateway message ID. It contains ingress metadata, downstream target, provider outcome, delivery status, retry schedule, timestamps, and fenced attempt number.sendium_dlr.provider_correlationmaps(provider_name, provider_message_id)to the gateway message ID. Provider scoping allows different providers to reuse the same message ID independently.Provider outcome and downstream delivery are separate concerns. A provider can have completed the message while Sendium still has a pending HTTP callback or SMPP
deliver_smto deliver.The first terminal receipt stores the exact provider outcome and consumes every correlation for the gateway message in one transaction. Concurrent terminal receipts can therefore resolve the lifecycle only once.
End-to-End Flows
HTTP Submission to HTTP Callback
sequenceDiagram participant Client as HTTP client participant Kannel as KannelResource participant DlrService participant DB as PostgreSQL participant Router participant Provider as Upstream SMPP provider participant Tracker as StandardMessageTracker participant Delivery as ForwardDlrService participant Callback as HTTP callback endpoint Client->>Kannel: GET /sendsms Kannel->>Kannel: validate, assign UUID, create MessageState Kannel->>DlrService: save initial state DlrService->>DB: insert WAITING_PROVIDER row alt Initial persistence fails DB-->>DlrService: unavailable DlrService-->>Kannel: persistence failure Kannel-->>Client: HTTP 503, not routed else Initial state committed DB-->>DlrService: persisted DlrService-->>Kannel: persistence succeeded Kannel->>Router: enqueue message alt Router admission is interrupted Router-->>Kannel: admission failure Kannel-->>Client: HTTP 503 else Router admission succeeds Router-->>Kannel: queued Kannel-->>Client: HTTP 202 + gateway UUID Router->>Provider: submit_sm Provider-->>Tracker: submit_sm_resp + provider message ID Tracker->>DlrService: link provider correlation DlrService->>DB: persist provider-scoped correlation Provider->>Tracker: terminal provider DLR Tracker->>DlrService: resolve terminal outcome DlrService->>DB: move HTTP delivery to PENDING DB-->>DlrService: resolution committed Tracker-->>Provider: deliver_sm_resp STATUS_OK loop Up to 10 due delivery attempts Delivery->>DlrService: poll and start fenced attempt DlrService->>DB: claim delivery attempt Delivery->>Callback: GET resolved dlr-url alt HTTP 200-399 Callback-->>Delivery: success Delivery->>DlrService: complete attempt DlrService->>DB: delete lifecycle row else Timeout or non-success response Callback-->>Delivery: failure Delivery->>DlrService: retry in 120s or mark FAILED DlrService->>DB: update lifecycle row end end end endKannelResourcereceives/sendsms, validates the request, and creates the message and initialMessageState.dlr-urlselects the HTTP delivery channel; otherwise the channel isNONE.503and the message is not queued.202with the gateway message ID.submit_sm_resplinks the gateway message to the provider-scoped message ID.NONEdeletes the lifecycle row. ChannelHTTPmoves it toPENDING.ForwardDlrServicepolls up to 100 due rows every second and performs callbacks serially.200through399completes delivery and deletes the row. Redirects are not followed.FAILEDand remains until retention cleanup.SMPP Ingress Architecture
sequenceDiagram participant Client as Downstream SMPP client participant Server as SMPP server worker participant DlrService as DLR service participant DB as PostgreSQL participant Router participant Upstream as Upstream SMPP worker participant Provider as Upstream SMPP provider Client->>Server: submit_sm Server->>Server: fence ingress, validate, and assign gateway UUID alt Rejected before acceptance Server-->>Client: error submit_sm_resp else Accepted Server-->>Client: submit_sm_resp STATUS_OK + UUID loop Until initial state is persisted Server->>DlrService: save accepted event DlrService->>DB: save initial state alt PostgreSQL unavailable DB-->>DlrService: persistence failure DlrService-->>Server: schedule internal retry Note over Client,Server: No second submit_sm_resp else Commit succeeds DB-->>DlrService: persisted DlrService-->>Server: continue accepted event end end Server->>Server: assemble multipart parts if needed Server->>Router: admit persisted message Router->>Upstream: route message Upstream->>Provider: submit_sm Provider-->>Upstream: submit_sm_resp + provider message ID Upstream->>DlrService: link provider correlation DlrService->>DB: persist provider-scoped correlation Provider->>Upstream: deliver_sm with terminal provider DLR Upstream->>DlrService: resolve terminal outcome DlrService->>DB: consume correlation and set SMPP delivery PENDING alt Terminal resolution fails DB-->>DlrService: persistence failure DlrService-->>Upstream: resolution failed Upstream-->>Provider: deliver_sm_resp STATUS_SYSERR else Terminal resolution committed DB-->>DlrService: resolved SMPP delivery DlrService-->>Upstream: durable outcome Upstream-->>Provider: deliver_sm_resp STATUS_OK Upstream->>Router: enqueue internal MSG_DLR Router->>Server: route to matching SMPP server worker alt Original system_id is bound Server->>DlrService: start fenced delivery attempt DlrService->>DB: mark active attempt loop One deliver_sm per original submission ID Server->>Client: deliver_sm Client-->>Server: deliver_sm_resp end alt Every deliver_sm_resp is STATUS_OK Server->>DlrService: complete fenced attempt DlrService->>DB: delete lifecycle row else Timeout, disconnect, NACK, or non-OK response Server->>DlrService: return attempt to PENDING DlrService->>DB: persist retryable state Note over Client,Server: Replay on next bind for the same system_id end else No matching bound session Note over Client,DB: Delivery remains PENDING and replays oldest-first on bind end end endDownstream SMPP Submission to SMPP DLR
submit_sm;registered_deliverydetermines whether a receipt is requested.InTaskgathers up to 100 ingress events for up to 100 ms.submit_sm_respbefore persistence.submit_sm_resplinks the provider message ID, and the first terminal provider DLR resolves the lifecycle.StandardMessageTrackercreates an internalMSG_DLRand routes it to the matching SMPP server worker.deliver_sm, or one per original multipart submission ID.DlrDeliveryBatchtreats all generated PDUs as one fenced delivery attempt.deliver_sm_respsucceeds.generic_nack, wrong response type, non-OK response, or send failure releases the entire attempt back toPENDING.system_idbinds again.Provider Receipt Processing
ACCEPTDandENROUTEreceipts are acknowledged but do not consume correlation.DELIVRDandSEENnormalize toDELIVERED; other terminal outcomes normalize toFAILEDwhile retaining the exact DLR state and error code.STATUS_SYSERR, allowing the provider to retry the receipt.Provider IDs are protected by transaction-scoped advisory locks and canonical gateway-row lock ordering. Reusing an ID within one provider transfers ownership to the newest gateway message; different providers remain isolated.
msg.hash.prefixcan make multiple workers share one provider namespace and must remain stable while correlations are outstanding.Acknowledgement and Failure Contract
503submit_sm_resp503; no second SMPP responsedeliver_sm_respwithSTATUS_SYSERRPENDINGand replays on a later matching bind.For downstream SMPP submission,
STATUS_OKmeans the validated message was accepted into Sendium's in-memory ingress pipeline. Persistence and routing happen asynchronously afterward; the message is never routed before its initial state is persisted.Important Boundaries
See
docs/13-dlr-persistence.mdfor the operational durability contract, retention, migration behavior, and remaining crash windows.Deployment and Migration
dlrdatasource.sendium-dlr-storagereadiness check and storage-operation metrics.Verification
mvnw -pl sendium-core verifygit diff --checkpass.Review Guide
Review the final behavior by concern rather than treating this as only a storage-adapter replacement. Compatibility wiring introduced in earlier stages is removed by finalization stages; the final tree is PostgreSQL-only.
d0ab195throughb58faf20126df5throughe433984f39679bthroughefa5086c442ae2through19de0c8d13655ethrough8f91844144ff19through04a18b1Suggested review order: