From 1d5413039803dfd70b421b8e1a86f2941266aaf6 Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Wed, 3 Jul 2024 09:32:16 +0200 Subject: [PATCH] RED-9496 - Implement graceful shutdown --- .../report/v1/server/Application.java | 2 + .../v1/server/aspect/LifecycleAspect.java | 53 +++++ .../v1/server/service/LifecycleManager.java | 106 +++++++++ .../src/main/resources/application.yml | 2 + .../report/v1/server/LifecycleAspectTest.java | 208 ++++++++++++++++++ .../v1/server/config/AspectTestConfig.java | 24 ++ .../v1/server/utils/TestController.java | 17 ++ 7 files changed, 412 insertions(+) create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/aspect/LifecycleAspect.java create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/LifecycleManager.java create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/LifecycleAspectTest.java create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/config/AspectTestConfig.java create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/utils/TestController.java diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/Application.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/Application.java index 981a8cd..b556e64 100644 --- a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/Application.java +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/Application.java @@ -12,6 +12,7 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfi import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import org.springframework.scheduling.annotation.EnableAsync; @@ -34,6 +35,7 @@ import io.micrometer.core.instrument.MeterRegistry; @EnableFeignClients(basePackageClasses = {DossierClient.class}) @EnableMongoRepositories(basePackages = "com.iqser.red.service.persistence") @EnableConfigurationProperties(ReportTemplateSettings.class) +@EnableAspectJAutoProxy public class Application { /** diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/aspect/LifecycleAspect.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/aspect/LifecycleAspect.java new file mode 100644 index 0000000..4bb4049 --- /dev/null +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/aspect/LifecycleAspect.java @@ -0,0 +1,53 @@ +package com.iqser.red.service.redaction.report.v1.server.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.stereotype.Component; + +import com.iqser.red.service.redaction.report.v1.server.service.LifecycleManager; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class LifecycleAspect { + + private final LifecycleManager lifecycleManager; + + + @Around("execution(* com.iqser.red.service.redaction.report.v1.server..*(..)) && (" + + "@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) || " + + "@annotation(org.springframework.web.bind.annotation.GetMapping) || " + + "@annotation(org.springframework.web.bind.annotation.PostMapping) || " + + "@annotation(org.springframework.web.bind.annotation.PutMapping) || " + + "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " + + "@annotation(org.springframework.web.bind.annotation.RequestMapping))") + public Object checkLifecycle(ProceedingJoinPoint joinPoint) throws Throwable { + + synchronized (lifecycleManager) { + if (!lifecycleManager.isRunning()) { + log.info("Application is shutting down, rejecting new messages."); + throw new AmqpRejectAndDontRequeueException("Application is shutting down, rejecting new messages."); + } + lifecycleManager.incrementAndGet(); + } + + try { + return joinPoint.proceed(); + } finally { + int remainingTasks = lifecycleManager.decrementAndGet(); + synchronized (lifecycleManager) { + if (remainingTasks == 0 && !lifecycleManager.isRunning()) { + lifecycleManager.countDown(); + log.info("All tasks are done, ready for shutdown."); + } + } + } + } + +} \ No newline at end of file diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/LifecycleManager.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/LifecycleManager.java new file mode 100644 index 0000000..e06e18e --- /dev/null +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/LifecycleManager.java @@ -0,0 +1,106 @@ +package com.iqser.red.service.redaction.report.v1.server.service; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LifecycleManager implements SmartLifecycle, ApplicationListener { + + private volatile boolean running; // by default initialized as false + private volatile boolean shutdownInitiated; // by default initialized as false + private final Object shutdownMonitor = new Object(); + private final AtomicInteger activeTasks = new AtomicInteger(0); + private final CountDownLatch latch = new CountDownLatch(1); + + + public void countDown() { + + latch.countDown(); + } + + + public int incrementAndGet() { + + return activeTasks.incrementAndGet(); + } + + + public int decrementAndGet() { + + return activeTasks.decrementAndGet(); + } + + + @Override + public void start() { + + synchronized (shutdownMonitor) { + running = true; + } + } + + + @Override + public void stop() { + + synchronized (shutdownMonitor) { + running = false; + if (activeTasks.get() == 0) { + latch.countDown(); // No active tasks, release the latch immediately + } + } + } + + + @Override + public boolean isRunning() { + + return running; + } + + + @Override + public boolean isAutoStartup() { + + return true; + } + + + @Override + public int getPhase() { + + return Integer.MAX_VALUE; // Start this component last and stop it first + } + + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + + synchronized (shutdownMonitor) { + if (shutdownInitiated) { + return; // Avoid multiple shutdown initiations + } + shutdownInitiated = true; + } + + stop(); + log.info("Context is closing, waiting for ongoing tasks to complete."); + try { + latch.await(); // Wait for the latch to count down to zero + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Shutdown was interrupted", e); + } + } + +} diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/resources/application.yml b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/resources/application.yml index 2654d2b..f0ba4d8 100644 --- a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/resources/application.yml +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/resources/application.yml @@ -7,6 +7,8 @@ fforesight.tenants.remote: true server: port: 8080 + shutdown: graceful + logging.pattern.level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]" diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/LifecycleAspectTest.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/LifecycleAspectTest.java new file mode 100644 index 0000000..8f6a6bc --- /dev/null +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/LifecycleAspectTest.java @@ -0,0 +1,208 @@ +package com.iqser.red.service.redaction.report.v1.server; + +import static com.iqser.red.service.redaction.report.v1.server.configuration.MessagingConfiguration.REPORT_RESULT_QUEUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier; +import com.iqser.red.service.redaction.report.v1.api.model.ReportRequestMessage; +import com.iqser.red.service.redaction.report.v1.api.model.ReportResultMessage; +import com.iqser.red.service.redaction.report.v1.server.client.ComponentClient; +import com.iqser.red.service.redaction.report.v1.server.client.DictionaryClient; +import com.iqser.red.service.redaction.report.v1.server.client.DossierAttributesClient; +import com.iqser.red.service.redaction.report.v1.server.client.DossierAttributesConfigClient; +import com.iqser.red.service.redaction.report.v1.server.client.DossierClient; +import com.iqser.red.service.redaction.report.v1.server.client.FileAttributesConfigClient; +import com.iqser.red.service.redaction.report.v1.server.client.ReportTemplateClient; +import com.iqser.red.service.redaction.report.v1.server.config.AspectTestConfig; +import com.iqser.red.service.redaction.report.v1.server.configuration.MessagingConfiguration; +import com.iqser.red.service.redaction.report.v1.server.service.EntityLogConverterService; +import com.iqser.red.service.redaction.report.v1.server.service.ExcelReportGenerationService; +import com.iqser.red.service.redaction.report.v1.server.service.GeneratePlaceholderService; +import com.iqser.red.service.redaction.report.v1.server.service.LifecycleManager; +import com.iqser.red.service.redaction.report.v1.server.service.ReportMessageReceiver; +import com.iqser.red.service.redaction.report.v1.server.service.WordReportGenerationService; +import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService; +import com.iqser.red.service.redaction.report.v1.server.utils.TestController; +import com.iqser.red.storage.commons.service.StorageService; +import com.knecon.fforesight.tenantcommons.TenantsClient; + +import lombok.SneakyThrows; + +@ExtendWith({SpringExtension.class, MockitoExtension.class}) +@SpringBootTest +@Import(AspectTestConfig.class) +public class LifecycleAspectTest { + + @Autowired + private ReportMessageReceiver reportMessageReceiver; + + @Autowired + private TestController testController; + + @Autowired + private LifecycleManager lifecycleManager; + + @Mock + private Message mockMessage; + + @MockBean + TenantsClient tenantsClient; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private StorageService storageService; + + @MockBean + private RabbitTemplate rabbitTemplate; + + @MockBean + private ReportStorageService reportStorageService; + + @MockBean + private ReportTemplateClient reportTemplateClient; + + @MockBean + private FileAttributesConfigClient fileAttributesConfigClient; + + @Autowired + private WordReportGenerationService wordReportGenerationService; + + @MockBean + private MessagingConfiguration messagingConfiguration; + + @Autowired + private EntityLogConverterService entityLogConverterService; + + @MockBean + private DossierAttributesConfigClient dossierAttributesConfigClient; + + @MockBean + private DossierAttributesClient dossierAttributesClient; + + @Autowired + private ExcelReportGenerationService excelTemplateReportGenerationService; + + @Autowired + private GeneratePlaceholderService generatePlaceholderService; + + @MockBean + private DictionaryClient dictionaryClient; + + @MockBean + private DossierClient dossierClient; + + @MockBean + private ComponentClient componentClient; + + @Mock + private MessageProperties mockMessageProperties; + + @BeforeEach + public void setup() { + + lifecycleManager.start(); + } + + + @Test + public void testRabbitListenerAspectWhenRunning() throws Exception { + + ReportRequestMessage reportRequestMessage = ReportRequestMessage.builder() + .userId("test-user") + .downloadId("123") + .dossierId("dossierId") + .dossierTemplateId("dossierTemplateId") + .build(); + + String json = objectMapper.writeValueAsString(reportRequestMessage); + + prepareDossier(); + when(mockMessage.getBody()).thenReturn(json.getBytes()); + when(mockMessage.getMessageProperties()).thenReturn(mockMessageProperties); + + reportMessageReceiver.receive(mockMessage); + lifecycleManager.stop(); + + verify(rabbitTemplate, times(1)).convertAndSend(eq(REPORT_RESULT_QUEUE), any(ReportResultMessage.class)); + } + + @Test + public void testRabbitListenerAspectWhenNotRunning() { + + lifecycleManager.stop(); + + assertThrows(AmqpRejectAndDontRequeueException.class, () -> { + reportMessageReceiver.receive(mockMessage); + }); + } + + + @Test + @SneakyThrows + public void testHttpGetAspectWhenRunning() { + + String response = testController.testGet(); + + assertEquals("Test Get", response); + } + + + @Test + @SneakyThrows + public void testStopLifecycleWhenRunning() { + + String response = testController.testGet(); + + lifecycleManager.stop(); + assertEquals("Test Get", response); + } + + + @Test + public void testHttpGetAspectWhenNotRunning() { + + lifecycleManager.stop(); + + Exception exception = assertThrows(AmqpRejectAndDontRequeueException.class, () -> { + testController.testGet(); + }); + assertEquals("Application is shutting down, rejecting new messages.", exception.getMessage()); + } + + @SneakyThrows + private Dossier prepareDossier() { + + var testDossier = new Dossier(); + testDossier.setName("Test Dossier"); + testDossier.setDossierTemplateId("dossierTemplateId"); + testDossier.setId("dossierId"); + + when(dossierClient.getDossierById(eq("dossierId"), anyBoolean(), anyBoolean())).thenReturn(testDossier); + return testDossier; + } +} diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/config/AspectTestConfig.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/config/AspectTestConfig.java new file mode 100644 index 0000000..b4a2893 --- /dev/null +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/config/AspectTestConfig.java @@ -0,0 +1,24 @@ +package com.iqser.red.service.redaction.report.v1.server.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +import com.iqser.red.service.redaction.report.v1.server.aspect.LifecycleAspect; +import com.iqser.red.service.redaction.report.v1.server.service.LifecycleManager; + +@TestConfiguration +@EnableAspectJAutoProxy +public class AspectTestConfig { + + @Bean + public LifecycleManager lifecycleManager() { + return new LifecycleManager(); + } + + @Bean + public LifecycleAspect lifecycleAspect(LifecycleManager lifecycleManager) { + return new LifecycleAspect(lifecycleManager); + } +} + diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/utils/TestController.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/utils/TestController.java new file mode 100644 index 0000000..80df84b --- /dev/null +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/utils/TestController.java @@ -0,0 +1,17 @@ +package com.iqser.red.service.redaction.report.v1.server.utils; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test-get") + public String testGet() throws InterruptedException { + + Thread.sleep(5000); + return "Test Get"; + } + +} +