Merge branch 'RED-9496' into 'master'

RED-9496 - Implement graceful shutdown

Closes RED-9496

See merge request redactmanager/redaction-report-service!81
This commit is contained in:
Andrei Isvoran 2024-07-03 09:32:16 +02:00
commit 6b322e4ea6
7 changed files with 412 additions and 0 deletions

View File

@ -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 {
/**

View File

@ -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.");
}
}
}
}
}

View File

@ -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<ContextClosedEvent> {
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);
}
}
}

View File

@ -7,6 +7,8 @@ fforesight.tenants.remote: true
server:
port: 8080
shutdown: graceful
logging.pattern.level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]"

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}