Skip to content

fix: scope KV router cancellation to router lifetime - #10730

Closed
AmeenP wants to merge 1 commit into
ai-dynamo:mainfrom
AmeenP:codex/fix-kv-router-lifecycle-cancellation
Closed

fix: scope KV router cancellation to router lifetime#10730
AmeenP wants to merge 1 commit into
ai-dynamo:mainfrom
AmeenP:codex/fix-kv-router-lifecycle-cancellation

Conversation

@AmeenP

@AmeenP AmeenP commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

This scopes KV-router and worker-monitor background work to the lifetime of the owner that created it, instead of using process/runtime-level cancellation for WorkerSet-scoped work.

Refs #10729.

What changed

  • Create a router-owned cancellation token in KvRouter::new using component.drt().child_token().
  • Pass the router token through indexer construction, scheduler startup, event-plane / JetStream subscriber startup, and WorkerQueryClient recovery/discovery.
  • Stop deriving component.drt().primary_token() inside router-owned indexer/subscriber/recovery paths.
  • Use the router token for scheduler queue metrics and scheduler worker monitoring work.
  • Give KvWorkerMonitor an owned cancellation token that is canceled when the last monitor handle is dropped, while still bridging runtime shutdown into that owned token.

Why

On LoRA-enabled KV-router deployments, frontend RSS and thread count can grow until OOM. The suspected root cause is that WorkerSet/router-scoped background tasks survive WorkerSet/router removal because they are tied to runtime/process cancellation instead of owner lifetime.

LoRA deployments make this especially visible because model-card/adapter churn repeatedly exercises WorkerSet/router teardown and rebuild.

Validation

Passed locally on current origin/main after rebase:

git diff --check origin/main..HEAD
PATH=/Users/ameenp/.cargo/bin:$PATH cargo fmt --all --check
PATH=/Users/ameenp/.cargo/bin:$PATH cargo check -p dynamo-llm --no-default-features --locked
PATH=/Users/ameenp/.cargo/bin:$PATH cargo clippy -p dynamo-llm --no-default-features --locked -- -D warnings

Follow-up validation

  • Deploy under Freyr-like LoRA churn and compare frontend RSS/thread count/discovery stream count before and after this change.

Summary by CodeRabbit

  • Refactor
    • Enhanced cancellation and shutdown coordination across KV router and monitoring components for improved consistency in lifecycle management and graceful termination.

@copy-pr-bot

copy-pr-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi AmeenP! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor router Relates to routing, KV-aware routing, etc. labels Jun 15, 2026
@AmeenP AmeenP changed the title [codex] Scope KV router cancellation to router lifetime Scope KV router cancellation to router lifetime Jun 15, 2026
@AmeenP
AmeenP force-pushed the codex/fix-kv-router-lifecycle-cancellation branch from 5d4f5a7 to 88a89ec Compare June 15, 2026 18:53
@AmeenP
AmeenP marked this pull request as ready for review June 15, 2026 18:54
@AmeenP
AmeenP requested a review from a team June 15, 2026 18:54
@AmeenP AmeenP changed the title Scope KV router cancellation to router lifetime fix: scope KV router cancellation to router lifetime Jun 15, 2026
@github-actions github-actions Bot added the fix label Jun 15, 2026
Signed-off-by: AmeenP <ameenp360@gmail.com>
@AmeenP
AmeenP force-pushed the codex/fix-kv-router-lifecycle-cancellation branch from 88a89ec to ca361d1 Compare June 15, 2026 18:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88a89ec8cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/llm/src/kv_router.rs
kv_router_config.validate()?;
let component = endpoint.component();
let cancellation_token = component.drt().primary_token();
let cancellation_token = component.drt().child_token();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep router token off endpoint shutdown

Runtime::child_token() is tied to the endpoint-shutdown token (lib/runtime/src/runtime.rs:293-295), and shutdown cancels that in Phase 1 before waiting for graceful endpoints (lib/runtime/src/runtime.rs:322-338). Because this router token is now passed into the KV indexer/subscriber paths, single-thread KV routers (router_event_threads=1) can stop their KvIndexer during the graceful-drain window and make in-flight requests hit IndexerOffline; the previous primary_token() stayed alive until Phase 3. Use a router-owned child of the primary token, while still canceling it in Drop.

