RED-9496 - Implement graceful shutdown #572

Merged
andrei.isvoran.ext merged 1 commits from RED-9496-fix-shutdown-bp into release/2.465.x 2024-07-05 10:57:43 +02:00
6 changed files with 183 additions and 10 deletions

View File

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

View File

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

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

View File

@ -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")

View File

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

View File

@ -22,7 +22,7 @@ server:
port: 8080
lifecycle:
base-package: com.iqser.red.service.peristence
base-package: com.iqser.red.service.persistence
spring:
main: