Graceful Shutdown in Spring Boot: Ensuring Smooth Application Termination

Graceful shutdown is a critical feature for production-ready Spring Boot applications that ensures your application can complete ongoing requests, release resources properly, and maintain data integrity when being stopped.

Understanding Graceful Shutdown

Graceful Shutdown allows an application to:

  • Complete processing of ongoing requests
  • Reject new requests
  • Release database connections, thread pools, and other resources
  • Finish current transactions
  • Save application state if needed

Configuration Methods

Method 1: Application Properties Configuration

Configure graceful shutdown through application.properties or application.yml:

application.properties

# Enable graceful shutdown
server.shutdown=graceful
# Time to wait for ongoing requests to complete (default: 30s)
spring.lifecycle.timeout-per-shutdown-phase=30s
# Additional Tomcat specific configuration (if using embedded Tomcat)
server.tomcat.connection-timeout=30s
server.tomcat.keep-alive-timeout=30s

application.yml

server:
shutdown: graceful
tomcat:
connection-timeout: 30s
keep-alive-timeout: 30s
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
# For Undertow server
server:
undertow:
threads:
worker: 64
io: 4

Method 2: Programmatic Configuration

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GracefulShutdownConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> gracefulShutdownCustomizer() {
return factory -> factory.addConnectorCustomizers(connector -> {
connector.setProperty("relaxedQueryChars", "[]");
connector.setProperty("relaxedPathChars", "[]");
});
}
}

Implementing Custom Graceful Shutdown

1. Custom Graceful Shutdown Component

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import java.util.concurrent.atomic.AtomicBoolean;
@Component
public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> {
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
@Autowired(required = false)
private HealthEndpoint healthEndpoint;
public boolean isShuttingDown() {
return shuttingDown.get();
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
shuttingDown.set(true);
System.out.println("Application shutdown initiated. Starting graceful shutdown...");
// Add custom shutdown logic here
performCleanup();
System.out.println("Graceful shutdown completed.");
}
private void performCleanup() {
try {
// Wait for ongoing processing to complete
Thread.sleep(5000);
// Close custom resources
closeCustomResources();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Shutdown was interrupted");
}
}
private void closeCustomResources() {
// Implement resource cleanup logic
System.out.println("Closing custom resources...");
}
}

2. Spring Boot Actuator for Graceful Shutdown

Add Actuator dependency and configure shutdown endpoint:

pom.xml

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.properties

# Enable shutdown endpoint
management.endpoints.web.exposure.include=health,info,shutdown
management.endpoint.shutdown.enabled=true
# Custom management port (optional)
management.server.port=8081

Custom Shutdown Endpoint

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;
@Component
@Endpoint(id = "graceful-shutdown")
public class CustomGracefulShutdownEndpoint {
private final GracefulShutdownHandler gracefulShutdownHandler;
public CustomGracefulShutdownEndpoint(GracefulShutdownHandler gracefulShutdownHandler) {
this.gracefulShutdownHandler = gracefulShutdownHandler;
}
@WriteOperation
public Map<String, String> gracefulShutdown() {
new Thread(() -> {
try {
Thread.sleep(1000); // Give time for response
gracefulShutdownHandler.initiateShutdown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
return Collections.singletonMap("message", "Graceful shutdown initiated");
}
}

Database Connection Cleanup

1. DataSource Configuration for Graceful Shutdown

import org.springframework.boot.jdbc.DataSourceUnwrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import javax.sql.DataSource;
import java.sql.SQLException;
@Configuration
public class DataSourceGracefulShutdownConfig {
@Bean
public ApplicationListener<ContextClosedEvent> dataSourceShutdownListener(DataSource dataSource) {
return event -> {
System.out.println("Closing database connections...");
try {
if (DataSourceUnwrapper.isHikari(dataSource)) {
DataSourceUnwrapper.getHikariDataSource(dataSource).close();
}
// Add other data source types as needed
} catch (SQLException e) {
System.err.println("Error closing data source: " + e.getMessage());
}
};
}
}

2. HikariCP Specific Configuration

# HikariCP graceful shutdown configuration
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20

Thread Pool Graceful Shutdown

1. Configuring Async Thread Pool Shutdown

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncThread-");
executor.setWaitForTasksToCompleteOnShutdown(true); // Important for graceful shutdown
executor.setAwaitTerminationSeconds(30); // Wait up to 30 seconds
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return taskExecutor();
}
}

2. Custom Thread Pool Manager

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Component
public class CustomThreadPoolManager implements DisposableBean {
private final ExecutorService executorService;
public CustomThreadPoolManager() {
this.executorService = Executors.newFixedThreadPool(10);
}
public void submitTask(Runnable task) {
if (!executorService.isShutdown()) {
executorService.submit(task);
}
}
@Override
public void destroy() throws Exception {
System.out.println("Shutting down custom thread pool...");
executorService.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
// Cancel currently executing tasks
executorService.shutdownNow();
// Wait again for tasks to respond to being cancelled
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("Thread pool did not terminate");
}
}
} catch (InterruptedException ie) {
// Cancel if current thread also interrupted
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("Custom thread pool shutdown completed");
}
}