Useful? React with 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Across the KV router subsystem, cancellation tokens are no longer derived internally from component.drt(). Instead, KvRouter::new creates a child DRT token and passes it down through Indexer::new, KvScheduler::start, SideIndexer::new_predict_on_route, and the entire recovery subscriber pipeline. Separately, KvWorkerMonitor gains a drop-triggered shared cancellation mechanism with a runtime-to-monitor bridge task.

Changes

KV Router Cancellation Token Externalization

Layer / File(s) Summary
KvWorkerMonitor shared drop-triggered cancellation
lib/llm/src/discovery/worker_monitor.rs
Adds MonitorCancellation struct with a Drop-triggered CancellationToken, attaches it to KvWorkerMonitor::new, and spawns a tokio::select! bridge in start_monitoring to propagate runtime cancellation into the shared monitor token.
KvRouter child token creation and downstream wiring
lib/llm/src/kv_router.rs
KvRouter::new switches from the primary DRT token to a child token; threads it into Indexer::new, KvScheduler::start, and the indexer::start_subscriber call.
Indexer::new and SideIndexer::new_predict_on_route token propagation
lib/llm/src/kv_router/indexer/mod.rs, lib/llm/src/kv_router/indexer/side.rs
Both Indexer::new and SideIndexer::new_predict_on_route gain a cancellation_token parameter; all internal derivation via component.drt().primary_token() is removed across remote and local indexer branches.
KvScheduler::start token propagation
lib/llm/src/kv_router/scheduler.rs
KvScheduler::start gains a cancellation_token parameter replacing the internal component.drt().child_token() for both LocalScheduler::new_with_overlap_refresh and the metrics-update task; unit test updated.
Recovery subscriber chain token propagation
lib/llm/src/kv_router/indexer/recovery/subscriber.rs, lib/llm/src/kv_router/indexer/recovery/jetstream.rs, lib/llm/src/kv_router/indexer/recovery/worker_query.rs
start_subscriber, start_kv_router_background, start_kv_router_background_event_plane, and WorkerQueryClient::spawn each gain an explicit cancellation_token/cancel_token parameter, removing all internal token derivation from the recovery pipeline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: scoping KV router cancellation to router lifetime, which is the primary objective of this PR.
Description check ✅ Passed The description comprehensively covers the overview, detailed changes, reviewer guidance, validation steps, and references the related issue #10729, aligning well with the template requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (3)
lib/llm/src/discovery/worker_monitor.rs (1)

555-576: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel the per-attempt token when monitor startup fails.

The bridge task is spawned before list_and_watch can fail; on the error path only started is reset, so retries can leave bridge tasks parked until runtime or monitor shutdown. Cancel this child token before returning the error.

Proposed fix
             Err(e) => {
                 tracing::error!("KvWorkerMonitor: failed to create discovery stream: {}", e);
+                cancellation_token.cancel();
                 // Reset started flag so retry can work
                 self.started.store(false, Ordering::SeqCst);
                 return Err(e);
             }
🤖 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/llm/src/discovery/worker_monitor.rs` around lines 555 - 576, The bridge
task spawned at the beginning of the code block uses a child cancellation token
that remains active even when list_and_watch fails. In the error handling block
where the started flag is reset and the error is returned, add a call to cancel
the cancellation_token (the child token created at the start) before returning
the Err(e). This ensures the bridge task is properly cleaned up on retry
attempts rather than remaining parked until runtime or monitor shutdown.
lib/llm/src/kv_router/scheduler.rs (1)

353-364: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cancel the scheduler token used by the test.

Line 363 now passes an independent token, so Line 395 no longer stops the scheduler or metrics task. Keep the token in a variable and cancel that instead.

Proposed fix
+        let cancellation_token = CancellationToken::new();
         let scheduler = KvScheduler::start(
             component.clone(),
             64,
             cfg_rx,
             DefaultWorkerSelector::new(Some(config.clone()), "decode"),
             &config,
             None,
             None::<Arc<NoopOverlapScoresRefresh>>,
             None,
             "decode",
-            CancellationToken::new(),
+            cancellation_token.clone(),
         )
         .await
         .unwrap();
 ...
-        component.drt().primary_token().cancel();
+        cancellation_token.cancel();

Also applies to: 395-395

🤖 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/llm/src/kv_router/scheduler.rs` around lines 353 - 364, The test is
creating a CancellationToken inline in the KvScheduler::start call at line
353-364, but then at line 395 needs to cancel the scheduler. Since the token is
not stored in a variable, line 395 cannot reference it to perform the
cancellation. Extract the CancellationToken::new() call into a variable before
the KvScheduler::start invocation, pass that variable instead of the inline
token creation, and then use that same variable at line 395 to cancel the
scheduler token.
lib/llm/src/kv_router.rs (1)

