Skip to content

fix(health): use local endpoint registry for canary health checks - #8294

Merged
tmonty12 merged 5 commits into
mainfrom
tmonty12/dis-1185-canary-use-local-registry
Apr 17, 2026
Merged

fix(health): use local endpoint registry for canary health checks#8294
tmonty12 merged 5 commits into
mainfrom
tmonty12/dis-1185-canary-use-local-registry

Conversation

@tmonty12

@tmonty12 tmonty12 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Overview:

The canary health check used PushRouter/discovery/direct() to send a test request back to itself through the full distributed pipeline. This was fragile in Kubernetes because it depended on discovery settling (EndpointSlice + DynamoWorkerMetadata CR correlation), and PushRouter::direct(instance_id) required an exact instance_id match that broke on pod restarts due to stale data from previous pods. The wait_for_instances() call only checked that any instance existed — not that the correct instance was routable — so the canary would discover a stale pod, then fail with "instance_id not found" when trying to direct-route to itself.

This PR replaces the entire discovery/routing path with a direct in-process call via LocalEndpointRegistry, which is already populated during EndpointConfigBuilder::start() using the same endpoint.name key. This eliminates all discovery timing dependencies and instance_id matching issues.

Details:

  • lib/runtime/src/health_check.rs:
    • Replaced send_health_check_request() to use drt.local_endpoint_registry().get(endpoint_subject)engine.generate() (same pattern as call_lora_endpoint in system_status_server.rs)
    • Removed RouterCache type alias, router_cache field, and get_or_create_router() method
    • Removed unused imports: Client, Component, Endpoint, Instance, PushRouter, Context, ManyOut, Annotated, MaybeError, Serialize, Deserialize, Instant, MissedTickBehavior, interval
    • No changes to the timer/notifier/per-endpoint task lifecycle (start(), spawn_endpoint_health_check_task(), spawn_new_endpoint_monitor())

Where should the reviewer start?

lib/runtime/src/health_check.rs — specifically the new send_health_check_request() method (line ~240). The old method was ~140 lines of discovery/routing logic; the new one is ~80 lines with a single local_endpoint_registry.get()engine.generate() call.

Related Issues:

Summary by CodeRabbit

  • Refactor
    • Simplified internal health check mechanism by removing router-discovery machinery and updating endpoint resolution to use local registry. No public API changes.

@tmonty12
tmonty12 requested a review from a team April 17, 2026 03:16
@github-actions github-actions Bot added the fix label Apr 17, 2026
@tmonty12
tmonty12 marked this pull request as draft April 17, 2026 03:21
Base automatically changed from neelays/dis-1185-race-condition-for-canary-health-check to main April 17, 2026 13:45

@nnshah1 nnshah1 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.

Nice simplification — eliminating the discovery/routing path for canary is a big improvement.

One concern: register_local_engine() is called on the builder (optional), not inside start(). Today all backends go through the Python binding which always calls it (lib.rs:881). But if a future Rust-native backend uses EndpointConfigBuilder with a health_check_payload and forgets register_local_engine(), the canary silently skips it — just logs "endpoint not found in local registry" every cycle with no obvious error.

Suggestion: could start() automatically register the local engine when health_check_payload is provided? Or at minimum, emit a warn! in start() if a payload is set but no local engine is registered — that way it's not a silent failure.

Also heads up — this PR includes the health_check_enabled gate from #8165 (now merged to main). You'll want to rebase to pick up the latest version which renames set_ready()set_endpoint_registered() and trims the comments.

@tmonty12
tmonty12 force-pushed the tmonty12/dis-1185-canary-use-local-registry branch from 7f23f72 to 2a09c0c Compare April 17, 2026 17:59
@copy-pr-bot

copy-pr-bot Bot commented Apr 17, 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.

Replace the PushRouter/discovery/direct() pipeline in the canary health
check with a direct in-process call via LocalEndpointRegistry. The
previous approach was fragile in Kubernetes because it depended on
discovery settling (EndpointSlice + DynamoWorkerMetadata CR correlation)
and required an exact instance_id match via PushRouter::direct() that
broke on pod restarts due to stale data from previous pods.

The LocalEndpointRegistry is already populated during
EndpointConfigBuilder::start() via .register_local_engine(), using the
same endpoint.name key as health check target registration. This
eliminates the discovery race condition entirely.

Removed:
- RouterCache type alias and router_cache field
- get_or_create_router() method
- Discovery/PushRouter/direct() logic in send_health_check_request()
- Unused imports (Client, Component, Endpoint, Instance, PushRouter, etc.)

