Skip to content

perf(metrics-endpoint): encode the registry on a dedicated thread#3535

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

perf(metrics-endpoint): encode the registry on a dedicated thread#3535
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3524

Conversation

@chet

@chet chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

The shared /metrics handler ran registry.gather() and the text encode inline on the tokio task serving the connection, so a large scrape could stall a worker -- and every binary serves /metrics through this crate. Spawning each encode on the blocking pool would cap one encode's latency but not the aggregate CPU: N concurrent scrapes would run N encoders on N cores. Instead this hands the encode to one dedicated thread behind a bounded queue, capping encode CPU to a single core no matter how many scrapes arrive.

  • run_metrics_endpoint_with_listener spawns one metrics-encoder thread that owns the registry and prefix and serves encode requests off a small bounded channel; the /metrics handler sends a request and awaits the result.
  • Shutdown is prompt and connection-independent: a cancel-driven stop flag lets the thread exit within one poll interval even while idle keep-alive connections are open, and the server joins it. A full queue returns 503 (backpressure); a failed encode returns 500 carrying the Prometheus error, kept distinct from a dead-thread 500.
  • A panic in gather() (a misbehaving collector or callback) is caught, so it returns 500 for that one scrape and the encoder survives to serve the next -- the per-request isolation the inline/pool approach had.
  • encode_metrics now returns the Prometheus encode error instead of unwrapping it. The success-path bytes are unchanged, so /metrics output stays byte-identical.
  • A thread-creation failure at startup now propagates as an io::Error from the endpoint entry points instead of panicking; bmc-proxy (the one direct caller of run_metrics_endpoint_with_listener) logs that error rather than discarding the server's exit outcome.

This supports #3524

@chet chet requested a review from a team as a code owner July 15, 2026 05:47
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, sounds good — I'll redo the full review of all changes in this PR.

