Skip to content

feat: time firmware downloads and split their failures by cause#3407

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

feat: time firmware downloads and split their failures by cause#3407
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3174-downloader

Conversation

@chet

@chet chet commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Firmware artifact downloads are the api process's longest-running background HTTP work, and until now their only trace was log lines. carbide_firmware_download_duration_seconds{outcome} times every attempt; its _count series split by outcome is the fetch/status/checksum failure-counter set in one metric.

  • outcome names what actually broke: fetch (no response), status (non-2xx), transfer (mid-stream break), checksum (SHA-256 mismatch), io (staging/rename) -- the download path now tags its errors with the cause instead of flattening everything into one report.
  • The event owns the four historical log lines at their levels, with the full error chain as a structured field. One line per attempt now: a failed-verify attempt used to log completion INFO then checksum ERROR; it logs only the ERROR.
  • An ok attempt's duration spans fetch, verification, and publish (the description says so), so each outcome series reads consistently.
  • The no-URL config path stays log-only (it re-fires every poll without a download running).
  • No catalogue row: the integration environment never exercises a download, and the scrape-derived catalogue lists only what the regen mints -- same treatment as the machine-update Failed-state histogram.

Tests added!

This supports #3174

@chet chet requested a review from a team as a code owner July 12, 2026 01:44
@chet

chet commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@chet Sounds good — I'll redo the full review of the changes now.

(=^・ω・^=)

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added structured telemetry for background firmware downloads, including categorized outcomes, elapsed durations, and source URLs with query parameters removed.
    • Completion events now emit richer success/failure details (filename and error message).
  • Bug Fixes
    • Improved download verification and publishing behavior so invalid firmware is not published and failure artifacts are cleaned up reliably.
  • Tests
    • Updated downloader tests to assert outcomes and durations deterministically via metrics/log capture, and added coverage for failure cases and URL sanitization.
  • Chores
    • Updated firmware crate dependencies to support the new test instrumentation.

Walkthrough

Firmware downloads now classify failures, emit structured completion events with duration metrics, clean up staging files, and use deterministic tests for outcome reporting, labels, logging, and timing.

Changes

Firmware download reporting

Layer / File(s) Summary
Download reporting contract
crates/firmware/Cargo.toml, crates/firmware/src/downloader.rs
Adds instrumentation dependencies, DownloadOutcome, DownloadFinished, URL sanitization, and structured download errors.
Categorized download flow
crates/firmware/src/downloader.rs
Classifies setup, fetch, status, transfer, checksum, and publication failures while emitting completion events, measuring duration, and removing failed staging files.
Download reporting validation
crates/firmware/src/tests/downloader.rs
Adds asynchronous metric polling and verifies outcomes, snake_case labels, URL sanitization, completion logs, error context, and duration histograms.

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

Sequence Diagram(s)

sequenceDiagram
  participant FirmwareDownloader
  participant download_and_publish
  participant DownloadFinished
  FirmwareDownloader->>download_and_publish: download, verify, and publish firmware
  download_and_publish-->>FirmwareDownloader: categorized outcome and error
  FirmwareDownloader->>DownloadFinished: emit completion event with duration and context
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: timing firmware downloads and classifying failures by cause.
Description check ✅ Passed The description is directly related to the PR and describes the same metrics, logging, and test changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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: 3

🤖 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/firmware/src/downloader.rs`:
- Around line 196-198: Update the cleanup call in the result error branch to use
the filesystem operation’s `.ok()` form instead of assigning its result to `_`,
while preserving the existing behavior of removing dst_filename and ignoring
cleanup failures.
- Around line 306-309: Update the File::create call in the downloader flow to
pass dst_filename directly instead of borrowing it again. Preserve the existing
error wrapping and DownloadOutcome::Io mapping.
- Around line 66-78: Update DownloadFinished construction and event emission to
sanitize FirmwareFileArtifact.url before assigning it to the #[context] url
field. Ensure query parameters and other embedded credentials or tokens are
removed while preserving the usable URL location, and apply the same redacted
value on both success and failure paths.
🪄 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: a874f290-4e45-4103-8ac9-10b82d1c79ba

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5316b and dcadefe.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/firmware/Cargo.toml
  • crates/firmware/src/downloader.rs
  • crates/firmware/src/tests/downloader.rs

Comment thread crates/firmware/src/downloader.rs
Comment thread crates/firmware/src/downloader.rs
Comment thread crates/firmware/src/downloader.rs Outdated
@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.

@chet chet force-pushed the gh-issue-3174-downloader branch from dcadefe to 1af83db Compare July 12, 2026 03:21
@chet

chet commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@chet Sounds good — I'll redo the full review of the changes now.

(=^・ω・^=)

✅ 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/firmware/src/tests/downloader.rs (1)

272-360: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Status/Transfer/Io outcomes are only verified via hand-built events, not the real download() path.

download_finished_records_duration_and_owns_the_completion_line constructs DownloadFinished events directly rather than driving them through download()/download_and_publish(). Combined with the e2e tests, only Ok, Checksum, and Fetch are exercised through the real code path — the HTTP-status, stream-transfer, and local-IO branches in download() are covered only by the enum/label mapping, not by actual failing transfers. A small mock HTTP server (e.g. a bound TcpListener returning a fixed 404, or a body that closes mid-stream) would close this gap, though it's a moderate lift for a codebase where the branch logic itself is simple match-driven classification.

🤖 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/firmware/src/tests/downloader.rs` around lines 272 - 360, Extend the
real download-path tests around download() or download_and_publish() to trigger
HTTP status, stream-transfer, and local-IO failures, rather than validating
Status, Transfer, and Io only through hand-built DownloadFinished events. Use a
small controlled mock HTTP server or equivalent fixtures to produce a 404,
mid-stream disconnect, and local write failure, then assert each path emits the
expected outcome, duration metric, and completion log.
🤖 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/firmware/src/downloader.rs`:
- Around line 226-250: Update download and download_and_publish so every
diagnostic that includes the URL uses loggable_url(url) instead of the raw URL
before being wrapped or returned in DownloadFinished.error. Add a regression
test covering a failed download with a query-string URL and verify the raw query
is absent while the sanitized URL remains.

---

Nitpick comments:
In `@crates/firmware/src/tests/downloader.rs`:
- Around line 272-360: Extend the real download-path tests around download() or
download_and_publish() to trigger HTTP status, stream-transfer, and local-IO
failures, rather than validating Status, Transfer, and Io only through
hand-built DownloadFinished events. Use a small controlled mock HTTP server or
equivalent fixtures to produce a 404, mid-stream disconnect, and local write
failure, then assert each path emits the expected outcome, duration metric, and
completion log.
🪄 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: c0919bc7-794e-4be0-bd7a-9c29b105e7d9

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5316b and 1af83db.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/firmware/Cargo.toml
  • crates/firmware/src/downloader.rs
  • crates/firmware/src/tests/downloader.rs

Comment thread crates/firmware/src/downloader.rs
Firmware artifact downloads are the api process's longest-running background HTTP work -- hundreds of megabytes per artifact -- and until now their only trace was log lines. `carbide_firmware_download_duration_seconds{outcome}` times every attempt, and its `_count` series split by outcome is the fetch/status/checksum failure-counter set in one metric.

- `outcome` distinguishes what actually broke: `fetch` (the request never got a response), `status` (non-2xx), `transfer` (the body broke mid-stream), `checksum` (SHA-256 mismatch), `io` (staging file, segment write, rename-into-place) -- the download path is retyped to tag its errors with the cause instead of flattening everything into one report.
- The event owns the four historical lines at their levels (the completion INFO and the failure/checksum/rename ERRORs), with the full error chain as a structured field -- richer than the old lines, which printed only the outermost message. One line per attempt now: a failed-verify attempt used to log the completion INFO and then the checksum ERROR; it logs only the ERROR. The spawned task has a single emit-and-cleanup site: `download_and_publish` chains download, verification, and rename.
- The no-URL config-error path stays log-only: it re-fires every availability poll without running a download, so counting it would flood zero-duration samples.
- No catalogue row: the integration environment never exercises a download (empty firmware directory, mocks already at their desired versions), and the scrape-derived catalogue lists only what the regen mints -- the same treatment the machine-update Failed-state histogram got.

Tests added!

This supports NVIDIA#3174

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet chet force-pushed the gh-issue-3174-downloader branch from 1af83db to 5e23261 Compare July 12, 2026 04:36
@chet

chet commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — kicking off another full review of the PR now.

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

🧹 Nitpick comments (3)
crates/firmware/src/downloader.rs (2)

198-224: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the filename_string clone by reordering.

clear_download_state only needs &filename_string, but it runs after filename_string.clone() is moved into the event. Swapping the order lets emit take ownership of the original string, dropping the clone.

♻️ Proposed reorder
-            emit(DownloadFinished {
-                outcome,
-                took: started.elapsed(),
-                url: loggable_url(&url),
-                filename: filename_string.clone(),
-                error,
-            });
-            actual
-                .lock()
-                .unwrap()
-                .clear_download_state(&filename_string);
+            actual
+                .lock()
+                .unwrap()
+                .clear_download_state(&filename_string);
+            emit(DownloadFinished {
+                outcome,
+                took: started.elapsed(),
+                url: loggable_url(&url),
+                filename: filename_string,
+                error,
+            });
As per coding guidelines: "Struct initialization ordering: Sometimes just moving the order of parameters to a struct literal can avoid a clone."
🤖 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/firmware/src/downloader.rs` around lines 198 - 224, Reorder the
cleanup and event construction in the download task so
clear_download_state(&filename_string) runs before filename_string is moved into
DownloadFinished. Then pass the original filename_string to the event instead of
cloning it, preserving the existing cleanup and emission behavior.