Relates to DIS-1185

Signed-off-by: Tyler Montfort <tmontfort@nvidia.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tmonty12
tmonty12 force-pushed the tmonty12/dis-1185-canary-use-local-registry branch from 2a09c0c to 9074bfc Compare April 17, 2026 18:02
Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tmonty12
tmonty12 marked this pull request as ready for review April 17, 2026 18:14

@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 found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread lib/runtime/src/health_check.rs Outdated
Comment thread lib/runtime/src/health_check.rs Outdated
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Refactored HealthCheckManager to remove router-discovery and routing machinery. Reworked health check request flow from routing via discovered router to querying the local endpoint registry directly. Updated request/response handling to use engine invocation instead of router operations.

Changes

Cohort / File(s) Summary
Health Check Manager Refactor
lib/runtime/src/health_check.rs
Removed RouterCache type alias, router_cache field, and get_or_create_router() async helper. Reworked send_health_check_request() to use drt.local_endpoint_registry().get() instead of router discovery. Replaced router.direct(request, instance_id) with engine.generate(SingleIn::new(payload)) and changed error detection from response.err() to response.data.is_some(). Updated control-flow and logging for in-process local registry path.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: replacing PushRouter/discovery machinery with LocalEndpointRegistry in the health check module.
Description check ✅ Passed The description covers all required template sections: Overview (problem and solution), Details (specific code changes with file references), Where should the reviewer start (specific file and line guidance), and Related Issues (with the DIS-1185 linear issue link).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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.

❤️ Share

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/runtime/src/health_check.rs (2)

232-253: ⚠️ Potential issue | 🟠 Major

Use response.is_ok() instead of response.data.is_some() to check health.

The current check incorrectly treats any frame with data: None as unhealthy. The Annotated type defines is_ok() as event.as_deref() != Some("error")—the event field signals error status, not data presence. Metadata and event frames (SSE keep-alives, comments, ids) commonly have data: None and a non-error event, and would incorrectly flip the endpoint to NotReady with the current logic. Other in-process callers like lib/llm/src/http/service/openai.rs (lines 1708–1709) and lib/llm/src/http/service/anthropic.rs (lines 395–396) validate both data.is_none() AND event != "error" when inspecting stream frames. Replace with response.is_ok() at line 237, and update the data.is_none() branch (lines 244–250) to check response.is_err() instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime/src/health_check.rs` around lines 232 - 253, The health check
currently treats any frame with response.data == None as failure; instead use
the Annotated success helpers: when reading the first frame from the stream
produced by engine.generate(SingleIn::new(payload)) check response.is_ok() to
mark healthy, and change the branch that currently checks data.is_none() to
check response.is_err() to log warnings/failure; update usage around
response_stream.next().await, response.is_ok(), response.is_err(), and keep
endpoint_subject_owned in the log messages.

107-109: ⚠️ Potential issue | 🟡 Minor

Downgrade log level for expected startup race condition.

The PR description correctly notes that during startup, missing engines mean "the endpoint hasn't finished registering yet—the canary will retry on the next cycle." However, the error at lines 107–109 logs this via error!, which will spam error-level telemetry on every canary cycle during normal startup until register_local_engine() fires. Consider returning a typed error (or Ok(()) with a debug log) for the "not yet registered" case so the caller can log it at debug!/warn! instead of error!.

Note: The concern about deregistration causing "stuck-at-Ready" is unfounded. LocalEndpointRegistry contains no remove(), unregister(), or clear() methods—once an engine is registered, it cannot be deregistered. The registry's design prevents this scenario entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime/src/health_check.rs` around lines 107 - 109, The current error
log in the health-check loop indiscriminately logs any failure from
manager.send_health_check_request as error!, causing noisy telemetry for an
expected startup race; change send_health_check_request to return a typed Result
with a specific variant for "endpoint not registered" (e.g.,
HealthCheckError::EndpointNotRegistered or MissingEngine) and then update the
caller in health_check.rs to match the returned error: if it is the
NotRegistered/MissingEngine variant, log at debug! (or Ok(()) silently) with a
brief message referencing that registration is pending (register_local_engine),
otherwise log true failures at error!. Ensure you reference and update the
send_health_check_request function and the match in the health-check loop that
currently logs the Err(e).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lib/runtime/src/health_check.rs`:
- Around line 232-253: The health check currently treats any frame with
response.data == None as failure; instead use the Annotated success helpers:
when reading the first frame from the stream produced by
engine.generate(SingleIn::new(payload)) check response.is_ok() to mark healthy,
and change the branch that currently checks data.is_none() to check
response.is_err() to log warnings/failure; update usage around
response_stream.next().await, response.is_ok(), response.is_err(), and keep
endpoint_subject_owned in the log messages.
- Around line 107-109: The current error log in the health-check loop
indiscriminately logs any failure from manager.send_health_check_request as
error!, causing noisy telemetry for an expected startup race; change
send_health_check_request to return a typed Result with a specific variant for
"endpoint not registered" (e.g., HealthCheckError::EndpointNotRegistered or
MissingEngine) and then update the caller in health_check.rs to match the
returned error: if it is the NotRegistered/MissingEngine variant, log at debug!
(or Ok(()) silently) with a brief message referencing that registration is
pending (register_local_engine), otherwise log true failures at error!. Ensure
you reference and update the send_health_check_request function and the match in
the health-check loop that currently logs the Err(e).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1b4a2f25-978a-45f0-9dfc-ee842b467251

📥 Commits

Reviewing files that changed from the base of the PR and between 94ee2aa and bb0e84d.

📒 Files selected for processing (1)
  • lib/runtime/src/health_check.rs

When health_check_payload is set and canary is enabled
(DYN_HEALTH_CHECK_ENABLED=true), but the caller forgot to call
.register_local_engine() before .start(), the endpoint now fails
to start with a clear error instead of silently staying NotReady
forever while the canary retries against an empty local registry.

Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tmonty12

Copy link
Copy Markdown
Contributor Author

Nice simplification — eliminating the discovery/routing path for canary is a big improvement.

One concern: register_local_engine() is called on the builder (optional), not inside start(). Today all backends go through the Python binding which always calls it (lib.rs:881). But if a future Rust-native backend uses EndpointConfigBuilder with a health_check_payload and forgets register_local_engine(), the canary silently skips it — just logs "endpoint not found in local registry" every cycle with no obvious error.

Suggestion: could start() automatically register the local engine when health_check_payload is provided? Or at minimum, emit a warn! in start() if a payload is set but no local engine is registered — that way it's not a silent failure.

Also heads up — this PR includes the health_check_enabled gate from #8165 (now merged to main). You'll want to rebase to pick up the latest version which renames set_ready()set_endpoint_registered() and trims the comments.

Good point. The python binding serve_endpoint will always call register_local_engine but not the rust native API for EndpointConfigBuilder. I've made it actually error instead of warn if the canary is enabled with a registered health check payload but no locally registered engine. The user is intending to have canary enabled but it functionally wouldn't work - this fails fast instead of undesired behavior.

Match the original semantics: treat the first stream response as
healthy unless it contains an explicit error. An Annotated item can
have data: None without being an error (e.g., annotation/metadata
events), so checking data.is_some() was too strict.

Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread lib/runtime/src/component/endpoint.rs Outdated
Comment thread lib/runtime/src/health_check.rs Outdated
Comment thread lib/runtime/src/health_check.rs Outdated
Comment thread lib/runtime/src/health_check.rs Outdated
Comment thread lib/runtime/src/component/endpoint.rs Outdated

@nnshah1 nnshah1 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.

LGTM - but would do a simplify comments pass - comments seem a bit verbose and not meaningful without context.

Signed-off-by: tmontfort <tmontfort@nvidia.com>
@tmonty12
tmonty12 enabled auto-merge (squash) April 17, 2026 21:00

@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 found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread lib/runtime/src/health_check.rs
@tmonty12
tmonty12 merged commit c2ec335 into main Apr 17, 2026
90 of 91 checks passed
@tmonty12
tmonty12 deleted the tmonty12/dis-1185-canary-use-local-registry branch April 17, 2026 21:47
nvyutwu pushed a commit to nvyutwu/dynamo that referenced this pull request Apr 20, 2026
…-dynamo#8294)

Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Signed-off-by: tmontfort <tmontfort@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
indrajit96 pushed a commit that referenced this pull request Apr 20, 2026
)

Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Signed-off-by: Thomas Montfort <tmontfort@nvidia.com>
Signed-off-by: tmontfort <tmontfort@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
pvijayakrish pushed a commit that referenced this pull request Apr 30, 2026
…nstance_id-not-found (#8816)

Signed-off-by: nnshah1 <neelays@nvidia.com>
Co-authored-by: Thomas Montfort <61255722+tmonty12@users.noreply.github.com>
Co-authored-by: Thomas Montfort <tmontfort@nvidia.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants