Skip to content

feat: count managed loop iterations, retry failed health discovery#3400

Open
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3175-heartbeat
Open

feat: count managed loop iterations, retry failed health discovery#3400
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3175-heartbeat

Conversation

@chet

@chet chet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Every long-lived manager loop now reports the same heartbeat -- carbide_managed_loop_iterations_total{manager, outcome} -- and health discovery survives failures instead of dying on the first one.

  • One count per pass across the machine update manager, machine validation, the managed-host-state republisher, and health discovery; ok passes stay silent, failures own their historical warn lines. Lock contention counts as ok (another instance holds the work); a lock layer that is down counts as the error it is.
  • The measured boot metrics collector is deliberately absent -- its latency histogram's per-outcome _count already counts its iterations, and the metric description says where to look.
  • Health discovery: the first error used to kill the task, leaving the pod restart as the de-facto retry; it now warns, backs off (1s doubling to 30s with jitter, reset on success), and retries in-process. Expect restart counts to drop and the failure signal to live in this counter instead.
  • The health binary's framework-emitted metrics actually export now: run_service installs the meter provider and merges the framework registry into /metrics (expose_framework_registry finally has a caller), which also lights up the health egress counters and log-event counts.
  • Drive-by: the DPA MQTT stats println! is now a structured tracing::debug!.

Tests added!

This supports #3175

@chet chet requested review from a team and polarweasel as code owners July 11, 2026 03:34
@chet

chet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@chet, on it — I'll redo the full review of this PR.

(=^・ω・^=)

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Observability

    • Added shared managed-loop iteration metrics (carbide_managed_loop_iterations_total) with standardized WARN-on-failure logging.
    • Improved MQTT stats output using structured debug fields (received/sent/pending).
    • Updated documentation for core metrics.
  • Reliability

    • Background maintenance loops now consistently record per-iteration outcomes.
    • Refined lock handling: “already locked” is treated as a successful skip; other lock failures are surfaced as errors.
    • Endpoint discovery loop now uses capped exponential backoff with reset after success.
  • Monitoring / Tests

    • Health discovery loop now reports unified /metrics values and adds timing/backoff tests.
    • Added a lock-unavailability test for MachineUpdateManager.

Walkthrough

Changes

Managed loop observability

Layer / File(s) Summary
Instrumentation contract and exports
crates/utils/*, docs/observability/core_metrics.md
Adds managed-loop managers, outcome metrics, failure logging, recording helpers, tests, exports, dependencies, and metric documentation.
API-core loop integration
crates/api-core/src/dpa/handler.rs, crates/api-core/src/machine_update_manager/*, crates/api-core/src/machine_validation/mod.rs, crates/api-core/src/mqtt_state_change_hook/republisher.rs, crates/api-core/src/tests/machine_update_manager.rs
Records maintenance-loop outcomes, propagates infrastructure lock failures, validates the new error behavior, and converts MQTT statistics to structured debug logging.
Discovery loop engine
crates/health/src/discovery/mod.rs
Adds the single-pass discovery contract, reusable retry loop, exponential backoff reset behavior, and paused-clock metric/timing tests.
Health discovery wiring and metrics
crates/health/src/lib.rs, crates/health/src/main.rs
Wires service discovery into the reusable loop, updates endpoint gauges, exposes framework metrics through Prometheus, and adjusts task completion handling and comments.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServiceDiscoveryIteration
  participant run_discovery_loop
  participant record_iteration
  participant MetricsRegistry
  participant TokioTimer
  run_discovery_loop->>ServiceDiscoveryIteration: run_once()
  ServiceDiscoveryIteration-->>run_discovery_loop: Discovery result and stats
  run_discovery_loop->>record_iteration: Record manager and outcome
  record_iteration->>MetricsRegistry: Increment managed-loop counter
  run_discovery_loop->>TokioTimer: Sleep interval or backoff delay
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: managed loop iteration counting and health discovery retry handling.
Description check ✅ Passed The description is directly aligned with the PR changes, covering managed loop metrics, discovery retries, metrics export, and logging.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions

Copy link
Copy Markdown

@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

🧹 Nitpick comments (2)
crates/utils/src/background_loop.rs (1)

117-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider using check_cases instead of a hand-rolled table loop.

Each row here carries three expected fields (expect_manager, expect_outcome, expect_warn_error) — exactly the scenario the style guide calls out for carbide_test_support::check_cases rather than a bespoke Case/for loop, especially since carbide-test-support is already wired in as a dev-dependency for this crate.

Based on coding guidelines: "Use check_cases / check_values directly when a macro would obscure a table with several inputs or several expected fields per row in table-driven tests."

🤖 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 `@crates/utils/src/background_loop.rs` around lines 117 - 189, The test uses a
bespoke Case table and loop despite matching the intended check_cases pattern.
Refactor record_iteration_counts_every_outcome_and_logs_only_failures to use
carbide_test_support::check_cases, preserving each scenario’s manager, result,
expected labels, and log assertions within the case checks.

Source: Coding guidelines

crates/health/src/discovery/mod.rs (1)

47-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider threading a CancellationToken through run_discovery_loop.

The loop now runs forever with no way to stop gracefully other than aborting the task, unlike the sibling background loops in this same PR (machine_update_manager, machine_validation, republisher), which all accept a cancel_token per the established pattern for client-less background jobs. Worth aligning run_discovery_loop/ServiceDiscoveryIteration wiring in lib.rs with that pattern for consistency and graceful shutdown.

As per coding guidelines: "For background tasks that have no clients, but instead run forever at some interval until the container is terminated, ... prefer accepting an explicit cancellation token from the toplevel initialization method."

♻️ Suggested direction
 pub(crate) async fn run_discovery_loop(
     interval: Duration,
     backoff_config: BackoffConfig,
     mut iteration: impl DiscoveryIteration,
+    cancel_token: CancellationToken,
 ) {
     let mut backoff = ExponentialBackoff::new(&backoff_config);
-    loop {
+    while !cancel_token.is_cancelled() {
         let result = iteration.run_once().await;
         background_loop::record_iteration(BackgroundLoopManager::HealthDiscovery, &result);
         let delay = match result {
             Ok(()) => {
                 backoff.reset();
                 interval
             }
             Err(_) => backoff.next_delay(),
         };
-        tokio::time::sleep(delay).await;
+        cancel_token.run_until_cancelled(tokio::time::sleep(delay)).await;
     }
 }
🤖 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 `@crates/health/src/discovery/mod.rs` around lines 47 - 70, Thread a
CancellationToken through run_discovery_loop and the ServiceDiscoveryIteration
wiring in lib.rs, following the established pattern used by
machine_update_manager, machine_validation, and republisher. Update the loop to
select between cancellation and each iteration/sleep so shutdown exits
gracefully while preserving backoff behavior.

Source: Coding guidelines

🤖 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 `@crates/api-core/src/machine_update_manager/mod.rs`:
- Around line 196-201: Update the tracing::warn! call in the
AcquireLockError::WorkAlreadyLocked branch to pass the error as a structured
field (for example, error = %e) rather than interpolating {e} into the message,
while preserving the existing descriptive message.

---

Nitpick comments:
In `@crates/health/src/discovery/mod.rs`:
- Around line 47-70: Thread a CancellationToken through run_discovery_loop and
the ServiceDiscoveryIteration wiring in lib.rs, following the established
pattern used by machine_update_manager, machine_validation, and republisher.
Update the loop to select between cancellation and each iteration/sleep so
shutdown exits gracefully while preserving backoff behavior.

In `@crates/utils/src/background_loop.rs`:
- Around line 117-189: The test uses a bespoke Case table and loop despite
matching the intended check_cases pattern. Refactor
record_iteration_counts_every_outcome_and_logs_only_failures to use
carbide_test_support::check_cases, preserving each scenario’s manager, result,
expected labels, and log assertions within the case checks.
🪄 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: e9524a02-cee4-4cf4-b1f5-eb63a12a2f59

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3d005 and 6063d36.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/background_loop.rs
  • crates/utils/src/lib.rs
  • docs/observability/core_metrics.md

Comment thread crates/api-core/src/machine_update_manager/mod.rs

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

🧹 Nitpick comments (1)
crates/health/src/discovery/mod.rs (1)

47-70: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

run_discovery_loop has no shutdown mechanism.

run_discovery_loop runs forever with no CancellationToken, and run_service (crates/health/src/lib.rs) spawns it without one either. Every other client‑less background loop touched in this same PR stack — MachineUpdateManager::start, MachineValidationManager::start, and ManagedHostStateRepublisher::start — accepts an explicit cancel_token: CancellationToken per the documented convention for loops without client handles.

Without a token, this loop can only be stopped by aborting the task or killing the process; there is no graceful in‑process shutdown path, and the pattern is now inconsistent with its siblings introduced in this same feature.

As per coding guidelines, "For background tasks that have no clients, but instead run forever at some interval until the container is terminated, prefer accepting an explicit cancellation token from the toplevel initialize_and_start_controllers method, and stop your work when that token is cancelled." As per path instructions, "joined/cancellable background tasks" is an explicit review focus for crates/**/*.rs.