Source: Coding guidelines


330-378: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a direct DownloadError literal over fail(outcome)(report) at standalone construction sites.

fail() is documented as a .map_err() helper, but it's also called and immediately invoked (e.g. fail(DownloadOutcome::Status)(eyre!(...)) here and at the dirname-lookup site around line 304) to build a DownloadError directly. A plain struct literal (DownloadError { outcome: DownloadOutcome::Status, report: eyre!(...) }) is more direct at those call sites and keeps fail()'s use scoped to map_err chaining as documented.

As per coding guidelines: "Prefer struct literals for 'plain old data' types."

🤖 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/firmware/src/downloader.rs` around lines 330 - 378, The standalone
error constructions in the downloader, including the non-success response branch
and the dirname-lookup failure, should use direct DownloadError struct literals
instead of invoking fail(outcome)(report). Set the corresponding outcome and
eyre report fields explicitly, while keeping fail() for map_err chaining only.

Source: Coding guidelines

crates/firmware/src/tests/downloader.rs (1)

32-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Convert to a table-driven test with check_values.

loggable_url is a pure conversion (URL in, sanitized URL out) with two cases, which is exactly what value_scenarios!/check_values is for — and the pattern is already used elsewhere in this file (download_outcome_labels_render_snake_case).

♻️ Proposed table-driven test
-#[test]
-fn loggable_url_drops_query_parameters() {
-    assert_eq!(
-        loggable_url("https://firmware.example/bmc.fwpkg?X-Amz-Signature=secret"),
-        "https://firmware.example/bmc.fwpkg",
-    );
-    assert_eq!(
-        loggable_url("file:///tmp/bmc.fwpkg"),
-        "file:///tmp/bmc.fwpkg"
-    );
-}
+#[test]
+fn loggable_url_drops_query_parameters() {
+    check_values(
+        [
+            Check {
+                scenario: "presigned URL with query string",
+                input: "https://firmware.example/bmc.fwpkg?X-Amz-Signature=secret",
+                expect: "https://firmware.example/bmc.fwpkg".to_string(),
+            },
+            Check {
+                scenario: "file URL without query string",
+                input: "file:///tmp/bmc.fwpkg",
+                expect: "file:///tmp/bmc.fwpkg".to_string(),
+            },
+        ],
+        loggable_url,
+    );
+}
As per coding guidelines: "prefer table-driven tests using grouped `carbide-test-support` scenarios (`scenarios!`/`value_scenarios!`) or explicit cases (`check_cases`/`check_values`) for parsers, validators, conversions, and similar logic."
🤖 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/firmware/src/tests/downloader.rs` around lines 32 - 42, Convert
loggable_url_drops_query_parameters into a table-driven value test using the
existing value_scenarios!/check_values pattern demonstrated by
download_outcome_labels_render_snake_case. Include both the signed URL
sanitization case and the file URL unchanged case, preserving their expected
outputs.

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/firmware/src/downloader.rs`:
- Around line 198-224: Reorder the cleanup and event construction in the
download task so clear_download_state(&filename_string) runs before
filename_string is moved into DownloadFinished. Then pass the original
filename_string to the event instead of cloning it, preserving the existing
cleanup and emission behavior.
- Around line 330-378: The standalone error constructions in the downloader,
including the non-success response branch and the dirname-lookup failure, should
use direct DownloadError struct literals instead of invoking
fail(outcome)(report). Set the corresponding outcome and eyre report fields
explicitly, while keeping fail() for map_err chaining only.

In `@crates/firmware/src/tests/downloader.rs`:
- Around line 32-42: Convert loggable_url_drops_query_parameters into a
table-driven value test using the existing value_scenarios!/check_values pattern
demonstrated by download_outcome_labels_render_snake_case. Include both the
signed URL sanitization case and the file URL unchanged case, preserving their
expected outputs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 128570bd-9c63-4ddb-9818-c75db091e339

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5316b and 5e23261.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/firmware/Cargo.toml
  • crates/firmware/src/downloader.rs
  • crates/firmware/src/tests/downloader.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