228-287: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel the router token on KvRouter::new failure paths.

This token is only cancelled in Drop, but Drop never runs if KvRouter::new fails after Indexer::new, KvScheduler::start, start_subscriber, or ensure_served_indexer_service has started background work. Add a construction guard that cancels the token on early return, then disarm it immediately before returning Ok(Self { ... }).

Sketch of the guard pattern
+struct CancelOnDrop(Option<tokio_util::sync::CancellationToken>);
+
+impl CancelOnDrop {
+    fn new(token: tokio_util::sync::CancellationToken) -> Self {
+        Self(Some(token))
+    }
+
+    fn disarm(mut self) {
+        self.0.take();
+    }
+}
+
+impl Drop for CancelOnDrop {
+    fn drop(&mut self) {
+        if let Some(token) = self.0.take() {
+            token.cancel();
+        }
+    }
+}
+
 ...
         let component = endpoint.component();
         let cancellation_token = component.drt().child_token();
+        let cancellation_guard = CancelOnDrop::new(cancellation_token.clone());
 ...
-        Ok(Self {
+        let router = Self {
             indexer,
             scheduler,
             workers_with_configs,
             block_size,
             kv_router_config,
             prefill_load_estimator,
             cancellation_token,
             client,
             is_eagle,
             _served_indexer_handle: served_indexer_handle,
             shared_cache,
-        })
+        };
+        cancellation_guard.disarm();
+        Ok(router)

Also applies to: 296-327

🤖 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/llm/src/kv_router.rs` around lines 228 - 287, Implement a cancellation
guard pattern to ensure the cancellation_token is properly cancelled if
KvRouter::new fails after background work has started. Create a guard variable
that wraps the cancellation_token immediately after obtaining it from
component.drt().child_token(). The guard should automatically cancel the token
in its Drop implementation. Before the final Ok(Self { ... }) return statement,
call a method to disarm the guard to prevent cancellation on successful
initialization. This ensures that if any of the subsequent operations
(Indexer::new, KvScheduler::start, start_subscriber, or
ensure_served_indexer_service) fail and the function returns early, the
background tasks will be properly cancelled rather than left running orphaned.
🤖 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/llm/src/kv_router/indexer/recovery/worker_query.rs`:
- Around line 90-100: The cancellation token is only applied to the discovery
loop in the spawn function, but recovery tasks can continue running after
cancellation. Store the cancel_token as a field in the struct by adding it to
the `new` method signature and storing it in Self. In the spawn function at
lines 90-100, pass the cancel_token to the `new` call. In the
spawn_recovery_task method at lines 358-384, clone the stored cancel_token and
pass it to the task spawning logic. In the recovery task implementation at lines
531-590, wrap all awaitable operations (jitter sleeps, semaphore acquisition,
retry backoffs, and query_worker calls) in tokio::select! branches that also
listen for the cancel_token.cancelled() signal to allow immediate cancellation
when the token fires.

---

Outside diff comments:
In `@lib/llm/src/discovery/worker_monitor.rs`:
- Around line 555-576: The bridge task spawned at the beginning of the code
block uses a child cancellation token that remains active even when
list_and_watch fails. In the error handling block where the started flag is
reset and the error is returned, add a call to cancel the cancellation_token
(the child token created at the start) before returning the Err(e). This ensures
the bridge task is properly cleaned up on retry attempts rather than remaining
parked until runtime or monitor shutdown.

In `@lib/llm/src/kv_router.rs`:
- Around line 228-287: Implement a cancellation guard pattern to ensure the
cancellation_token is properly cancelled if KvRouter::new fails after background
work has started. Create a guard variable that wraps the cancellation_token
immediately after obtaining it from component.drt().child_token(). The guard
should automatically cancel the token in its Drop implementation. Before the
final Ok(Self { ... }) return statement, call a method to disarm the guard to
prevent cancellation on successful initialization. This ensures that if any of
the subsequent operations (Indexer::new, KvScheduler::start, start_subscriber,
or ensure_served_indexer_service) fail and the function returns early, the
background tasks will be properly cancelled rather than left running orphaned.