Handling HTTP Requests During Shutdown

1. Request Interceptor for Graceful Shutdown

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class GracefulShutdownInterceptor implements HandlerInterceptor {
private final GracefulShutdown gracefulShutdown;
public GracefulShutdownInterceptor(GracefulShutdown gracefulShutdown) {
this.gracefulShutdown = gracefulShutdown;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (gracefulShutdown.isShuttingDown()) {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
response.getWriter().write("Service is shutting down");
return false;
}
return true;
}
}

2. Web Configuration

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private GracefulShutdownInterceptor gracefulShutdownInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(gracefulShutdownInterceptor);
}
}

Complete Graceful Shutdown Handler

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.CompletableFuture;
@Component
public class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> {
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
@Autowired
private CustomThreadPoolManager threadPoolManager;
@Autowired(required = false)
private DataSource dataSource;
public boolean isShutdownInProgress() {
return shutdownInProgress.get();
}
public void initiateShutdown() {
shutdownInProgress.set(true);
// Custom shutdown trigger logic
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
shutdownInProgress.set(true);
System.out.println("=== Starting Graceful Shutdown ===");
// Phase 1: Stop accepting new requests
stopAcceptingNewRequests();
// Phase 2: Wait for ongoing requests to complete
waitForOngoingRequests();
// Phase 3: Close resources
closeResources();
// Phase 4: Force shutdown if necessary
forceShutdownIfNeeded();
System.out.println("=== Graceful Shutdown Completed ===");
}
private void stopAcceptingNewRequests() {
System.out.println("Stopping acceptance of new requests...");
// Implementation depends on your load balancer/API gateway
}
private void waitForOngoingRequests() {
System.out.println("Waiting for ongoing requests to complete...");
try {
// Monitor active requests and wait
CompletableFuture<Void> completion = monitorActiveRequests();
completion.get(30, java.util.concurrent.TimeUnit.SECONDS);
} catch (Exception e) {
System.err.println("Error waiting for requests to complete: " + e.getMessage());
}
}
private CompletableFuture<Void> monitorActiveRequests() {
return CompletableFuture.runAsync(() -> {
// Implement request monitoring logic
// This is application-specific
try {
Thread.sleep(10000); // Example: wait 10 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
private void closeResources() {
System.out.println("Closing resources...");
// Close database connections, file handles, network connections, etc.
}
private void forceShutdownIfNeeded() {
System.out.println("Initiating force shutdown if needed...");
// Force kill after timeout
}
}

Testing Graceful Shutdown

1. Integration Test

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GracefulShutdownTest {
@LocalServerPort
private int port;
@Test
public void testGracefulShutdown() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:" + port + "/api/health";
// Test that application responds normally
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
// In a real test, you would trigger shutdown and verify behavior
// This is simplified for demonstration
}
}

2. Manual Testing Script

#!/bin/bash
# graceful-shutdown-test.sh
echo "Starting Spring Boot application..."
java -jar your-application.jar &
APP_PID=$!
echo "Application started with PID: $APP_PID"
# Wait for application to start
sleep 30
echo "Sending shutdown signal..."
curl -X POST http://localhost:8080/actuator/shutdown
# Wait for graceful shutdown
wait $APP_PID
echo "Application shutdown complete"

Best Practices

1. Configuration Recommendations

# Optimal graceful shutdown configuration
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
spring.datasource.hikari.max-lifetime=1200000
management.endpoints.web.exposure.include=health,info,metrics,shutdown

2. Monitoring and Logging

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class ShutdownMonitor {
private static final Logger logger = LoggerFactory.getLogger(ShutdownMonitor.class);
public void logShutdownProgress(String phase) {
logger.info("Shutdown phase: {}", phase);
// Add metrics collection here
}
}

3. Health Check Integration

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class ShutdownHealthIndicator implements HealthIndicator {
private final GracefulShutdownHandler shutdownHandler;
public ShutdownHealthIndicator(GracefulShutdownHandler shutdownHandler) {
this.shutdownHandler = shutdownHandler;
}
@Override
public Health health() {
if (shutdownHandler.isShutdownInProgress()) {
return Health.down()
.withDetail("message", "Application is shutting down")
.build();
}
return Health.up().build();
}
}

Conclusion

Implementing graceful shutdown in Spring Boot ensures that your application can handle termination signals properly, complete ongoing work, and maintain data integrity. The key aspects include:

  1. Configuration: Enable graceful shutdown in properties
  2. Resource Management: Properly close connections and thread pools
  3. Request Handling: Complete ongoing requests and reject new ones
  4. Monitoring: Track shutdown progress and health status
  5. Testing: Verify shutdown behavior in different scenarios

By following these practices, you can ensure your Spring Boot applications shutdown gracefully without data loss or corruption.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper