skip to content

How does SmartLifecycle.stop(Runnable callback) enable graceful, phased shutdown, and what is the per-phase timeout?

level: principalimportance: should knowfreq 30%

answer

  1. stop(Runnable) = async, must call callback.run()
  2. CountDownLatch per phase
  3. default 30s timeoutPerShutdownPhase
  4. per-phase wait, descending order
  5. spring.lifecycle.timeout-per-shutdown-phase

basics

~20 s

stop(Runnable) lets a bean shut down asynchronously: do your cleanup, then call callback.run() to signal completion. DefaultLifecycleProcessor stops all beans in a phase, then waits up to a timeout (default 30s) for their callbacks before moving to the next phase.

solid answer

~40 s

SmartLifecycle adds stop(Runnable callback) on top of Lifecycle.stop(). During shutdown, DefaultLifecycleProcessor processes phases in descending order; for each phase it calls stop(callback) on every running bean in that phase and then blocks, waiting for all their callbacks to fire, up to timeoutPerShutdownPhase (default 30000 ms). Only after the phase's beans signal completion (or the timeout elapses) does it descend to the next phase. This gives you ordered draining: a bean can stop accepting new work synchronously, then finish in-flight work on a background thread, calling callback.run() when truly done — so lower-phase infrastructure isn't torn down until higher-phase consumers have drained. The default stop(Runnable) implementation just calls stop() then callback.run(). You configure the timeout via DefaultLifecycleProcessor.setTimeoutPerShutdownPhase, or in Spring Boot via spring.lifecycle.timeout-per-shutdown-phase.

code

java · 27 lines
java
import org.springframework.context.SmartLifecycle;
import java.util.concurrent.ExecutorService;

class DrainingConsumer implements SmartLifecycle {
    private volatile boolean running;
    private final ExecutorService pool = /* ... */ null;

    @Override public void start() { running = true; }

    // Async shutdown: stop intake now, drain in background, THEN signal done.
    @Override public void stop(Runnable callback) {
        running = false;                 // stop accepting new work immediately
        pool.submit(() -> {
            try {
                pool.shutdown();         // let in-flight tasks finish
                pool.awaitTermination(20, java.util.concurrent.TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                callback.run();          // MUST call, or the phase waits the full timeout
            }
        });
    }

    @Override public void stop() { running = false; } // fallback if callback variant unused
    @Override public boolean isRunning() { return running; }
}

go deeper

for a junior

Just know stop() releases resources; the async variant is advanced.

for a middle

Should know a per-phase timeout exists and defaults to 30s.

for a senior

Should explain the CountDownLatch-per-phase wait and the must-call-callback rule.

for a principal

Designs ordered draining across phases, tunes timeoutPerShutdownPhase against SLAs, and ties it to Boot graceful shutdown; reasons about worst-case total shutdown time.

## Why an async stop variant exists Plain `Lifecycle.stop()` is synchronous and returns when the method returns. But real graceful shutdown often needs to: (a) stop accepting new work immediately, then (b) let in-flight work finish, which may take time and happen on other threads. Blocking the shutdown thread the whole time is crude. **`SmartLifecycle.stop(Runnable callback)`** solves this: the container hands you a callback you invoke **when your shutdown is genuinely complete**, allowing the work itself to proceed asynchronously. ## The processor's shutdown algorithm `DefaultLifecycleProcessor.stopBeans()` (invoked from `onClose()` / `ConfigurableApplicationContext.stop()`): 1. Collect all `Lifecycle` beans, group by phase. 2. Process phases in **descending** order (highest phase first). 3. For each phase, build a `CountDownLatch` sized to the number of running beans, then call **`stop(Runnable)`** on each — passing a callback that decrements the latch. 4. **Wait** on the latch up to `timeoutPerShutdownPhase` (default **30 000 ms = 30 s**). 5. If the timeout elapses before all callbacks fire, log a warning naming the beans that failed to stop in time, and proceed to the next phase anyway. Because each phase fully drains (or times out) before the next begins, you get **ordered draining**: MAX_VALUE-phase consumers stop and drain first; only then do lower-phase pools/registries stop and release resources they depend on. ## The default implementation SmartLifecycle provides a default: ```java default void stop(Runnable callback) { stop(); callback.run(); } ``` So if you only override `stop()`, shutdown is synchronous per bean and the callback fires immediately — perfectly fine when cleanup is fast. Override `stop(Runnable)` **only** when you need async completion, and you **must** eventually call `callback.run()`, or that phase will always hit the timeout and stall shutdown by up to 30s. ## Configuring the timeout - Programmatically: register a `DefaultLifecycleProcessor` bean named `lifecycleProcessor` and call `setTimeoutPerShutdownPhase(millis)`. The timeout is **per phase**, not global — total shutdown can take up to `timeout × numberOfPhases`. - Spring Boot: **`spring.lifecycle.timeout-per-shutdown-phase`** (e.g. `30s`). ## Spring Boot web graceful shutdown builds on this Boot's graceful shutdown (`server.shutdown=graceful`) is implemented with a `SmartLifecycle` (`WebServerGracefulShutdownLifecycle`) at a phase just below `Integer.MAX_VALUE`, so the web server stops accepting new requests and drains in-flight ones during its shutdown-phase wait, before other beans stop. The same `timeout-per-shutdown-phase` bounds that drain. ## Gotchas / pitfalls - **Forgetting `callback.run()`** — the single most common bug; each affected phase blocks for the full timeout, then logs "failed to shut down in X ms". - **Doing blocking work directly in `stop(Runnable)` on the caller thread** defeats the purpose; hand off long draining to a background executor and call the callback from there. - **`isRunning()` must return false once stopped** so the processor doesn't try to stop it again and so a later `start()` behaves. - **Per-phase, not global** — many phases with slow beans multiply the worst-case shutdown time.

  • What happens if a bean's stop(Runnable) never calls the callback?
    Its phase blocks for the full timeoutPerShutdownPhase (default 30s), the processor logs a warning naming the bean, then proceeds. Shutdown is delayed but not deadlocked.
  • Is the shutdown timeout global or per phase, and why does that matter?
    Per phase. Worst-case total shutdown ≈ timeout × number of phases with laggard beans, so many phases with slow drains can multiply shutdown time.
  • How does Spring Boot's graceful web shutdown relate to this mechanism?
    It's a SmartLifecycle at a phase just under Integer.MAX_VALUE; the server stops accepting requests and drains in-flight ones during that phase's bounded wait, governed by the same per-phase timeout.

context