In `@lib/llm/src/kv_router/scheduler.rs`:
- Around line 353-364: The test is creating a CancellationToken inline in the
KvScheduler::start call at line 353-364, but then at line 395 needs to cancel
the scheduler. Since the token is not stored in a variable, line 395 cannot
reference it to perform the cancellation. Extract the CancellationToken::new()
call into a variable before the KvScheduler::start invocation, pass that
variable instead of the inline token creation, and then use that same variable
at line 395 to cancel the scheduler token.
🪄 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: 58736094-7527-4f74-b4b6-0debfcc4c86d

📥 Commits

Reviewing files that changed from the base of the PR and between 6a37976 and 88a89ec.

📒 Files selected for processing (8)
  • lib/llm/src/discovery/worker_monitor.rs
  • lib/llm/src/kv_router.rs
  • lib/llm/src/kv_router/indexer/mod.rs
  • lib/llm/src/kv_router/indexer/recovery/jetstream.rs
  • lib/llm/src/kv_router/indexer/recovery/subscriber.rs
  • lib/llm/src/kv_router/indexer/recovery/worker_query.rs
  • lib/llm/src/kv_router/indexer/side.rs
  • lib/llm/src/kv_router/scheduler.rs

Comment on lines +90 to 100
pub async fn spawn(
component: Component,
indexer: Indexer,
cancel_token: tokio_util::sync::CancellationToken,
) -> Result<Arc<Self>> {
let transport = Arc::new(RuntimeWorkerQueryTransport::new(&component).await?);
let client = Self::new(component.clone(), indexer, transport);

let client_bg = client.clone();
let cancel_token = component.drt().primary_token();
tokio::spawn(async move {
if let Err(e) = client_bg.run_discovery_loop(cancel_token).await {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Store the cancellation token and apply it to recovery tasks too.

The new token only stops the discovery loop. Recovery tasks spawned by spawn_recovery_task still hold Arc<WorkerQueryClient> and can continue through jitter, semaphore waits, retries, and worker queries after the router token is cancelled.

Direction for the fix
 pub struct WorkerQueryClient {
     component: Component,
     transport: Arc<dyn WorkerQueryTransport>,
     /// Indexer for applying recovered events and worker removals.
     indexer: Indexer,
+    cancel_token: tokio_util::sync::CancellationToken,
     worker_states: DashMap<WorkerId, Arc<Mutex<WorkerState>>>,
     query_endpoints: WorkerQueryEndpointDirectory,
     recovery_semaphore: Arc<Semaphore>,
 }

Then pass the token through new, clone it in spawn_recovery_task, and wrap jitter sleeps, semaphore acquisition, retry backoffs, and query_worker(...) waits in tokio::select! branches that return when cancel_token.cancelled() fires.

Also applies to: 358-384, 531-590

🤖 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/llm/src/kv_router/indexer/recovery/worker_query.rs` around lines 90 -
100, The cancellation token is only applied to the discovery loop in the spawn
function, but recovery tasks can continue running after cancellation. Store the
cancel_token as a field in the struct by adding it to the `new` method signature
and storing it in Self. In the spawn function at lines 90-100, pass the
cancel_token to the `new` call. In the spawn_recovery_task method at lines
358-384, clone the stored cancel_token and pass it to the task spawning logic.
In the recovery task implementation at lines 531-590, wrap all awaitable
operations (jitter sleeps, semaphore acquisition, retry backoffs, and
query_worker calls) in tokio::select! branches that also listen for the
cancel_token.cancelled() signal to allow immediate cancellation when the token
fires.

@biswapanda

Copy link
Copy Markdown
Contributor

/ok to test ca361d1

@AmeenP

AmeenP commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded: #11390 (merged 2026-07-09) landed the core of this design — router-owned child_token() in KvRouter::new with a drop guard and impl Drop cancelling background tasks. Combined with the WorkerLoadMonitor rework, the full footprint this PR targeted is covered at current main: no primary_token derivations remain in kv_router.rs, scheduler.rs, indexer/ (incl. recovery paths), or worker_monitor.rs. No meaningful delta left to rebase onto the post-#11841 structure.

@AmeenP AmeenP closed this Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix router Relates to routing, KV-aware routing, etc. size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants