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

Summary by CodeRabbit

  • New Features

    • Improved /metrics request handling with accurate response sizes and Prometheus-compatible content types.
    • Added clear error responses when metrics encoding is unavailable or fails.
  • Bug Fixes

    • Metrics collection now continues after an individual collector panic.
    • Improved server shutdown behavior, including active keep-alive connections.
    • Metrics endpoint failures are now logged for easier diagnosis.

Walkthrough

The metrics endpoint now queues /metrics scrapes for a dedicated encoder thread. It explicitly handles queue saturation, encoding failures, collector panics, abandoned replies, and 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
Handler state uses a bounded queue and oneshot replies; startup, fallible encoding, panic isolation, and coordinated shutdown are implemented for the dedicated encoder thread.
Async scrape handling and validation
crates/metrics-endpoint/src/lib.rs
The async handler queues scrape requests, returns 503 or 500 responses as appropriate, preserves Prometheus headers and bytes, and tests shutdown 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 issues

  • NVIDIA/infra-controller issue 3524 — Addresses moving Prometheus gathering and encoding to a dedicated thread.

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 oneshot reply channel
  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 main change: moving metrics encoding to a dedicated thread for performance.
Description check ✅ Passed The description directly matches the changeset and explains the dedicated encoder thread, shutdown behavior, 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 809 48 238 290 36 197
machine_validation 809 48 238 290 36 197
machine_validation-aarch64 809 48 238 290 36 197
nvmetal-carbide 809 48 238 290 36 197
TOTAL 3501 207 983 1244 151 916

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
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
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 -- 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 on lines +292 to +304
// The accept loop only returns once the token is cancelled. Signal the encoder
// thread to stop and join it so shutdown is clean. The thread exits within one
// `ENCODER_RECV_TIMEOUT` (plus any encode already in progress), independent of
// connection tasks that may still hold a queue sender. Join on a blocking thread so
// this brief wait does not stall an async worker.
should_stop.store(true, Ordering::SeqCst);
match tokio::task::spawn_blocking(move || encoder_thread.join()).await {
Ok(Ok(())) => {}
Ok(Err(_panic)) => tracing::error!("metrics encoder thread panicked"),
Err(err) => tracing::error!(error = %err, "failed to join metrics encoder thread"),
}

Ok(())

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Cancel and join active connection tasks before returning.

Cancellation stops the accept loop and encoder, but detached serve_connection tasks can continue serving open keep-alive sockets after this function returns. Track them in a JoinSet, terminate them during shutdown, and extend the test to assert that held is closed.

As per coding guidelines, “Do not spawn background tasks without joining them”; as per path instructions, prefer joined and cancellable background tasks.

Also applies to: 890-900

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

Sources: Coding guidelines, Path instructions

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

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