ʬʬ (`・ω・´)ゞ

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary by CodeRabbit

  • New Features

    • Improved /metrics reliability by processing scrapes through a dedicated encoder.
    • Added clear error responses when the encoder is busy, unavailable, or encounters a failure.
    • A collector panic now affects only the current scrape; subsequent requests continue normally.
  • Bug Fixes

    • Improved metrics server shutdown and connection handling.
    • Metrics endpoint failures are now logged for easier diagnosis.

Walkthrough

The metrics endpoint now queues /metrics scrapes for a dedicated encoder thread. It handles queue saturation, encoding failures, collector panics, abandoned replies, and coordinated shutdown while preserving successful response bytes.

Changes

Metrics endpoint request handling

Layer / File(s) Summary
Encoder thread lifecycle and contracts
crates/metrics-endpoint/src/lib.rs
A bounded queue and oneshot replies connect request handling to a dedicated encoder thread, with fallible encoding, per-scrape panic isolation, explicit stop signaling, and bounded shutdown joining.
Async scrape handling and validation
crates/metrics-endpoint/src/lib.rs
The handler maps queue, encoder, and reply failures to 503 or 500, returns encoded Prometheus bytes with headers, and tests response identity, shutdown, backpressure, and panic recovery.
Endpoint failure reporting
crates/bmc-proxy/src/metrics.rs
The spawned endpoint task logs an error when the metrics server exits unsuccessfully.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant service_fn
  participant handle_metrics_request
  participant encoder_thread
  Client->>service_fn: GET /metrics
  service_fn->>handle_metrics_request: await request
  handle_metrics_request->>encoder_thread: enqueue scrape and oneshot reply
  encoder_thread->>encoder_thread: gather and encode metrics
  encoder_thread-->>handle_metrics_request: encoded bytes or EncodeError
  handle_metrics_request-->>service_fn: HTTP 200, 503, or 500
  service_fn-->>Client: metrics response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: moving metrics encoding to a dedicated thread for performance.
Description check ✅ Passed The description is directly aligned with the changeset and accurately describes the dedicated encoder thread, shutdown, and error handling.
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.

@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/metrics-endpoint/src/lib.rs (1)

285-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add table-driven regression coverage for the new response contract.

Cover successful /metrics responses byte-for-byte, including status and headers, plus the blocking-task failure path returning HTTP 500 while the listener remains usable. As per coding guidelines, Rust tests for observable results should use table-driven cases. As per path instructions, prioritize behavioral and missing-test coverage.

🤖 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/metrics-endpoint/src/lib.rs` around lines 285 - 309, Add table-driven
Rust regression tests for handle_metrics_request covering successful GET
/metrics responses byte-for-byte, including status and headers, and the
spawn_blocking failure path returning HTTP 500. Ensure the failure test verifies
the listener or server remains usable afterward, reusing existing test helpers
and observable response assertions.

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.

Inline comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 285-309: Bound concurrent metrics encoding in
handle_metrics_request by adding a shared semaphore around the spawn_blocking
work, or reuse a shared rendered snapshot to avoid duplicate encoding. Ensure
each /metrics request acquires the shared limit before encoding and returns HTTP
503 or 429 immediately when saturated, while preserving the existing successful
response and encoding-error handling.

---

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 285-309: Add table-driven Rust regression tests for
handle_metrics_request covering successful GET /metrics responses byte-for-byte,
including status and headers, and the spawn_blocking failure path returning HTTP
500. Ensure the failure test verifies the listener or server remains usable
afterward, reusing existing test helpers and observable response assertions.
🪄 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: 8ba85c34-6036-4b7c-bbf7-a4b8207d73e8

📥 Commits

Reviewing files that changed from the base of the PR and between eb4ea22 and 611516f.

📒 Files selected for processing (1)
  • crates/metrics-endpoint/src/lib.rs

Comment thread crates/metrics-endpoint/src/lib.rs 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/metrics-endpoint/src/lib.rs (1)

285-309: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Propagate encoder errors instead of using panic as the error channel.

encode_metrics still calls unwrap() at Line 280, so an encoding failure is converted into a JoinError and the original Prometheus error is lost. Return a Result from encode_metrics and handle Ok(Err(error)) separately from task panics or cancellation.

As per coding guidelines, library code should use error propagation/conversions rather than panic-based handling.

🤖 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/metrics-endpoint/src/lib.rs` around lines 285 - 309, The metrics
encoding flow should preserve encoder errors instead of panicking and hiding
them inside JoinError. Update encode_metrics to return a Result and propagate
the underlying Prometheus encoding error, then adjust handle_metrics_request to
handle Ok(Err(error)) separately from task panics or cancellation while
retaining distinct logging and HTTP failure handling.

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.

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 285-309: The metrics encoding flow should preserve encoder errors
instead of panicking and hiding them inside JoinError. Update encode_metrics to
return a Result and propagate the underlying Prometheus encoding error, then
adjust handle_metrics_request to handle Ok(Err(error)) separately from task
panics or cancellation while retaining distinct logging and HTTP failure
handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ad9f7143-77fb-4471-ab08-e22f294b4baa

📥 Commits

Reviewing files that changed from the base of the PR and between eb4ea22 and 611516f.

📒 Files selected for processing (1)
  • crates/metrics-endpoint/src/lib.rs

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

If encoding is an issue then I would suggest to spawn ONE regular thread that listens bounded mpsc queue, encodes things and send result back in async world. In this case we will cap CPU consumption of encoding to 1 core.

@github-actions

github-actions Bot commented Jul 15, 2026

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 259 15 31 78 7 128
machine-validation-runner 532 38 115 183 15 181
machine_validation 532 38 115 183 15 181
machine_validation-aarch64 532 38 115 183 15 181
nvmetal-carbide 532 39 113 184 15 181
TOTAL 2393 168 489 817 67 852

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

@chet chet changed the title perf(metrics-endpoint): encode the registry off the async worker perf(metrics-endpoint): encode the registry on a dedicated thread Jul 15, 2026
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@poroh thanks -- reworked exactly to your suggestion. There's now one dedicated metrics-encoder thread reading a bounded queue and replying to the async side, so encode CPU is capped to a single core no matter how many scrapes land (the previous spawn_blocking used the blocking pool, so N concurrent scrapes could run N encoders). The thread exits promptly on shutdown independent of open keep-alive connections, and a panic in gather() is caught so it can't take the endpoint down. PTAL, thanks!

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai -- encode_metrics now returns the Prometheus encode error instead of panicking, and the handler maps it to a 500 distinct from a task/thread failure (and a caught encode panic to its own 500), so encoder errors are no longer hidden. Addressed as part of the encoder-thread rework.

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, understood — I'll redo the full review of all changes in this PR, taking into account the encoder-thread rework (dedicated single-core metrics-encoder thread with bounded queue, graceful shutdown, panic-catching around gather(), and distinct 500 responses for encode errors vs. task/thread failures).

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@chet, thanks for the update. Separating the encode-failure path from the task/thread-failure path — and returning the underlying Prometheus error rather than relying on a panic — is the correct approach here: it keeps the failure mode observable (proper error propagated to the 500 response/logs) without conflating "the blocking task itself failed to run" with "the encoder ran but the registry produced an invalid state." Catching the panic separately as a last line of defense is a reasonable belt-and-suspenders measure, provided it doesn't mask the underlying error class in logs.

This resolves the earlier concern about hidden encoder errors. Good work on the encoder-thread rework overall — bounding the CPU cost to a single core and ensuring clean shutdown independent of keep-alive connections are the right characteristics for a metrics endpoint under load.

@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: 4

🤖 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/metrics-endpoint/src/lib.rs`:
- Around line 250-254: Update spawn_encoder_thread to return io::Result instead
of calling expect on thread creation, and propagate the result through both
metrics endpoint entry points covering the referenced startup paths. Preserve
the existing encoder channel/thread setup on success, while converting detached
endpoint startup failures into logged errors rather than task panics.
- Around line 843-855: Update the keep-alive setup in the test around the held
TcpStream to send an HTTP /health request and read its response fully before
calling cancel_token.cancel(). Preserve the connection until after server
shutdown so the accepted connection task retains its encoder_tx clone and the
test deterministically verifies the shutdown behavior.
- Around line 446-451: The metrics endpoint’s 500 responses must distinguish
encoding errors, caught panics, and encoder task/thread failures instead of
sharing “Failed to encode metrics.” Update the response construction in the
encoder error and panic branches, including the TrySendError::Disconnected path
near the visible match, to use stable, distinct bodies for each failure class.
Add table-driven tests covering each class and asserting its mapped status/body
response.
- Around line 368-370: Update the reply transmission in the shown
requester-disconnect handling block to call `.ok()` on the result of
`reply_tx.send(result)`, explicitly discarding the expected send failure while
preserving the existing behavior.
🪄 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: 4a48348c-d1ec-41a6-8519-6a56520e5260

📥 Commits

Reviewing files that changed from the base of the PR and between 611516f and d7237e8.

📒 Files selected for processing (1)
  • crates/metrics-endpoint/src/lib.rs

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
Comment thread crates/metrics-endpoint/src/lib.rs Outdated
Comment on lines +446 to +451
Err(TrySendError::Disconnected(_)) => {
tracing::error!("metrics encoder thread is gone; cannot encode metrics");
return Ok(Response::builder()
.status(500)
.body(Full::new(Bytes::from("Failed to encode metrics")))
.unwrap());

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return distinct responses for each 500 failure class.

Encoding errors, caught panics, and encoder task/thread failures all currently return "Failed to encode metrics", contrary to the PR objective. Give each class a stable, distinct body and add table-driven assertions for the mapping.

As per coding guidelines, use table-driven tests for functions mapping inputs to errors or observable results.

Also applies to: 462-483

🤖 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/metrics-endpoint/src/lib.rs` around lines 446 - 451, The metrics
endpoint’s 500 responses must distinguish encoding errors, caught panics, and
encoder task/thread failures instead of sharing “Failed to encode metrics.”
Update the response construction in the encoder error and panic branches,
including the TrySendError::Disconnected path near the visible match, to use
stable, distinct bodies for each failure class. Add table-driven tests covering
each class and asserting its mapped status/body response.

Source: Coding guidelines

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai -- all three addressed:

  • spawn_encoder_thread and the endpoint entry points now return io::Result, so a thread-creation failure at startup is a logged error, not a panic. Its one direct caller, bmc-proxy, now handles that Result (and no longer discards the server's exit outcome).
  • The live-connection shutdown test now completes a real GET /health round-trip before cancelling, so it deterministically exercises the "thread exits despite a live connection" path.
  • The 500s already distinguish encode error / caught panic / dropped-reply via the EncodeError enum and distinct logs; they stay status 500 (each is a server-side failure with no client-actionable difference), which is the intent.

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, understood — I'll redo the full review of all changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "--- spawn_encoder_thread signature ---"
rg -n 'fn spawn_encoder_thread' -A5 crates/metrics-endpoint/src/lib.rs || true
echo "--- entry point signatures ---"
rg -n 'pub (async )?fn (run|serve|start)' -A3 crates/metrics-endpoint/src/lib.rs || true
echo "--- EncodeError enum ---"
rg -n 'enum EncodeError' -A10 crates/metrics-endpoint/src/lib.rs || true
echo "--- bmc-proxy caller ---"
rg -n 'metrics_endpoint|spawn_encoder_thread|metrics-endpoint' -g '*bmc-proxy*' -A3 -B3 -r . 2>/dev/null || true
fd -t f . -x echo {} | rg -i 'bmc-proxy' | head -20
echo "--- shutdown test with keep-alive ---"
rg -n 'GET /health|keep-alive|keep_alive' crates/metrics-endpoint/src/lib.rs -A5 -B5 || true

Length of output: 2610


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "--- find bmc-proxy dir ---"
fd -t d bmc-proxy
echo "--- bmc-proxy usage of metrics endpoint ---"
rg -n 'metrics_endpoint|run_metrics_endpoint' -C4 --glob '*bmc-proxy*' . || true
echo "--- shutdown test ---"
rg -n 'fn.*shutdown|GET /health|keep_alive|keep-alive' crates/metrics-endpoint/src/lib.rs

Length of output: 1544


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'run_metrics_endpoint' -C5 crates/bmc-proxy/src/**/*.rs 2>/dev/null || rg -n 'run_metrics_endpoint' -C5 -r crates/bmc-proxy

Length of output: 467


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'run_metrics_endpoint' crates/bmc-proxy -C5

Length of output: 1039


@chet, thanks for the detailed follow-up — I verified each point against the current code:

  1. Startup error handling: spawn_encoder_thread now returns std::io::Result<(SyncSender<EncodeReply>, JoinHandle<()>)>, and run_metrics_endpoint/run_metrics_endpoint_with_cancellation/run_metrics_endpoint_with_listener all propagate Result<(), std::io::Error>. In crates/bmc-proxy/src/metrics.rs, the spawned task now does if let Err(e) = metrics_endpoint::run_metrics_endpoint_with_listener(...), so a thread-spawn failure is logged rather than panicking, and the exit outcome is no longer silently discarded.
  2. Shutdown test fidelity: the live-connection test now completes a real GET /health round-trip (held.write_all(b"GET /health HTTP/1.1\r\n...")) before cancelling, which properly exercises the "encoder thread exits despite an open keep-alive connection" path rather than relying on a socket that was never actually used.
  3. Error taxonomy: EncodeError::Encode(prometheus::Error) and EncodeError::Panicked remain distinct from the dropped-reply/task-failure case, each logged separately while returning 500 — consistent with "same client-facing status, different server-side diagnosis," which is the right call for an internal metrics endpoint.

All three points check out. This looks solid — nice work tightening up the failure modes around thread startup and shutdown.

ヽ(=^・ω・^=)丹

@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/metrics-endpoint/src/lib.rs (1)

443-489: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Count scrape failures instead of only logging them. At crates/metrics-endpoint/src/lib.rs:443-489, the busy, disconnected, encode-error, panic, and dropped-reply paths are bounded operational outcomes; emit a single carbide_instrument::Event such as carbide_metrics_scrape_failures_total with an enum outcome label.

🤖 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/metrics-endpoint/src/lib.rs` around lines 443 - 489, The metrics
scrape failure branches in the request handling flow must emit a
carbide_instrument::Event named carbide_metrics_scrape_failures_total instead of
only logging. Add one event per failure outcome, labeled distinctly for the
TrySendError::Full, TrySendError::Disconnected, EncodeError::Encode,
EncodeError::Panicked, and reply_rx.await error paths, while preserving their
existing responses and logs.

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/metrics-endpoint/src/lib.rs`:
- Around line 292-304: Update the connection-serving flow around
serve_connection to track each spawned task in a JoinSet instead of detaching
it. During shutdown, cancel the active connection tasks, await their completion,
and only then return from the endpoint after stopping and joining the encoder;
extend the relevant test to assert that the held connection is closed.

---

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 443-489: The metrics scrape failure branches in the request
handling flow must emit a carbide_instrument::Event named
carbide_metrics_scrape_failures_total instead of only logging. Add one event per
failure outcome, labeled distinctly for the TrySendError::Full,
TrySendError::Disconnected, EncodeError::Encode, EncodeError::Panicked, and
reply_rx.await error paths, while preserving their existing responses and logs.
🪄 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: ca8c028e-bedc-4fd1-b22b-8b9c53c8b1f2

📥 Commits

Reviewing files that changed from the base of the PR and between eb4ea22 and f0e5ac2.

📒 Files selected for processing (2)
  • crates/bmc-proxy/src/metrics.rs
  • crates/metrics-endpoint/src/lib.rs

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai -- two dispositions here:

  • Track connections in a JoinSet and drain them on shutdown: declining on this PR. The specific concern that motivated the encoder rework -- the encoder thread lingering because connections held its sender -- is already resolved: the thread now exits on a cancel-driven stop flag independent of connection refcounts, and the server joins it. Gracefully draining the in-flight /metrics connections themselves is a separate, deliberate call tracked in Gracefully drain in-flight /metrics connections on metrics-endpoint shutdown #3496 (for /metrics, snapping the endpoint shut with the server is a truthful representation of its state, and scrapes are sub-second and retried each interval).

  • Count scrape failures as a metric: good idea, but out of scope for this perf-focused PR (it moves the encode off the async worker; a new metric family + catalogue row would be scope creep), and the failures are already logged. Filed as metrics-endpoint: count /metrics scrape failures by outcome #3546 to add carbide_metrics_scrape_failures_total with a bounded outcome label.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@chet, thanks for closing the loop on both — both dispositions are reasonable and I have no objection.

  • JoinSet draining decline: Agreed. The original concern (encoder thread lingering because connections held a sender clone) is architecturally resolved by the cancel-driven should_stop flag, which is independent of connection refcounts. Deferring the broader question of gracefully draining in-flight /metrics connections to #3496 as a deliberate design call is sound — for a sub-second, retried-every-interval scrape endpoint, snapping the listener shut on server shutdown is a defensible and simple semantics, and conflating it with this perf-focused PR would add unrelated scope.
  • Scrape-failure metric deferral: Agreed this is out of scope here — adding a new metric family plus catalogue entry is a distinct piece of work, and the failures remain visible via logs in the interim. #3546 tracking carbide_metrics_scrape_failures_total with a bounded outcome label is the right place for it.

Nice work landing the encoder-thread rework — this PR is in good shape.

(=^・ω・^=) ノ


✏️ Learnings added
Learnt from: chet
URL: https://github.com/NVIDIA/infra-controller/pull/3535

Timestamp: 2026-07-15T08:07:30.409Z
Learning: In crates/metrics-endpoint/src/lib.rs, the `/metrics` endpoint intentionally does not gracefully drain in-flight connections on server shutdown; the accept loop cancellation flips a shared `should_stop` flag that the dedicated metrics-encoder thread checks to exit promptly, independent of open keep-alive connection refcounts. Graceful draining of in-flight `/metrics` connections themselves is a deliberate, separate design decision tracked in NVIDIA/infra-controller#3496, not a bug — scrapes are sub-second and retried each interval, so snapping the endpoint shut with the server is considered an acceptable truthful representation of server state.

Learnt from: chet
URL: https://github.com/NVIDIA/infra-controller/pull/3535

Timestamp: 2026-07-15T08:07:30.409Z
Learning: NVIDIA/infra-controller#3546 tracks adding a `carbide_metrics_scrape_failures_total` metric (with a bounded outcome label) for the `/metrics` endpoint in crates/metrics-endpoint/src/lib.rs; this was deliberately deferred out of the encoder-thread rework PR (`#3535`) as scope creep for a perf-focused change, since scrape failures are already logged.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
/// scrapes arrive at once. This small queue lets a couple of scrapes wait behind an
/// in-flight encode; a scrape that arrives with the queue already full is shed with
/// `503` rather than allowed to pile up.
const ENCODER_QUEUE_CAPACITY: usize = 4;

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.

I would increase it to some bigger number. Something that represent of 0.5 second of work on modern core otherwise it can be annoyingly flaky.

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
if should_stop.load(Ordering::SeqCst) {
break;
}
match encoder_rx.recv_timeout(ENCODER_RECV_TIMEOUT) {

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.

You can consider channel with type enum Item { Work(reply_tx), Stop } to avoid polling.

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

Couple improvements possible but both not critical

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@poroh thanks -- both adopted:

  • Queue depth: bumped ENCODER_QUEUE_CAPACITY 4 → 64, so a burst of concurrent scrapes waits behind the in-flight encode instead of being shed with 503; only a sustained overload that fills all 64 slots sheds the excess.
  • No more polling: the channel now carries enum Item { Work(reply_tx), Stop } and the encoder thread blocks on a plain recv(). On shutdown the server enqueues Item::Stop (via a retained sender clone) and joins -- so the thread exits on a message, not a timeout poll. The recv_timeout + AtomicBool stop-flag are gone. Shutdown stays connection-independent, and the test suite (incl. the live-connection shutdown case) is green. Thanks for the review!

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, understood — I'll redo the full review of all changes in this PR, taking into account the latest updates (queue capacity increase from 4 to 64, and the switch from recv_timeout/AtomicBool polling to a message-based Item::Work/Item::Stop shutdown via blocking recv()).

✅ 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: 2

🤖 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/metrics-endpoint/src/lib.rs`:
- Around line 800-803: Update the lifecycle test’s reply receive around
reply_rx.blocking_recv() to use the existing thread-exit deadline, polling until
a reply arrives or the deadline expires. Preserve the current encoder-reply and
encode-success assertions while ensuring an encoder regression cannot hang the
test indefinitely.
- Around line 299-303: The shutdown path around the encoder thread join must
enforce a bounded timeout, since send(Item::Stop) cannot be processed while
encode_metrics() is blocked in gather(). Update the tokio::task::spawn_blocking
flow to apply the established scrape/shutdown timeout mechanism or otherwise
bound the wait for encoder_thread.join(), while preserving normal completion and
panic isolation behavior.
🪄 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: 012e96c1-fbed-4cac-af61-c5e0c22e59de

📥 Commits

Reviewing files that changed from the base of the PR and between 8832d65 and 81a78d3.

📒 Files selected for processing (2)
  • crates/bmc-proxy/src/metrics.rs
  • crates/metrics-endpoint/src/lib.rs

Comment thread crates/metrics-endpoint/src/lib.rs Outdated
Comment thread crates/metrics-endpoint/src/lib.rs Outdated
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai -- both addressed:

  • The encoder lifecycle test now waits for its reply with a bounded deadline (polling try_recv) instead of a bare blocking_recv, so an encoder regression fails the test rather than hanging it.
  • Shutdown now caps the encoder-thread join with a 5s tokio::time::timeout: a clean join is silent, a panic/JoinError logs an error, and a timeout (a scrape likely mid-encode, which Item::Stop can't preempt and catch_unwind can't catch if it's a hang) logs a warning and returns rather than blocking shutdown -- the detached thread finishes its encode and exits on the queued Stop shortly after.

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, understood — I'll redo the full review of all changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "--- try_recv polling in test ---"
rg -n 'try_recv|blocking_recv' crates/metrics-endpoint/src/lib.rs -B3 -A3
echo "--- shutdown join with timeout ---"
rg -n 'timeout|join' crates/metrics-endpoint/src/lib.rs -B3 -A3 | head -80

Length of output: 5106


Both points check out against the current code, @chet:

  • The encoder lifecycle test now polls reply_rx.try_recv() against a bounded 5s deadline (Lines 813-819) instead of a bare blocking_recv, so a hung encoder fails the test rather than hanging it.
  • Shutdown does spawn_blocking to send Item::Stop and join() the encoder thread (Lines 308-312), wrapped in tokio::time::timeout(ENCODER_SHUTDOWN_JOIN_TIMEOUT, join_task) (Line 313): a clean join is silent, a panic/JoinError logs tracing::error!, and a timeout logs tracing::warn! and returns without blocking shutdown — matching the description that the detached thread will still exit once its in-flight encode finishes and it observes the queued Stop.

Both changes are correctly implemented and consistent with the connection-independent shutdown design. Nice work.

(=^・ω・^=)ノ

@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/metrics-endpoint/src/lib.rs (1)

747-1046: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage of success/panic paths; consider adding the queue-full and disconnected-encoder branches.

TrySendError::Full (503) and TrySendError::Disconnected (500) in handle_metrics_request aren't exercised by any test. Both are unit-testable directly against MetricsHandlerState — build a sync_channel and either fill it without draining, or drop the receiver — without needing a real server or thread.

As per coding guidelines: "Use table-driven tests for functions mapping inputs to outputs, errors, or observable results."

🤖 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/metrics-endpoint/src/lib.rs` around lines 747 - 1046, The test suite
lacks coverage for the queue-full and disconnected-encoder branches in
handle_metrics_request. Add a table-driven unit test that constructs
MetricsHandlerState with a sync_channel, fills the queue to trigger
TrySendError::Full and drops the receiver to trigger TrySendError::Disconnected,
then verifies the handler returns 503 and 500 respectively without requiring a
live server or encoder thread.

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/metrics-endpoint/src/lib.rs`:
- Around line 308-312: Replace the `let _ =` Result-discard patterns in the
encoder-thread shutdown and reply paths with `.ok()`: update
`shutdown_tx.send(Item::Stop)` inside the `spawn_blocking` closure and
`reply_tx.send(result)` near the reply handling. Preserve the existing behavior
of intentionally ignoring these expected send failures.

---

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 747-1046: The test suite lacks coverage for the queue-full and
disconnected-encoder branches in handle_metrics_request. Add a table-driven unit
test that constructs MetricsHandlerState with a sync_channel, fills the queue to
trigger TrySendError::Full and drops the receiver to trigger
TrySendError::Disconnected, then verifies the handler returns 503 and 500
respectively without requiring a live server or encoder thread.
🪄 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: bbf76dd0-c5cf-436a-9108-b2626ab96713

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc686a and 3015269.

📒 Files selected for processing (2)
  • crates/bmc-proxy/src/metrics.rs
  • crates/metrics-endpoint/src/lib.rs

Comment thread crates/metrics-endpoint/src/lib.rs
The shared `/metrics` handler ran `registry.gather()` and the text encode inline on the tokio task serving the connection, so a large scrape could stall a worker -- and every binary serves `/metrics` through this crate. Spawning each encode on the blocking pool would cap one encode's latency but not the aggregate CPU: N concurrent scrapes would run N encoders on N cores. Instead this hands the encode to one dedicated thread behind a bounded queue, capping encode CPU to a single core no matter how many scrapes arrive.

- `run_metrics_endpoint_with_listener` spawns one `metrics-encoder` thread that owns the registry and prefix and serves encode requests off a small bounded channel; the `/metrics` handler sends a request and awaits the result. The thread exits when the server shuts down and the sender drops.
- A full queue returns 503 (backpressure); a failed encode returns 500 carrying the Prometheus error, kept distinct from a dead-thread 500.
- `encode_metrics` now returns the Prometheus encode error instead of unwrapping it. The success-path bytes are unchanged, so `/metrics` output stays byte-identical.

This supports NVIDIA#3524
@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai -- both done: the two intentional fire-and-forget send discards now use .ok(), and a new table-driven unit test (test_handle_metrics_request_queue_backpressure) covers the queue-full → 503 and disconnected-encoder → 500 branches directly, with no live server or encoder thread.

@chet

chet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@chet, understood — I'll redo the full review of all changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "--- .ok() usages on send ---"
rg -n '\.send\(.*\)\.ok\(\)' crates/metrics-endpoint/src/lib.rs -B3 -A1
echo "--- backpressure test ---"
rg -n 'test_handle_metrics_request_queue_backpressure' -A40 crates/metrics-endpoint/src/lib.rs

Length of output: 2725


Both changes check out, @chet:

  • The two fire-and-forget sends (Line 310 for Item::Stop during shutdown, Line 385 for the reply in the encoder loop) now use .ok() with clear comments explaining why the error is intentionally discarded.
  • test_handle_metrics_request_queue_backpressure (Line 1052) is a clean, table-driven test that exercises the two backpressure branches directly against MetricsHandlerState/handle_metrics_request — priming a capacity-1 sync_channel to force Full for the 503 case, and dropping the receiver to force a disconnected send for the 500 case — with no live server or encoder thread involved.

Nice, minimal way to pin down both edge cases. This PR is in good shape.

(=^・ω・^=)ノ

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

♻️ Duplicate comments (1)
crates/metrics-endpoint/src/lib.rs (1)

308-323: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The five-second timeout does not bound the blocking shutdown task.

Once spawn_blocking starts, timing out its JoinHandle only detaches it. If send(Item::Stop) blocks on a full queue or encoder_thread.join() waits on a hung collector, that blocking task remains alive and can still stall Tokio runtime shutdown. Use a shutdown mechanism that does not leave an unbounded blocking task—for example, bounded stop delivery plus is_finished() polling, joining only after completion—and test a permanently blocked encoder.

#!/bin/bash
set -euo pipefail

rg -n -A3 -B3 'name = "tokio"|spawn_blocking|ENCODER_SHUTDOWN_JOIN_TIMEOUT|shutdown_tx\.send|encoder_thread\.join' \
  Cargo.lock crates/metrics-endpoint/src/lib.rs

As per coding guidelines, “Do not spawn background tasks without joining them.” As per path instructions, prioritize resource lifetimes and joined/cancellable background tasks.

🤖 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/metrics-endpoint/src/lib.rs` around lines 308 - 323, The shutdown path
around the spawn_blocking task must not leave an unbounded blocking task after
the timeout. Replace the blocking stop-send and unconditional
encoder_thread.join flow with bounded stop delivery and completion polling via
is_finished(), only awaiting or joining once completion is confirmed; preserve
the timeout behavior while ensuring all spawned tasks are joined before runtime
shutdown. Add coverage for a permanently blocked encoder to verify shutdown
returns without retaining an unjoined blocking task.

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.

Duplicate comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 308-323: The shutdown path around the spawn_blocking task must not
leave an unbounded blocking task after the timeout. Replace the blocking
stop-send and unconditional encoder_thread.join flow with bounded stop delivery
and completion polling via is_finished(), only awaiting or joining once
completion is confirmed; preserve the timeout behavior while ensuring all
spawned tasks are joined before runtime shutdown. Add coverage for a permanently
blocked encoder to verify shutdown returns without retaining an unjoined
blocking task.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 49d23c0f-af7b-4148-8807-bf550e279acd

📥 Commits

Reviewing files that changed from the base of the PR and between 3abd252 and cea4e54.

📒 Files selected for processing (2)
  • crates/bmc-proxy/src/metrics.rs
  • crates/metrics-endpoint/src/lib.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.

2 participants