From 1e9af7566ade0b794c50cd58253d799338a5189a Mon Sep 17 00:00:00 2001 From: pavlos Date: Fri, 18 Sep 2026 12:07:08 +0300 Subject: [PATCH] fix(dlr): retain provider correlations for seven days Signed-off-by: pavlos --- docs/13-dlr-persistence.md | 9 +++--- .../core/worker/PostgresqlDlrStorage.java | 2 +- .../core/worker/PostgresqlDlrStorageIT.java | 30 ++++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/13-dlr-persistence.md b/docs/13-dlr-persistence.md index b0bc79f..9b6fbee 100644 --- a/docs/13-dlr-persistence.md +++ b/docs/13-dlr-persistence.md @@ -118,11 +118,12 @@ A terminal receipt remains in `sendium_dlr.dlr_message` while HTTP or SMPP deliv The V1 retention thresholds are fixed application behavior, not environment settings: -| State | Eligible for cleanup after | +| DLR lifecycle state | Eligible for cleanup after | | :--- | :--- | -| Provider message correlation | 3 days | -| Message waiting for provider receipt | 7 days from creation | -| Pending or failed terminal delivery | 7 days from resolution | +| Waiting for final provider DLR | 7 days from provider acceptance | +| Waiting to forward final DLR to the client | 7 days from receiving the final provider outcome | + +Waiting for a final provider DLR includes both the message row and its provider-message correlations. These are separate database records with separate cleanup conditions, but both use the same seven-day threshold. After Sendium records a final provider DLR or provider rejection, it removes the correlations and retains the message row while forwarding the outcome to the HTTP or SMPP client. Successful client delivery deletes the row immediately; pending or failed forwarding remains eligible for cleanup seven days after provider resolution. Cleanup is triggered by storage activity and runs no more than once per hour. These values are therefore eligibility thresholds, not exact physical deletion deadlines: idle records can remain in the database longer, and an active deployment can retain newly eligible state until the next cleanup pass. A provider receipt cannot be matched after its correlation has been removed. Making the thresholds or cleanup schedule configurable is outside the V1 storage replacement. diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorage.java b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorage.java index 2d2c3fe..aff1458 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorage.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorage.java @@ -218,7 +218,7 @@ WITH due AS ( private static final String DELETE_EXPIRED_CORRELATIONS_SQL = """ DELETE FROM sendium_dlr.provider_correlation - WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '3 days' + WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '7 days' """; private static final String DELETE_EXPIRED_MESSAGES_SQL = """ diff --git a/sendium-core/src/test/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorageIT.java b/sendium-core/src/test/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorageIT.java index 42350dc..c841ce7 100644 --- a/sendium-core/src/test/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorageIT.java +++ b/sendium-core/src/test/java/gr/cytech/sendium/core/worker/PostgresqlDlrStorageIT.java @@ -405,22 +405,19 @@ void retentionUsesCreatedAtWhileWaitingAndResolvedAtAfterResolution() throws SQL } @Test - void correlationRetentionRemainsThreeDays() throws SQLException { + void correlationRetentionMatchesWaitingMessageRetention() throws SQLException { MessageState state = state(MessageState.DeliveryChannel.HTTP, "system", "https://example.test/dlr"); acceptProviderMessage(state, "old-correlation"); - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(""" - UPDATE sendium_dlr.provider_correlation - SET created_at = CURRENT_TIMESTAMP - INTERVAL '4 days' - WHERE provider_name = ? AND provider_message_id = ? - """)) { - statement.setString(1, PROVIDER); - statement.setString(2, "old-correlation"); - statement.executeUpdate(); - } + setCorrelationCreatedAt("old-correlation", "CURRENT_TIMESTAMP - INTERVAL '4 days'"); PostgresqlDlrStorage cleanup = new PostgresqlDlrStorage(dataSource, 0); assertThat(cleanup.getState(state.getGatewayMsgId())).isPresent(); + assertThat(countCorrelations(state.getGatewayMsgId())).isOne(); + + setCorrelationCreatedAt("old-correlation", "CURRENT_TIMESTAMP - INTERVAL '8 days'"); + + cleanup = new PostgresqlDlrStorage(dataSource, 0); + assertThat(cleanup.getState(state.getGatewayMsgId())).isPresent(); assertThat(countCorrelations(state.getGatewayMsgId())).isZero(); } @@ -501,6 +498,17 @@ SELECT COUNT(*) FROM sendium_dlr.provider_correlation WHERE gateway_message_id = } } + private void setCorrelationCreatedAt(String providerMessageId, String expression) throws SQLException { + String sql = "UPDATE sendium_dlr.provider_correlation SET created_at = " + expression + + " WHERE provider_name = ? AND provider_message_id = ?"; + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, PROVIDER); + statement.setString(2, providerMessageId); + statement.executeUpdate(); + } + } + private void setCreatedAt(String gatewayMsgId, String expression) throws SQLException { updateTimestamp(gatewayMsgId, "created_at", expression); }