fix: runtime graceful shutdown - #9951
Conversation
Signed-off-by: Michael Feil <63565275+michaelfeil@users.noreply.github.com>
|
👋 Hi michaelfeil! Thank you for contributing to ai-dynamo/dynamo. Just a reminder: The 🚀 |
WalkthroughThis PR implements runtime graceful shutdown: adds configurable graceful timeout, defers PushEndpoint stop until inflight requests drain, introduces discovery unregister lifecycle tied to endpoint shutdown, and orchestrates shutdown phases with a timeout. ChangesGraceful Shutdown with Configurable Timeout
🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/runtime/src/pipeline/network/ingress/push_endpoint.rs (1)
63-77:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPrioritize cancellation before
endpoint.next()intokio::select!With
tokio::select! { biased; ... }, branches are polled top-to-bottom and the first ready branch in that order is selected—so if cancellation andendpoint.next()are both ready, the request branch can run for the iteration. Reorder the branches so shutdown always wins when cancellation is ready.💡 Suggested change
let req = tokio::select! { biased; - // await on service request - req = endpoint.next() => { - req - } - // process shutdown _ = self.cancellation_token.cancelled() => { tracing::info!( "PushEndpoint received cancellation signal, stopping service after inflight requests drain" ); stop_service_after_drain = true; break; } + + // await on service request + req = endpoint.next() => { + req + } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/runtime/src/pipeline/network/ingress/push_endpoint.rs` around lines 63 - 77, The tokio::select! in PushEndpoint currently lists the endpoint.next() branch before self.cancellation_token.cancelled(), so with biased selection a ready cancellation can be lost; to fix, reorder the branches inside the select! so the cancellation branch (awaiting self.cancellation_token.cancelled()) appears before the endpoint.next() branch (the req = endpoint.next() => { ... }) while keeping biased; and preserve the existing behavior of setting stop_service_after_drain and breaking when cancellation fires.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/runtime/src/runtime.rs`:
- Around line 327-335: The runtime_graceful_shutdown_timeout function silently
falls back to DEFAULT_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS when the env var
DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS is missing or unparsable; change it
to log invalid values: read the env var first, if missing keep current default
but emit a debug/info log, and if present but s.parse::<u64>() fails emit a
warning (including the variable name and the bad value) that you are falling
back to DEFAULT_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS; update
runtime_graceful_shutdown_timeout to perform these checks and use the crate's
logging/tracing facility so callers can see misconfigurations.
---
Outside diff comments:
In `@lib/runtime/src/pipeline/network/ingress/push_endpoint.rs`:
- Around line 63-77: The tokio::select! in PushEndpoint currently lists the
endpoint.next() branch before self.cancellation_token.cancelled(), so with
biased selection a ready cancellation can be lost; to fix, reorder the branches
inside the select! so the cancellation branch (awaiting
self.cancellation_token.cancelled()) appears before the endpoint.next() branch
(the req = endpoint.next() => { ... }) while keeping biased; and preserve the
existing behavior of setting stop_service_after_drain and breaking when
cancellation fires.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ea7cc821-16ab-4c29-b597-4573755daef0
📒 Files selected for processing (4)
lib/runtime/src/component/endpoint.rslib/runtime/src/config/environment_names.rslib/runtime/src/pipeline/network/ingress/push_endpoint.rslib/runtime/src/runtime.rs
Signed-off-by: Michael Feil <63565275+michaelfeil@users.noreply.github.com>
Signed-off-by: Michael Feil <63565275+michaelfeil@users.noreply.github.com>
Signed-off-by: Michael Feil <63565275+michaelfeil@users.noreply.github.com>
614e682 to
1adf149
Compare
|
@CodeRabbit can you perform a stricter review and find issues with it across the codebase? |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plan_graceful.md`:
- Line 24: The plan document currently hardcodes the global timeout as "840
seconds" which is inconsistent with the PR's runtime contract; update every
occurrence (including the line with "Default global budget: 840 seconds." and
the other instances around the referenced sections) to the new default "870
seconds" and ensure any explanatory text about Kubernetes grace/teardown
references the 900s cluster grace and ~30s teardown accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1d95399d-641a-4c53-8d58-3b7cf33a43f8
📒 Files selected for processing (6)
lib/runtime/src/component/endpoint.rslib/runtime/src/config.rslib/runtime/src/config/environment_names.rslib/runtime/src/pipeline/network/ingress/push_endpoint.rslib/runtime/src/runtime.rsplan_graceful.md
Signed-off-by: Michael Feil <63565275+michaelfeil@users.noreply.github.com>
|
/ok to test 106a8f9 |
kthui
left a comment
There was a problem hiding this comment.
Thanks for implementing graceful shutdown logic at the Rust level! This will help making graceful shutdown behavior unified across backends.
LGTM overall!
Since both discovery and endpoint unregistration happens in parallel at the same time upon shutdown, it creates a ms interval where some request(s) may still be routed from the frontend to the worker. Would it be better if we wait for the full graceful shutdown timeout interval, before stopping the endpoint?
For instance:
- unregister from discovery
- wait for the full graceful shutdown timeout interval, while allowing existing/new requests to be served.
- unregister the endpoint
- drop all ongoing requests, if any
- shutdown the worker
For requests that are sent after shutdown begins but before the frontend acknowledges the worker unregistration, they can still be processed normally as long as they complete within the graceful shutdown timeout interval, so we avoid failing any request due to shutdown.
The tradeoff is each shutdown will take longer, but still bounded by the graceful shutdown timeout interval.
|
Picking this up as #11068 — rebased onto post-#10705 Context: a production deployment hit a lease-loss "zombie worker" — a worker loses its etcd lease while a request is stuck in-flight (an engine that can't make progress and can't be aborted), the unbounded per-endpoint drain wedges, Two adjustments vs this branch:
I also added a GPU regression test that reproduces the zombie by freezing the vLLM engine mid-generation (a genuinely non-cancellable inflight) and asserting the worker still exits: without the bound it wedges (still running at 60s); with it, the drain times out and the worker exits (~27s). Does this line up with the internal fix you mentioned — anything from your version that should fold in here? — Neelay + 🤖 |
Signed-off-by: Michael Feil 63565275+michaelfeil@users.noreply.github.com
Improve Graceful Shutdown
Summary
Improve graceful shutdown to remove workers from routing immediately while still completing accepted work within a bounded budget. This prevents routers from selecting draining workers and ensures active streaming requests complete before service shutdown.
Problem
Runtime graceful shutdown currently drains inflight work too late in two critical places:
ControlMessage::Stopand surface as cancelled or 500 responsesDuring rollout restart or pod termination, a worker should remove itself from routing quickly while still finishing work it has already accepted. We also have issue with orphaned pods. Deleting it from k8s cluster with force would leave the pod running. If something goes wrong in the worker (e.g. drain takes forever due to the casual deadlock of inference engine), the worker would hog gpu memory forever / until the cluster decides to delete worker (5h).
Changes
1. Unpublish Discovery at Drain Start
2. Drain Before Stopping PushEndpoint
graceful_shutdownflag - non-graceful shutdowns maintain immediate stop behavior3. Add Global Runtime Shutdown Budget
DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECSenvironment variableTesting
All tests use fast deterministic timeouts (milliseconds) and avoid external services.
Configuration
New environment variable:
DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS: Maximum time in seconds the runtime waits for graceful endpoint drain before tearing down shared transports (default: 870)Kubernetes Alignment
Recommended configuration:
This leaves roughly 30 seconds for final cleanup, transport teardown, and process exit after the runtime stops waiting gracefully.
Related
plan_graceful.mdSummary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests