fix(health): use local endpoint registry for canary health checks - #8294
Conversation
nnshah1
left a comment
There was a problem hiding this comment.
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.
7f23f72 to
2a09c0c
Compare
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>
2a09c0c to
9074bfc
Compare
Signed-off-by: Thomas Montfort <tmontfort@nvidia.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughRefactored Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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.
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 | 🟠 MajorUse
response.is_ok()instead ofresponse.data.is_some()to check health.The current check incorrectly treats any frame with
data: Noneas unhealthy. TheAnnotatedtype definesis_ok()asevent.as_deref() != Some("error")—the event field signals error status, not data presence. Metadata and event frames (SSE keep-alives, comments, ids) commonly havedata: Noneand a non-error event, and would incorrectly flip the endpoint toNotReadywith the current logic. Other in-process callers likelib/llm/src/http/service/openai.rs(lines 1708–1709) andlib/llm/src/http/service/anthropic.rs(lines 395–396) validate bothdata.is_none()ANDevent != "error"when inspecting stream frames. Replace withresponse.is_ok()at line 237, and update thedata.is_none()branch (lines 244–250) to checkresponse.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 | 🟡 MinorDowngrade 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 untilregister_local_engine()fires. Consider returning a typed error (orOk(())with a debug log) for the "not yet registered" case so the caller can log it atdebug!/warn!instead oferror!.Note: The concern about deregistration causing "stuck-at-Ready" is unfounded.
LocalEndpointRegistrycontains noremove(),unregister(), orclear()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
📒 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>
Good point. The python binding |
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>
nnshah1
left a comment
There was a problem hiding this comment.
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>
…-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>
…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>
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. Thewait_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 duringEndpointConfigBuilder::start()using the sameendpoint.namekey. This eliminates all discovery timing dependencies and instance_id matching issues.Details:
lib/runtime/src/health_check.rs:send_health_check_request()to usedrt.local_endpoint_registry().get(endpoint_subject)→engine.generate()(same pattern ascall_lora_endpointinsystem_status_server.rs)RouterCachetype alias,router_cachefield, andget_or_create_router()methodClient,Component,Endpoint,Instance,PushRouter,Context,ManyOut,Annotated,MaybeError,Serialize,Deserialize,Instant,MissedTickBehavior,intervalstart(),spawn_endpoint_health_check_task(),spawn_new_endpoint_monitor())Where should the reviewer start?
lib/runtime/src/health_check.rs— specifically the newsend_health_check_request()method (line ~240). The old method was ~140 lines of discovery/routing logic; the new one is ~80 lines with a singlelocal_endpoint_registry.get()→engine.generate()call.Related Issues:
Summary by CodeRabbit