diff --git a/charts/rpe-send-letter-service/Chart.yaml b/charts/rpe-send-letter-service/Chart.yaml index d5e549172..9dc10ed83 100644 --- a/charts/rpe-send-letter-service/Chart.yaml +++ b/charts/rpe-send-letter-service/Chart.yaml @@ -1,7 +1,7 @@ name: rpe-send-letter-service apiVersion: v2 home: https://github.com/hmcts/send-letter-service -version: 0.4.25 +version: 0.4.26 description: HMCTS Send letter service maintainers: - name: HMCTS BSP Team diff --git a/src/main/java/uk/gov/hmcts/reform/sendletter/launchdarkly/Flags.java b/src/main/java/uk/gov/hmcts/reform/sendletter/launchdarkly/Flags.java index d3e2c4221..73105162c 100644 --- a/src/main/java/uk/gov/hmcts/reform/sendletter/launchdarkly/Flags.java +++ b/src/main/java/uk/gov/hmcts/reform/sendletter/launchdarkly/Flags.java @@ -6,4 +6,5 @@ private Flags() { } public static final String SEND_LETTER_SERVICE_TEST = "send-letter-service-test"; + public static final String SEND_LETTER_SERVICE_DELETE_LETTERS_CRON = "send-letter-service-delete-letters-cron"; } diff --git a/src/main/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTask.java b/src/main/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTask.java new file mode 100644 index 000000000..c2fd644e7 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTask.java @@ -0,0 +1,148 @@ +package uk.gov.hmcts.reform.sendletter.tasks; + +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import uk.gov.hmcts.reform.sendletter.launchdarkly.LaunchDarklyClient; + +import java.util.Optional; + +import static uk.gov.hmcts.reform.sendletter.launchdarkly.Flags.SEND_LETTER_SERVICE_DELETE_LETTERS_CRON; +import static uk.gov.hmcts.reform.sendletter.util.TimeZones.EUROPE_LONDON; + +/** + * Deletes old letters from the database based on a hardcoded SQL query. + * This task runs in all environments. + */ +@Component +public class DeleteOldLettersTask { + + private static final Logger logger = LoggerFactory.getLogger(DeleteOldLettersTask.class); + + private static final String TASK_NAME = "DeleteOldLetters"; + + private final JdbcTemplate jdbcTemplate; + + private final LaunchDarklyClient launchDarklyClient; + + @Value("${delete-old-letters.batch-size:1000}") + private int batchSize; + + @Value("${delete-old-letters.civil-general-applications-interval:6 years}") + private String civilGeneralApplicationsInterval; + + @Value("${delete-old-letters.civil-service-interval:6 years}") + private String civilServiceInterval; + + @Value("${delete-old-letters.cmc-claim-store-interval:2 years}") + private String cmcClaimStoreInterval; + + @Value("${delete-old-letters.divorce-frontend-interval:3 months}") + private String divorceFrontendInterval; + + @Value("${delete-old-letters.finrem-case-orchestration-interval:3 months}") + private String finremCaseOrchestrationInterval; + + @Value("${delete-old-letters.finrem-document-generator-interval:3 months}") + private String finremDocumentGeneratorInterval; + + @Value("${delete-old-letters.fpl-case-service-interval:2 years}") + private String fplCaseServiceInterval; + + @Value("${delete-old-letters.nfdiv-case-api-interval:3 months}") + private String nfdivCaseApiInterval; + + @Value("${delete-old-letters.prl-cos-api-interval:18 years}") + private String prlCosApiInterval; + + @Value("${delete-old-letters.probate-backend-interval:1 year}") + private String probateBackendInterval; + + @Value("${delete-old-letters.send-letter-tests-interval:2 years}") + private String sendLetterTestsInterval; + + @Value("${delete-old-letters.sscs-interval:3 months}") + private String sscsInterval; + + // See V028__Add_batch_delete_letters_function.sql for sql function + private final String deleteQuery = """ + SELECT batch_delete_letters( + ?, -- Batch size + CAST(? AS INTERVAL), -- civil_general_applications_interval + CAST(? AS INTERVAL), -- civil_service_interval + CAST(? AS INTERVAL), -- cmc_claim_store_interval + CAST(? AS INTERVAL), -- divorce_frontend_interval + CAST(? AS INTERVAL), -- finrem_case_orchestration_interval + CAST(? AS INTERVAL), -- finrem_document_generator_interval + CAST(? AS INTERVAL), -- fpl_case_service_interval + CAST(? AS INTERVAL), -- nfdiv_case_api_interval + CAST(? AS INTERVAL), -- prl_cos_api_interval + CAST(? AS INTERVAL), -- probate_backend_interval + CAST(? AS INTERVAL), -- send_letter_tests_interval + CAST(? AS INTERVAL) -- sscs_interval + ); + """; + + /** + * Constructor for the DeleteOldLettersTask. + * @param jdbcTemplate The JDBC template for running SQL queries. + */ + public DeleteOldLettersTask(JdbcTemplate jdbcTemplate, LaunchDarklyClient launchDarklyClient) { + this.jdbcTemplate = jdbcTemplate; + this.launchDarklyClient = launchDarklyClient; + } + + /** + * Deletes old letters from the database in batches based on the batch_delete_letters query. + * default cron: Every Saturday at 5 PM + */ + @SchedulerLock(name = TASK_NAME) + @Scheduled(cron = "${delete-old-letters.cron:0 0 17 ? * SAT}", zone = EUROPE_LONDON) + public void run() { + logger.info("Starting {} task", TASK_NAME); + if (launchDarklyClient.isFeatureEnabled(SEND_LETTER_SERVICE_DELETE_LETTERS_CRON)) { + logger.info("Flag enabled. Task {} running", TASK_NAME); + + int totalRowsDeleted = 0; + int rowsDeleted; + + try { + do { + rowsDeleted = Optional.ofNullable( + jdbcTemplate.queryForObject( + deleteQuery, + new Object[]{ + batchSize, + civilGeneralApplicationsInterval, // civil_general_applications_interval + civilServiceInterval, // civil_service_interval + cmcClaimStoreInterval, // cmc_claim_store_interval + divorceFrontendInterval, // divorce_frontend_interval + finremCaseOrchestrationInterval, // finrem_case_orchestration_interval + finremDocumentGeneratorInterval, // finrem_document_generator_interval + fplCaseServiceInterval, // fpl_case_service_interval + nfdivCaseApiInterval, // nfdiv_case_api_interval + prlCosApiInterval, // prl_cos_api_interval + probateBackendInterval, // probate_backend_interval + sendLetterTestsInterval, // send_letter_tests_interval + sscsInterval // sscs_interval + }, + Integer.class + ) + ).orElse(0); + totalRowsDeleted += rowsDeleted; + logger.info("Batch deleted: {} rows, Total deleted: {} rows", rowsDeleted, totalRowsDeleted); + } while (rowsDeleted > 0); + logger.info("{} task completed. Total rows deleted: {}", TASK_NAME, totalRowsDeleted); + } catch (Exception e) { + logger.error("Error occurred during {} task", TASK_NAME, e); + } + } else { + logger.info("Flag disabled. Task {} did not run", TASK_NAME); + } + + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9866e3003..b4c5e42c3 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -205,3 +205,22 @@ documents: launchdarkly: sdk-key: ${LAUNCH_DARKLY_SDK_KEY:XXXXX} offline-mode: ${LAUNCH_DARKLY_OFFLINE_MODE:false} + +#(0 */2 * * * ?) replace cron with this to test, will run every 2 minutes +delete-old-letters: + cron: ${DELETE_OLD_LETTERS_CRON:0 0 17 ? * SAT} + batch-size: ${BATCH_SIZE:1000} + civil-general-applications-interval: ${CIVIL_GENERAL_APPLICATIONS_INTERVAL:6 years} + civil-service-interval: ${CIVIL_SERVICE_INTERVAL:6 years} + cmc-claim-store-interval: ${CMC_CLAIM_STORE_INTERVAL:2 years} + divorce-frontend-interval: ${DIVORCE_FRONTEND_INTERVAL:3 months} + finrem-case-orchestration-interval: ${FINREM_CASE_ORCHESTRATION_INTERVAL:3 months} + finrem-document-generator-interval: ${FINREM_DOCUMENT_GENERATOR_INTERVAL:3 months} + fpl-case-service-interval: ${FPL_CASE_SERVICE_INTERVAL:2 years} + nfdiv-case-api-interval: ${NFDIV_CASE_API_INTERVAL:3 months} + prl-cos-api-interval: ${PRL_COS_API_INTERVAL:18 years} + probate-backend-interval: ${PROBATE_BACKEND_INTERVAL:1 year} + send-letter-tests-interval: ${SEND_LETTER_TESTS_INTERVAL:2 years} + sscs-interval: ${SSCS_INTERVAL:3 months} + + diff --git a/src/test/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTaskTest.java b/src/test/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTaskTest.java new file mode 100644 index 000000000..efd2f3c2c --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/sendletter/tasks/DeleteOldLettersTaskTest.java @@ -0,0 +1,78 @@ +package uk.gov.hmcts.reform.sendletter.tasks; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.jdbc.core.JdbcTemplate; +import uk.gov.hmcts.reform.sendletter.launchdarkly.LaunchDarklyClient; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.hmcts.reform.sendletter.launchdarkly.Flags.SEND_LETTER_SERVICE_DELETE_LETTERS_CRON; + +class DeleteOldLettersTaskTest { + + @Mock + private JdbcTemplate jdbcTemplate; + + @Mock + private LaunchDarklyClient launchDarklyClient; + + private DeleteOldLettersTask deleteOldLettersTask; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + deleteOldLettersTask = new DeleteOldLettersTask(jdbcTemplate, launchDarklyClient); + } + + @Test + void testRunWhenFeatureFlagIsEnabled() { + // Arrange: Mock LaunchDarkly feature flag to return true + when(launchDarklyClient.isFeatureEnabled(SEND_LETTER_SERVICE_DELETE_LETTERS_CRON)).thenReturn(true); + + // Mock JdbcTemplate behavior for the deletion loop + when(jdbcTemplate.queryForObject(anyString(), any(Object[].class), eq(Integer.class))) + .thenReturn(100) + .thenReturn(50) + .thenReturn(0); // Simulate 3 batches with 100, 50 rows deleted, and then no more rows + + // Act: Call the run method + deleteOldLettersTask.run(); + + // Assert: Check if the correct number of deletions were made + verify(jdbcTemplate, times(3)).queryForObject(anyString(), any(Object[].class), eq(Integer.class)); + } + + @Test + void testRunWhenFeatureFlagIsDisabled() { + // Arrange: Mock LaunchDarkly feature flag to return false + when(launchDarklyClient.isFeatureEnabled(SEND_LETTER_SERVICE_DELETE_LETTERS_CRON)).thenReturn(false); + + // Act: Call the run method + deleteOldLettersTask.run(); + + // Assert: Verify no deletion attempt is made + verify(jdbcTemplate, never()).queryForObject(anyString(), any(Object[].class), eq(Integer.class)); + } + + @Test + void testRunWhenErrorOccurs() { + // Arrange: Mock LaunchDarkly feature flag to return true + when(launchDarklyClient.isFeatureEnabled(SEND_LETTER_SERVICE_DELETE_LETTERS_CRON)).thenReturn(true); + + // Mock JdbcTemplate to throw an exception + when(jdbcTemplate.queryForObject(anyString(), any(Object[].class), eq(Integer.class))) + .thenThrow(new RuntimeException("Database error")); + + // Act: Call the run method + deleteOldLettersTask.run(); + + } +}