Skip to content

Provide a built-in Actuator drain endpoint for Kubernetes preStop hook polling #51310

Description

@Raghav2211

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 sets ReadinessState.REFUSING_TRAFFIC on shutdown, which causes this to
    return 503
  • server.shutdown: graceful — Tomcat waits for in-flight HTTP requests to complete
  • SmartLifecycle — 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 @RestController returning 200 OK when idle, 503 while busy
  • A custom SmartLifecycle bean 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:

  1. Aggregates the state of all registered Drainable beans (or SmartLifecycle beans
    that have not yet completed their stop())
  2. Returns 200 OK when all components report no in-flight work
  3. Returns 503 Service Unavailable with 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();
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions