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:
commit
6b322e4ea6
@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -7,6 +7,8 @@ fforesight.tenants.remote: true
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
shutdown: graceful
|
||||
|
||||
|
||||
logging.pattern.level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]"
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user