From c747961b948682d8aecc658c3cd52aea52cb40bc Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 4 Jul 2024 17:06:55 +0300 Subject: [PATCH] RED-9496 - Implement graceful shutdown --- .../processor/lifecycle/LifecycleAspect.java | 57 ++++++++++ .../processor/lifecycle/LifecycleManager.java | 106 ++++++++++++++++++ .../lifecycle/LifecycleProperties.java | 16 +++ .../build.gradle.kts | 1 - .../peristence/v1/server/Application.java | 11 +- .../src/main/resources/application.yaml | 2 +- 6 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleAspect.java create mode 100644 persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleManager.java create mode 100644 persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleProperties.java diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleAspect.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleAspect.java new file mode 100644 index 000000000..2619086d7 --- /dev/null +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleAspect.java @@ -0,0 +1,57 @@ +package com.iqser.red.service.persistence.management.v1.processor.lifecycle; + +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class LifecycleAspect { + + private final LifecycleManager lifecycleManager; + + private final LifecycleProperties lifecycleProperties; + + + @Around("@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 { + + String targetClassName = joinPoint.getTarget().getClass().getPackageName(); + if (!targetClassName.startsWith(lifecycleProperties.getBasePackage())) { + return joinPoint.proceed(); + } + + 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/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleManager.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleManager.java new file mode 100644 index 000000000..11e0f3701 --- /dev/null +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleManager.java @@ -0,0 +1,106 @@ +package com.iqser.red.service.persistence.management.v1.processor.lifecycle; + +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); + } + } + +} \ No newline at end of file diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleProperties.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleProperties.java new file mode 100644 index 000000000..f11104bbd --- /dev/null +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/lifecycle/LifecycleProperties.java @@ -0,0 +1,16 @@ +package com.iqser.red.service.persistence.management.v1.processor.lifecycle; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ConfigurationProperties("lifecycle") +public class LifecycleProperties { + + private String basePackage; +} \ No newline at end of file diff --git a/persistence-service-v1/persistence-service-server-v1/build.gradle.kts b/persistence-service-v1/persistence-service-server-v1/build.gradle.kts index d9f78a69f..574accd15 100644 --- a/persistence-service-v1/persistence-service-server-v1/build.gradle.kts +++ b/persistence-service-v1/persistence-service-server-v1/build.gradle.kts @@ -24,7 +24,6 @@ dependencies { api(project(":persistence-service-internal-api-impl-v1")) api(project(":persistence-service-shared-mongo-v1")) api("com.iqser.red.commons:storage-commons:2.49.0") - api("com.knecon.fforesight:lifecycle-commons:0.6.0") api("junit:junit:4.13.2") api("org.apache.logging.log4j:log4j-slf4j-impl:2.20.0") api("net.logstash.logback:logstash-logback-encoder:7.4") diff --git a/persistence-service-v1/persistence-service-server-v1/src/main/java/com/iqser/red/service/peristence/v1/server/Application.java b/persistence-service-v1/persistence-service-server-v1/src/main/java/com/iqser/red/service/peristence/v1/server/Application.java index 387281ff8..eb861dfe6 100644 --- a/persistence-service-v1/persistence-service-server-v1/src/main/java/com/iqser/red/service/peristence/v1/server/Application.java +++ b/persistence-service-v1/persistence-service-server-v1/src/main/java/com/iqser/red/service/peristence/v1/server/Application.java @@ -1,11 +1,8 @@ package com.iqser.red.service.peristence.v1.server; -import java.util.Collections; import java.util.Map; import java.util.Optional; -import org.apache.catalina.Context; -import org.apache.tomcat.websocket.server.WsSci; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; @@ -21,8 +18,6 @@ import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfigurati import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -46,13 +41,13 @@ import com.iqser.red.service.dictionarymerge.commons.DictionaryMergeService; import com.iqser.red.service.persistence.management.v1.processor.PersistenceServiceProcessorConfiguration; import com.iqser.red.service.persistence.management.v1.processor.cache.PersistenceServiceExternalApiCacheConfiguration; import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration; +import com.iqser.red.service.persistence.management.v1.processor.lifecycle.LifecycleProperties; import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings; import com.iqser.red.service.persistence.v1.internal.api.PersistenceServiceInternalApiConfiguration; import com.iqser.red.storage.commons.StorageAutoConfiguration; import com.knecon.fforesight.databasetenantcommons.DatabaseTenantCommonsAutoConfiguration; import com.knecon.fforesight.jobscommons.JobsAutoConfiguration; import com.knecon.fforesight.keycloakcommons.DefaultKeyCloakCommonsAutoConfiguration; -import com.knecon.fforesight.lifecyclecommons.LifecycleAutoconfiguration; import com.knecon.fforesight.mongo.database.commons.MongoDatabaseCommonsAutoConfiguration; import com.knecon.fforesight.swaggercommons.SpringDocAutoConfiguration; import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration; @@ -72,9 +67,9 @@ import lombok.extern.slf4j.Slf4j; @EnableRetry @EnableScheduling @EnableCaching -@EnableConfigurationProperties({FileManagementServiceSettings.class}) +@EnableConfigurationProperties({FileManagementServiceSettings.class, LifecycleProperties.class}) @EnableMongoRepositories(basePackages = "com.iqser.red.service.persistence") -@ImportAutoConfiguration({StorageAutoConfiguration.class, JobsAutoConfiguration.class, DatabaseTenantCommonsAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class, MongoDatabaseCommonsAutoConfiguration.class, LifecycleAutoconfiguration.class}) +@ImportAutoConfiguration({StorageAutoConfiguration.class, JobsAutoConfiguration.class, DatabaseTenantCommonsAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class, MongoDatabaseCommonsAutoConfiguration.class}) @SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, CassandraAutoConfiguration.class, DataSourceAutoConfiguration.class, LiquibaseAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) @Import({PersistenceServiceExternalApiConfigurationV2.class, PersistenceServiceExternalApiConfiguration.class, PersistenceServiceInternalApiConfiguration.class, PersistenceServiceExternalApiCacheConfiguration.class, MultiTenancyWebConfiguration.class, PersistenceServiceProcessorConfiguration.class, MessagingConfiguration.class, MultiTenancyMessagingConfiguration.class}) @EnableAspectJAutoProxy diff --git a/persistence-service-v1/persistence-service-server-v1/src/main/resources/application.yaml b/persistence-service-v1/persistence-service-server-v1/src/main/resources/application.yaml index 191ba4a73..81f955325 100644 --- a/persistence-service-v1/persistence-service-server-v1/src/main/resources/application.yaml +++ b/persistence-service-v1/persistence-service-server-v1/src/main/resources/application.yaml @@ -22,7 +22,7 @@ server: port: 8080 lifecycle: - base-package: com.iqser.red.service.peristence + base-package: com.iqser.red.service.persistence spring: main: -- 2.47.2