Provide a built-in Actuator drain endpoint for Kubernetes preStop hook polling
Summary
Spring Boot 2.3 introduced server.shutdown: graceful, liveness/readiness probe
endpoints, and SmartLifecycle-based shutdown ordering. These cover HTTP-level draining
well. However, there is no standard Actuator endpoint that allows a Kubernetes preStop
hook to poll and determine when all application-level in-flight work (async tasks,
Kafka consumer processing, custom thread pools, etc.) has completed before SIGTERM is sent
to the JVM.
Current Behaviour
Spring Boot provides:
/health/liveness— signals whether the application context is alive/health/readiness— signals whether the pod should receive traffic; Spring automatically setsReadinessState.REFUSING_TRAFFICon shutdown, which causes this to return 503server.shutdown: graceful— Tomcat waits for in-flight HTTP requests to completeSmartLifecycle— allows beans to participate in ordered shutdown
None of these can be used as a simple HTTP 200/503 polling endpoint for a Kubernetes
preStop hook to determine when async, non-HTTP work is finished.
The Gap
The Kubernetes pod termination sequence is:
- Pod removed from Service endpoints
- preStop hook executes: Spring is fully alive here; no shutdown has started
- SIGTERM sent to JVM process
- Spring shutdown begins (SmartLifecycle, Tomcat drain, etc.)
- SIGKILL after terminationGracePeriodSeconds
The preStop hook runs before SIGTERM reaches the JVM. Spring's own graceful shutdown
therefore cannot participate in this phase. Teams are left implementing their own drain
endpoint, typically:
- A custom
@RestControllerreturning200 OKwhen idle,503while busy - A custom
SmartLifecyclebean that polls registered "drainable" components - Wiring of
ThreadPoolTaskExecutor.getActiveCount()and similar metrics by hand
This is a common enough pattern that every team deploying Spring Boot on Kubernetes implements it independently.
Proposed Solution
Introduce a Drainable SPI (or reuse SmartLifecycle) and a built-in Actuator
endpoint — e.g. /actuator/drain or a new drain health group — that:
- Aggregates the state of all registered
Drainablebeans (orSmartLifecyclebeans that have not yet completed theirstop()) - Returns
200 OKwhen all components report no in-flight work - Returns
503 Service Unavailablewith a JSON body listing the still-busy component names while work is in progress
The endpoint should be exposed under the management port and require no authentication by
default (consistent with /health/liveness and /health/readiness), so that a minimal
preStop script can use it:
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- until wget -qO- http://localhost:8080/actuator/drain; do sleep 2; done/**
* Register a bean of this type to participate in the built-in drain endpoint.
* Spring Boot will expose aggregate drain state at /actuator/drain.
*/
public interface Drainable {
/** Returns true when this component has no more in-flight work. */
boolean isDrained();
/** Human-readable name used in the /actuator/drain response body. */
String name();
}Source: spring-projects/spring-boot