♻️ Sketch of a cancellable loop
 pub(crate) async fn run_discovery_loop(
     interval: Duration,
     backoff_config: BackoffConfig,
     mut iteration: impl DiscoveryIteration,
+    cancel_token: tokio_util::sync::CancellationToken,
 ) {
     let mut backoff = ExponentialBackoff::new(&backoff_config);
-    loop {
+    while !cancel_token.is_cancelled() {
         let result = iteration.run_once().await;
         background_loop::record_iteration(BackgroundLoopManager::HealthDiscovery, &result);
         let delay = match result {
             Ok(()) => {
                 backoff.reset();
                 interval
             }
             Err(_) => backoff.next_delay(),
         };
-        tokio::time::sleep(delay).await;
+        if cancel_token.run_until_cancelled(tokio::time::sleep(delay)).await.is_none() {
+            break;
+        }
     }
 }
🤖 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 `@crates/health/src/discovery/mod.rs` around lines 47 - 70, Make
run_discovery_loop accept a CancellationToken and stop cleanly when it is
cancelled, including while waiting between iterations by selecting cancellation
against tokio::time::sleep. Update run_service and its caller chain to receive
and pass the shared token when spawning the discovery loop, following the
patterns used by MachineUpdateManager::start, MachineValidationManager::start,
and ManagedHostStateRepublisher::start.

Sources: Coding guidelines, Path instructions

🤖 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.

Nitpick comments:
In `@crates/health/src/discovery/mod.rs`:
- Around line 47-70: Make run_discovery_loop accept a CancellationToken and stop
cleanly when it is cancelled, including while waiting between iterations by
selecting cancellation against tokio::time::sleep. Update run_service and its
caller chain to receive and pass the shared token when spawning the discovery
loop, following the patterns used by MachineUpdateManager::start,
MachineValidationManager::start, and ManagedHostStateRepublisher::start.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 672f0202-f04c-4d6b-9fbf-edae67013a3f

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3d005 and 6063d36.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/background_loop.rs
  • crates/utils/src/lib.rs
  • docs/observability/core_metrics.md

@chet chet force-pushed the gh-issue-3175-heartbeat branch from 6063d36 to d9e2b8c Compare July 11, 2026 04:40
@chet

chet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@chet, on it — I'll redo the full review of this PR.

(=^・ω・^=)

Action performed

Full review triggered.

@github-actions

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 271 13 34 91 7 126
machine-validation-runner 804 40 237 296 36 195
machine_validation 804 40 237 296 36 195
machine_validation-aarch64 804 40 237 296 36 195
nvmetal-carbide 804 40 237 296 36 195
TOTAL 3493 173 982 1281 151 906

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@chet, on it — I'll redo the full review of this PR once more.

✅ Action performed

Full review finished.

@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

🧹 Nitpick comments (2)
crates/health/src/discovery/mod.rs (1)

52-70: 🩺 Stability & Availability | 🔵 Trivial

Operational note: in-process retry supersedes the crash-restart signal.

This loop now survives iteration failures and retries with capped backoff rather than terminating the discovery task. The consequence is that a persistently failing discovery (e.g., Carbide API unreachable) will no longer surface as a pod crash-loop; the sole liveness/failure signal becomes carbide_background_loop_iterations_total{manager="health_discovery"} — its heartbeat (series stops moving) and its outcome="error" rate. Recommend ensuring operator alerting is wired to that series (stale-heartbeat + error-rate) so the loss of the previous restart-based signal does not create a blind spot.

The retry/backoff mechanics themselves are sound.

🤖 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 `@crates/health/src/discovery/mod.rs` around lines 52 - 70, Ensure operational
monitoring and alerting covers the retrying health discovery loop in
run_discovery_loop: wire alerts to carbide_background_loop_iterations_total for
manager="health_discovery", detecting both stale or stopped heartbeats and
elevated outcome="error" rates to replace the former crash-loop signal.
crates/api-core/src/mqtt_state_change_hook/republisher.rs (1)

173-178: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a targeted test for the lock-acquisition error branch.

crates/api-core/src/mqtt_state_change_hook/republisher.rs:173-178 now returns an error for non-WorkAlreadyLocked failures, but this path still lacks a focused test. A small case that forces try_acquire_lock to fail with a different error would protect this heartbeat regression path.

🤖 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 `@crates/api-core/src/mqtt_state_change_hook/republisher.rs` around lines 173 -
178, The non-WorkAlreadyLocked error path in the republish lock acquisition
needs coverage. Add a focused test near the existing republisher lock tests that
makes try_acquire_lock return a distinct failure, invokes the relevant republish
function, and asserts the returned error preserves that failure and includes the
managed host state republish lock context.

Source: Coding guidelines

🤖 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 `@crates/api-core/src/machine_update_manager/mod.rs`:
- Around line 196-208: Add a focused test for the non-WorkAlreadyLocked branch
in run_single_iteration(), configuring try_acquire_lock() to return another
AcquireLockError and asserting the method returns
Err(CarbideError::internal(...)) with the expected message rather than Ok(()).

---

Nitpick comments:
In `@crates/api-core/src/mqtt_state_change_hook/republisher.rs`:
- Around line 173-178: The non-WorkAlreadyLocked error path in the republish
lock acquisition needs coverage. Add a focused test near the existing
republisher lock tests that makes try_acquire_lock return a distinct failure,
invokes the relevant republish function, and asserts the returned error
preserves that failure and includes the managed host state republish lock
context.

In `@crates/health/src/discovery/mod.rs`:
- Around line 52-70: Ensure operational monitoring and alerting covers the
retrying health discovery loop in run_discovery_loop: wire alerts to
carbide_background_loop_iterations_total for manager="health_discovery",
detecting both stale or stopped heartbeats and elevated outcome="error" rates to
replace the former crash-loop signal.
🪄 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: 9d6dc6c8-a4b1-4b3d-a4b9-45600cd70e79

📥 Commits

Reviewing files that changed from the base of the PR and between 568c589 and d9e2b8c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/background_loop.rs
  • crates/utils/src/lib.rs
  • docs/observability/core_metrics.md

Comment thread crates/api-core/src/machine_update_manager/mod.rs
@chet chet force-pushed the gh-issue-3175-heartbeat branch from d9e2b8c to aaf3c24 Compare July 11, 2026 08:14
@chet chet changed the title feat: count background loop iterations, retry failed health discovery feat: count managed loop iterations, retry failed health discovery Jul 11, 2026
@chet

chet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@chet, on it — I'll redo the full review of this PR once more.

(=^・ω・^=)

✅ Action performed

Full review finished.

@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

🤖 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 `@docs/observability/core_metrics.md`:
- Line 24: Update the metrics documentation entry for
carbide_background_loop_iterations_total to match the exported
carbide_managed_loop_iterations_total metric, and update the generated source or
associated test if that row is generated; otherwise remove the stale row. Ensure
the published metrics list reflects only the service’s actual metric name.
🪄 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: ac83d8fe-5372-4af0-8645-1a0ee4f6ddb1

📥 Commits

Reviewing files that changed from the base of the PR and between d9e2b8c and aaf3c24.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/api-core/src/tests/machine_update_manager.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/lib.rs
  • crates/utils/src/managed_loop.rs
  • docs/observability/core_metrics.md
✅ Files skipped from review due to trivial changes (1)
  • crates/health/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/utils/Cargo.toml
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs

Comment thread docs/observability/core_metrics.md Outdated

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

🧹 Nitpick comments (1)
crates/api-core/src/mqtt_state_change_hook/republisher.rs (1)

171-174: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a dedicated test for the infrastructure lock-failure path, mirroring machine_update_manager.

This branch now flips the sweep's outcome to error (via managed_loop::record_iteration) instead of silently swallowing it — the same class of change that machine_update_manager/mod.rs made, which was backed by test_run_single_iteration_propagates_lock_infrastructure_failure (shutting down the work-lock manager to force a non-WorkAlreadyLocked error). No equivalent test is visible here for run_sweep.

As per path instructions for crates/**/*.rs, findings about behavior and missing tests are preferred over style-only comments.

🤖 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 `@crates/api-core/src/mqtt_state_change_hook/republisher.rs` around lines 171 -
174, Add a dedicated run_sweep test covering infrastructure lock acquisition
failure, mirroring machine_update_manager’s
test_run_single_iteration_propagates_lock_infrastructure_failure. Shut down the
work-lock manager to produce a non-WorkAlreadyLocked error, invoke run_sweep,
and assert the sweep outcome is recorded as error via
managed_loop::record_iteration rather than swallowed.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@crates/api-core/src/mqtt_state_change_hook/republisher.rs`:
- Around line 171-174: Add a dedicated run_sweep test covering infrastructure
lock acquisition failure, mirroring machine_update_manager’s
test_run_single_iteration_propagates_lock_infrastructure_failure. Shut down the
work-lock manager to produce a non-WorkAlreadyLocked error, invoke run_sweep,
and assert the sweep outcome is recorded as error via
managed_loop::record_iteration rather than swallowed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 53ab5511-7551-41cf-8d56-3d03718d0938

📥 Commits

Reviewing files that changed from the base of the PR and between 76357e2 and aaf3c24.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/api-core/src/tests/machine_update_manager.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/lib.rs
  • crates/utils/src/managed_loop.rs
  • docs/observability/core_metrics.md

Every long-lived manager loop now reports the same heartbeat -- one `carbide_managed_loop_iterations_total{manager, outcome}` count per pass -- so "is this manager alive, and how often does it fail" is one PromQL query across the machine update manager, machine validation, the managed-host-state republisher, and health discovery, instead of four different guesses. And health discovery now survives what used to kill it: a single failed iteration terminated the whole task; it warns, backs off, and retries instead.

- `carbide_managed_loop_iterations_total{manager, outcome}` -- the shared event lives in `carbide-utils` (`managed_loop::record_iteration`), logs nothing on ok (a per-iteration info would be spam) and owns each site's historical failure warn at its historical level. The measured boot metrics collector is deliberately absent: its latency histogram's per-outcome `_count` already is its iteration count, and the metric description points there. Lock contention stays an `ok` pass (another instance is doing the work), but a lock acquisition failing for infrastructure reasons -- database down or overloaded -- now propagates and counts as the `error` it is.
- Health discovery retries with the crate's own `ExponentialBackoff` (1s doubling to a 30s cap, jitter, reset on success) instead of propagating the first error out of the loop. Until now the task died, `run_service` returned, and the pod restart was the de-facto retry -- so expect restart counts to drop and the failure signal to live in this counter and its warn instead. The paused-clock test scripts fail/fail/ok/fail/ok and pins the widening gaps, the reset after success, and the exact counter deltas.
- The health binary's framework-emitted metrics actually export now: `run_service` installs the meter provider and hands the framework registry to the existing `/metrics` merge (`expose_framework_registry` had no caller until now), and registers the log-events metric that `main.rs`'s comment promised. This also lights up the health egress counters (`carbide_health_otlp_export_failures_total`, `carbide_health_report_submissions_total`).
- Drive-by: the DPA MQTT client stats `println!` is now a structured `tracing::debug!`.

Tests added!

This supports NVIDIA#3175

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet chet force-pushed the gh-issue-3175-heartbeat branch from aaf3c24 to 99bace2 Compare July 11, 2026 09:25
@chet

chet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@chet, on it — I'll redo the full review of this PR once more.

✅ Action performed

Full review finished.

@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

🧹 Nitpick comments (1)
crates/utils/src/managed_loop.rs (1)

114-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Route this table through check_cases instead of a hand-rolled loop.

This test's Case struct carries six fields per row — exactly the "several expected fields per row" scenario the style guide calls out for check_cases/check_values (as opposed to a manual for loop with ad-hoc assert!/assert_eq! calls). Using check_cases directly would keep per-case failure reporting consistent with the rest of the codebase's table-driven tests.

As per path instructions, "Use check_cases / check_values directly when a macro would obscure a table with several inputs or several expected fields per row in table-driven tests."

🤖 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 `@crates/utils/src/managed_loop.rs` around lines 114 - 186, Refactor
record_iteration_counts_every_outcome_and_logs_only_failures to pass its Case
table through the repository’s check_cases helper instead of manually iterating
and asserting. Preserve all existing log, metric, and per-scenario expectations
while expressing the six expected fields through the established check_cases
pattern.

Source: Path instructions

🤖 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 `@crates/health/src/discovery/mod.rs`:
- Around line 52-70: Update run_discovery_loop to accept a CancellationToken and
use it to stop the loop during shutdown. Make each iteration and its delay
cancellable, including selecting cancellation alongside iteration.run_once() and
tokio::time::sleep(delay), and thread the token from the caller that spawns this
managed background loop.

---

Nitpick comments:
In `@crates/utils/src/managed_loop.rs`:
- Around line 114-186: Refactor
record_iteration_counts_every_outcome_and_logs_only_failures to pass its Case
table through the repository’s check_cases helper instead of manually iterating
and asserting. Preserve all existing log, metric, and per-scenario expectations
while expressing the six expected fields through the established check_cases
pattern.
🪄 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: 8dd9bedf-886c-4674-bb00-5ae91e45279a

📥 Commits

Reviewing files that changed from the base of the PR and between 76357e2 and 99bace2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/api-core/src/dpa/handler.rs
  • crates/api-core/src/machine_update_manager/mod.rs
  • crates/api-core/src/machine_validation/mod.rs
  • crates/api-core/src/mqtt_state_change_hook/republisher.rs
  • crates/api-core/src/tests/machine_update_manager.rs
  • crates/health/src/discovery/mod.rs
  • crates/health/src/lib.rs
  • crates/health/src/main.rs
  • crates/utils/Cargo.toml
  • crates/utils/src/lib.rs
  • crates/utils/src/managed_loop.rs
  • docs/observability/core_metrics.md

Comment thread crates/health/src/discovery/mod.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant