fix(runtime): bound endpoint drain + route etcd lease loss through Runtime::shutdown() - #11068
Conversation
WalkthroughRoutes etcd lease-loss through phased DIS-2295: Phased shutdown on lease loss with bounded inflight drain
ChangesDIS-2295: Phased shutdown on lease loss with bounded inflight drain
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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/pipeline/network/ingress/push_endpoint.rs`:
- Line 175: The source comments in push_endpoint’s ingress path still reference
an internal Linear ticket, which must be removed. Update the comment near the
lease-loss worker note to replace DIS-2295 with the corresponding public GitHub
issue reference, or omit the ticket reference entirely. Also scan the nearby
comment(s) in push_endpoint.rs for any other internal ticket IDs and replace
them consistently.
In `@lib/runtime/src/transports/etcd/lease.rs`:
- Line 17: Replace the internal Linear ticket reference in the comment on the
lease-related code with a public GitHub issue reference or remove the ticket
mention entirely. Update the comment near the lease field in the etcd transport
code, and also scan the surrounding lease-related comment(s) for any other
internal ticket IDs so they no longer reference DIS-2295 or similar internal
tracking numbers.
In `@tests/fault_tolerance/etcd_ha/test_vllm.py`:
- Line 454: Replace the internal Linear ticket marker in the added comment with
the matching public GitHub issue reference. Update the source comment/docstring
in the test_vllm-related change so it uses a public format like GH-NNNN or
`#NNNN`, and ensure any other added occurrences in the same diff are updated
consistently.
- Around line 519-530: The _freeze_vllm_engine_processes helper currently uses
pgrep -f EngineCore, which can match unrelated vLLM engines on the host; update
it to scope the search to this test worker’s process tree by starting from the
worker PID and only selecting descendant EngineCore processes. Keep the freezing
logic in _freeze_vllm_engine_processes, and switch the logger.info call to lazy
formatting so pid interpolation is deferred.
- Around line 499-516: The background helper in _run currently swallows all
exceptions and returns a thread that the test never validates, so the test can
pass even if the in-flight request ends early. Update the thread logic in _run
to catch only the expected request/connection-related exception(s), use lazy
logger formatting instead of an f-string, and make the failure visible to the
caller. Then, in the test flow that starts the thread and freezes the engine,
assert the thread is still alive before proceeding so the pinned request is
actually enforced; apply the same fix to the duplicated logic noted by the other
affected locations.
- Around line 576-594: The frozen vLLM engine cleanup is happening too late
because the outer finally around the whole test block only runs after the
worker/frontend/etcd context managers unwind. Move the SIGCONT/SIGKILL handling
for frozen_pids into an inner finally immediately after
_freeze_vllm_engine_processes() and before the teardown-prone
wait_for_processes_to_terminate flow, so the cleanup runs even if context
teardown hangs.
- Around line 534-538: Add the missing vLLM sizing markers to the test decorated
near the existing gpu_1/e2e/nightly marks in test_vllm.py so the scheduler can
account for its resource needs. Update the same test function that already has
FAULT_TOLERANCE_MODEL_NAME, timeout, and the module-level pytest.mark.vllm by
applying both pytest.mark.profiled_vram_gib(...) and
pytest.mark.requested_vllm_kv_cache_bytes(...) with the appropriate values used
elsewhere in this suite.
🪄 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: Enterprise
Run ID: 122bc114-ab7e-4d6a-a585-07e985509247
📒 Files selected for processing (5)
lib/runtime/src/pipeline/network/ingress/push_endpoint.rslib/runtime/src/runtime.rslib/runtime/src/transports/etcd.rslib/runtime/src/transports/etcd/lease.rstests/fault_tolerance/etcd_ha/test_vllm.py
2954065 to
4bb4edf
Compare
4bb4edf to
596b8af
Compare
596b8af to
8332dc1
Compare
8332dc1 to
b80e721
Compare
b80e721 to
c272f57
Compare
|
@CodeRabbit full review |
✅ Action performedFull review finished. |
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 `@tests/fault_tolerance/etcd_ha/test_vllm.py`:
- Around line 462-471: The parametrized E2E test is still using shared fixed
ports via FRONTEND_PORT and the default NatsServer/stack setup, which can
collide with xdist or other HA tests. Update the test helpers around
_zombie_verify_serving and the related frontend/worker startup paths to accept
injected ports, then switch to dynamic allocation using NatsServer(request,
port=0) and dynamo_dynamic_ports or allocate_port()/allocate_ports(). Thread the
allocated frontend and system/NATS ports through the request helpers and process
managers so the tcp/nats cases run in isolation without hardcoded port values.
🪄 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: Enterprise
Run ID: 387dbdff-e6bc-483a-a9f8-17cef2472533
📒 Files selected for processing (7)
lib/runtime/src/pipeline/network/ingress.rslib/runtime/src/pipeline/network/ingress/push_endpoint.rslib/runtime/src/pipeline/network/ingress/shared_tcp_endpoint.rslib/runtime/src/runtime.rslib/runtime/src/transports/etcd.rslib/runtime/src/transports/etcd/lease.rstests/fault_tolerance/etcd_ha/test_vllm.py
kthui
left a comment
There was a problem hiding this comment.
LGTM!
One limitation worth calling out: this assumes the stuck in-flight request affects only the Python/engine side and that the primary tokio executor remains healthy, so it can run the phased shutdown and enforce the drain timeout. If tokio itself is stalled, this change would not guarantee worker termination.
c272f57 to
2c88681
Compare
|
@kthui agreed, and good to call out. This fix targets the common case — a stuck request on the engine/native side while the tokio runtime stays healthy — so the phased shutdown + bounded drain can run. A fully-stalled tokio executor would defeat any cooperative mechanism here (including the existing — Neelay + 🤖 |
michaelfeil
left a comment
There was a problem hiding this comment.
seems good on high level! Thanks for bounding the draining of inflight requests.
|
actually, i think fully stuck tokio runtime is again OK, since in this case the k8s probe would not come though, etcd would loose the lease, and the pod would get cleared by k8s due to missing response, if the health check is e.g. async and requires tokio. |
On endpoint teardown the graceful-shutdown path waited on
`while inflight > 0 { notify.notified().await }` with no timeout, on BOTH
request planes (NATS `PushEndpoint` and the default TCP `SharedTcpServer`). A
single stuck inflight request (e.g. one whose engine can no longer make
progress and cannot be aborted) keeps inflight > 0, so the drain wedges, the
serve future never returns, and `Runtime::shutdown()` is never reached — the
worker zombies (Running, /health green, unable to serve).
Add a shared `drain_inflight` helper in the ingress module, bounded by the
existing #10705 `graceful_shutdown_timeout()` (made pub(crate); no new
env/const), and call it from both `PushEndpoint::start` and
`SharedTcpServer::unregister_endpoint`. Tested with paused time: the bounded
wait returns instead of hanging when a request never completes, and still
drains cleanly to zero when it does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: nnshah1 <neelays@nvidia.com>
On lease loss the keep-alive task called a bare `primary_token().cancel()`, tearing the primary token down at once and skipping the phased shutdown sequence (Phase 1 endpoint-token cancel -> Phase 2 bounded graceful drain -> Phase 3 backend teardown). Combined with the previously unbounded endpoint drain, a stuck inflight request left lease-loss workers wedged. Pass the Runtime into `create_lease` and, on an unrecoverable keep-alive error, call `Runtime::shutdown()` instead of a bare token cancel — honoring the documented `etcd::Client::new` contract that a lost lease shuts the worker down. Unit-tested: lease-loss teardown is phased (endpoint token cancels first, primary token only after graceful tasks complete). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: nnshah1 <neelays@nvidia.com>
The existing non-HA shutdown tests kill etcd with no request in flight, so the endpoint drain is instant and they pass even on the unbounded-drain code. The zombie requires an in-flight request that can neither complete nor be aborted (a stuck engine / transfer). This test SIGSTOPs the vLLM engine (rank) process mid-generation so the request stays pinned in the endpoint inflight counter, then kills etcd. With the bounded drain the worker times out the drain (remaining=1) and exits (~26s); without it the worker wedges (still running at 60s). Parametrized over both request planes (default `tcp` SharedTcpServer + `nats` PushEndpoint) since the bound must cover both. Verified RED/GREEN on an RTX A6000 with Qwen3-0.6B. Holds the frontend drain open so only the worker-side behavior is measured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: nnshah1 <neelays@nvidia.com>
2c88681 to
3ba5369
Compare
Summary
Fixes the etcd lease-loss "zombie worker" failure mode: when a worker loses its etcd lease while a request is stuck in-flight, it could fail to shut down — staying
Runningwith/healthgreen but unable to serve, until manually killed.Two root causes:
while inflight > 0 { notify.notified().await }with no timeout. A single stuck in-flight request (e.g. a request whose engine can no longer make progress and cannot be aborted) keepsinflight > 0forever, so the drain wedges, the serve future never returns, andRuntime::shutdown()is never reached.Runtime::shutdown(). The keep-alive task called a bareprimary_token().cancel()instead ofRuntime::shutdown(), skipping the phased shutdown sequence (and the documentedetcd::Client::newcontract that a lost lease shuts the worker down).Changes
push_endpoint.rs— extract the drain intodrain_inflight, bounded by the existinggraceful_shutdown_timeout()from fix(runtime): bound graceful shutdown drain #10705 (madepub(crate); no new env var or const). Returns the count still inflight if the bound fires.etcd/lease.rs— on an unrecoverable keep-alive failure, route throughRuntime::shutdown()instead of a bare token cancel, so Phases 1–3 run in order.tests/fault_tolerance/etcd_ha/test_vllm.py— GPU regression test reproducing the zombie with a frozen vLLM engine (SIGSTOP the engine mid-generation so the in-flight request is non-cancellable, then kill etcd). Verified RED/GREEN on an RTX A6000 / Qwen3-0.6B: without the bound the worker wedges (still running at 60s); with it the worker times out the drain (remaining=1) and exits ~27s.Notes
/healthreflect shutdown (so k8s cycles the pod even if a process can't exit); that readiness-side change is tracked separately.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
Limitation
The phased shutdown and bounded drain run on the tokio runtime, so they rely on the primary executor staying healthy (the common case: a stuck request on the engine/native side). A fully-stalled tokio executor would defeat any cooperative mechanism here (including the existing
exit(911)watchdog); guaranteeing termination in that case needs an OS-thread watchdog or the k8s liveness probe — out of scope for this drain fix.