Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/api-core/src/handlers/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@ pub(crate) async fn get_switch_nvos_credentials(
db::ObjectColumnFilter::One(db::switch::IdColumn, &switch_id),
)
.await?;
let _ = txn.rollback().await;
txn.rollback_or_log("read-only load of switch for credential lookup")
.await;

let switch = switches
.first()
Expand Down
6 changes: 4 additions & 2 deletions crates/api-core/src/handlers/expected_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,8 @@ async fn process_batch_operations(
}
},
Err(e) => {
let _ = txn.rollback().await;
txn.rollback_or_log("expected-machine write after operation failure")
.await;
results.push(build_failure_result(id, format!("Operation failed: {}", e)));
}
}
Expand Down Expand Up @@ -624,7 +625,8 @@ async fn process_batch_operations(
)
.await
{
let _ = txn.rollback().await;
txn.rollback_or_log("expected-machine write after operation failure")
.await;
return Err(e);
}
results.push(build_success_result(machine_for_result));
Expand Down
3 changes: 2 additions & 1 deletion crates/api-core/src/handlers/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ pub(crate) async fn find_by_ids(
for snapshot in snapshots.into_iter() {
instances.push(snapshot_to_instance(snapshot)?);
}
let _ = txn.rollback().await;
txn.rollback_or_log("read-only load of instances by id")
.await;
Comment on lines +281 to +282

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant handler sections with line numbers.
for f in \
  crates/api-core/src/handlers/instance.rs \
  crates/api-core/src/handlers/credential.rs \
  crates/api-core/src/handlers/power_shelf.rs \
  crates/api-core/src/handlers/rack.rs \
  crates/api-core/src/handlers/switch.rs
do
  echo "===== $f ====="
  sed -n '1,260p' "$f" | nl -ba | sed -n '1,260p' | tail -n +1
  echo
done

Repository: NVIDIA/infra-controller

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the exact ranges around the cited lines.
sed -n '240,310p' crates/api-core/src/handlers/instance.rs | nl -ba
echo "-----"
sed -n '500,560p' crates/api-core/src/handlers/credential.rs | nl -ba
echo "-----"
sed -n '110,170p' crates/api-core/src/handlers/power_shelf.rs | nl -ba
echo "-----"
sed -n '100,150p' crates/api-core/src/handlers/rack.rs | nl -ba
echo "-----"
sed -n '150,220p' crates/api-core/src/handlers/switch.rs | nl -ba

Repository: NVIDIA/infra-controller

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "rollback_or_log|fn rollback_or_log|impl .*rollback_or_log" crates/api-core/src crates -g '*.rs'

Repository: NVIDIA/infra-controller

Length of output: 1878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  f="$1"
  start="$2"
  end="$3"
  echo "===== $f:$start-$end ====="
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%d:%s\n", NR, $0 }' "$f"
  echo
}

show crates/api-core/src/handlers/instance.rs 250 295
show crates/api-core/src/handlers/credential.rs 500 545
show crates/api-core/src/handlers/power_shelf.rs 120 155
show crates/api-core/src/handlers/rack.rs 110 140
show crates/api-core/src/handlers/switch.rs 160 205
show crates/api-db/src/lib.rs 680 735

Repository: NVIDIA/infra-controller

Length of output: 9537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Transaction drop/commit behavior and any auto-rollback path.
rg -n "impl Drop for Transaction|rollback\(\)|commit\(\)|auto-rollback|Drop for .*Transaction" crates/api-db/src/lib.rs crates/api-db/src -g '*.rs'

Repository: NVIDIA/infra-controller

Length of output: 9308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the transaction type around the drop/commit implementation.
awk 'NR>=735 && NR<=860 { printf "%d:%s\n", NR, $0 }' crates/api-db/src/lib.rs

Repository: NVIDIA/infra-controller

Length of output: 4879


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Drop implementation and any references to implicit rollback.
rg -n "Drop for Transaction|drop\(self\)|rollback on drop|auto.*rollback|transaction.*drop" crates/api-db/src/lib.rs crates/api-db/src -g '*.rs'

Repository: NVIDIA/infra-controller

Length of output: 161


🌐 Web query:

sqlx Transaction drop automatically rolls back rollback errors ignored documentation

💡 Result:

In sqlx, when a Transaction object goes out of scope without being explicitly committed or rolled back, its Drop implementation triggers an automatic rollback [1][2]. Key points regarding this behavior include: 1. Automatic Rollback: If neither commit nor rollback is called before the transaction goes out of scope, the Transaction destructor (Drop) queues a rollback operation [1][2]. This is designed to ensure that pending transactions do not persist if an error occurs or the code path exits prematurely [1][2]. 2. Error Handling Limitations: Because the Drop trait in Rust is synchronous and cannot be async, the rollback operation triggered by dropping a transaction cannot be awaited [2][3]. Consequently, any errors that might occur during this automatic rollback cannot be captured or handled by your application code [3]. The rollback is effectively "fire-and-forget," and its success or failure remains opaque to the caller [2][3]. 3. Recommended Practice: Relying on the Drop implementation for rollback is considered a fallback mechanism rather than a best practice [3][4]. Since the automatic rollback relies on the underlying connection's state and future I/O, developers are encouraged to explicitly call.rollback.await or use helper functions like Connection::transaction (which handles commit/rollback automatically based on the result of the provided closure) to ensure predictable and reliable transaction lifecycle management [5][3]. 4. Performance Implications: Relying on Drop can lead to delayed rollbacks [3][4]. The rollback command is queued to be executed during the next asynchronous operation on that connection (or when the connection is returned to the pool and validated), which may result in database locks being held longer than necessary [3][4].

Citations:


🌐 Web query:

sqlx Transaction on Drop rollback silently ignore errors docs

💡 Result:

In sqlx, if a Transaction goes out of scope without an explicit call to commit or rollback, the transaction is automatically rolled back via its Drop implementation [1][2][3]. This automatic rollback occurs silently in the sense that the Drop trait cannot return a Result, and therefore it cannot propagate errors to the caller [2][3]. Because the rollback is initiated during the synchronous Drop method, sqlx queues a rollback operation using start_rollback, which is intended to be executed upon the next asynchronous invocation of the underlying connection (such as when the connection is returned to a connection pool) [2][3]. Key points regarding this behavior: - Automatic Rollback: If you do not explicitly commit or rollback, sqlx will ensure the transaction is aborted when the Transaction object is dropped [1][3]. - Error Handling: Because the automatic process happens in the drop implementation, any errors that occur during this background rollback cannot be caught or handled by your application code [2][3]. - Implementation: The library performs this by setting a flag in the connection state to trigger a rollback at the next available opportunity [2][3]. It is considered best practice to explicitly call commit on your transactions to ensure they are finalized successfully and to handle potential errors resulting from the commit operation [2][3].

Citations:


Move rollback_or_log ahead of the fallible work.

These read-only handlers return early on query/conversion errors, so the explicit rollback/log path is skipped and the transaction falls back to Drop’s silent rollback. That leaves rollback failures unreported on the error path. Apply the same pattern in the other listed handlers.

📍 Affects 5 files
  • crates/api-core/src/handlers/instance.rs#L281-L282 (this comment)
  • crates/api-core/src/handlers/credential.rs#L529-L530
  • crates/api-core/src/handlers/power_shelf.rs#L141-L142
  • crates/api-core/src/handlers/rack.rs#L128-L128
  • crates/api-core/src/handlers/switch.rs#L179-L180
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-core/src/handlers/instance.rs` around lines 281 - 282, Move each
handler’s rollback_or_log call before its fallible query and conversion work so
rollback failures are reported on all early-return paths. Apply this pattern in
crates/api-core/src/handlers/instance.rs:281-282, credential.rs:529-530,
power_shelf.rs:141-142, rack.rs:128, and switch.rs:179-180, preserving each
existing rollback context message.


Ok(Response::new(rpc::InstanceList { instances }))
}
Expand Down
3 changes: 2 additions & 1 deletion crates/api-core/src/handlers/power_shelf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ pub async fn find_by_ids(
)
.await?;

let _ = txn.rollback().await;
txn.rollback_or_log("read-only load of power shelves by id")
.await;

let power_shelves = convert_power_shelves(power_shelf_list)?;

Expand Down
2 changes: 1 addition & 1 deletion crates/api-core/src/handlers/rack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub async fn find_by_ids(
result.push(rack.into());
}

let _ = txn.rollback().await;
txn.rollback_or_log("read-only load of racks by id").await;

Ok(Response::new(rpc::RackList { racks: result }))
}
Expand Down
3 changes: 2 additions & 1 deletion crates/api-core/src/handlers/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ pub async fn find_by_ids(
.map(|row| (row.switch_id, row))
.collect();

let _ = txn.rollback().await;
txn.rollback_or_log("read-only load of switches by id")
.await;

let switches: Vec<rpc::Switch> = switch_list
.into_iter()
Expand Down
25 changes: 25 additions & 0 deletions crates/api-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,31 @@ impl<'a> Transaction<'a> {
})
}

// This function can just async when
// https://github.com/rust-lang/rust/issues/110011 will be
// implemented
/// Roll back the transaction, logging a warning tagged with `context` if
/// the rollback itself fails, rather than propagating -- for read-only or
/// cleanup paths where the caller can't act on a rollback error but must
/// not swallow it.
#[track_caller]
pub fn rollback_or_log(
self,
context: &'static str,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
let loc = Location::caller();
Box::pin(async move {
if let Err(e) = self
.inner
.rollback()
.await
.map_err(|e| DatabaseError::txn_rollback(e, loc))
{
tracing::warn!(error = %e, context, "Failed to roll back transaction");
}
})
}

pub fn as_pgconn(&mut self) -> &mut sqlx::PgConnection {
&mut self.inner
}
Expand Down
27 changes: 21 additions & 6 deletions crates/dhcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,27 @@ impl Default for CarbideDhcpContext {
Self {
api_endpoint: "https://[::1]:1079".to_string(),
nameservers: vec![Ipv4Addr::new(1, 1, 1, 1)],
forge_root_ca_path: std::env::var("FORGE_ROOT_CAFILE_PATH")
.unwrap_or_else(|_| tls_default::ROOT_CA.to_string()),
forge_client_cert_path: std::env::var("FORGE_CLIENT_CERT_PATH")
.unwrap_or_else(|_| tls_default::CLIENT_CERT.to_string()),
forge_client_key_path: std::env::var("FORGE_CLIENT_KEY_PATH")
.unwrap_or_else(|_| tls_default::CLIENT_KEY.to_string()),
forge_root_ca_path: std::env::var("FORGE_ROOT_CAFILE_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_ROOT_CAFILE_PATH unset or unreadable ({e}); falling back to the built-in default root CA at {}",
tls_default::ROOT_CA
);
tls_default::ROOT_CA.to_string()
}),
forge_client_cert_path: std::env::var("FORGE_CLIENT_CERT_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_CLIENT_CERT_PATH unset or unreadable ({e}); falling back to the built-in default client certificate at {}",
tls_default::CLIENT_CERT
);
tls_default::CLIENT_CERT.to_string()
}),
forge_client_key_path: std::env::var("FORGE_CLIENT_KEY_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_CLIENT_KEY_PATH unset or unreadable ({e}); falling back to the built-in default client key at {}",
tls_default::CLIENT_KEY
);
tls_default::CLIENT_KEY.to_string()
}),
Comment on lines +78 to +98

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 | ⚡ Quick win

Treat empty TLS-path environment variables as invalid.

std::env::var returns Ok("") for an explicitly empty variable, so these branches skip the fallback and pass empty certificate/key paths to build_forge_client_config. Validate the returned paths before accepting them, and use the built-in defaults when they are empty.

Proposed approach
-            forge_root_ca_path: std::env::var("FORGE_ROOT_CAFILE_PATH").unwrap_or_else(|e| {
+            forge_root_ca_path: match std::env::var("FORGE_ROOT_CAFILE_PATH") {
+                Ok(path) if !path.is_empty() => path,
+                Ok(_) | Err(_) => {
+                    // log the appropriate reason, then:
+                    tls_default::ROOT_CA.to_string()
+                }
+            },

Apply the same validation to the client certificate and key paths.

As per path instructions, DHCP TLS-path fallbacks must be reviewed for configuration correctness and runtime behavior.

🤖 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/dhcp/src/lib.rs` around lines 78 - 98, Update the TLS path
configuration fields forge_root_ca_path, forge_client_cert_path, and
forge_client_key_path to reject empty values returned by std::env::var,
accepting only non-empty paths and falling back to the corresponding tls_default
constants with the existing warnings. Apply identical validation to all three
paths before they reach build_forge_client_config.

Source: Path instructions

ntpservers: vec![
Ipv4Addr::new(172, 20, 0, 24),
Ipv4Addr::new(172, 20, 0, 26),
Expand Down
1 change: 1 addition & 0 deletions crates/dsx-exchange-consumer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ carbide-version = { path = "../version" }
[dev-dependencies]
carbide-instrument = { path = "../instrument", features = ["test-support"] }
carbide-test-support = { path = "../test-support" }
tracing = { workspace = true }

[lints]
workspace = true
12 changes: 9 additions & 3 deletions crates/dsx-exchange-consumer/src/health_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use crate::ConsumerMetrics;
use crate::api_client::{HEALTH_REPORT_SOURCE, RackHealthReportSink};
use crate::config::CacheConfig;
use crate::messages::{FaultValue, LeakMetadata, LeakPointType, ValueMessage};
use crate::metrics::{MessageDeduplicated, MessageProcessed};
use crate::metrics::{HealthReportPersistFailed, MessageDeduplicated, MessageProcessed};
use crate::mqtt_consumer::MqttMessage;

/// Health status updater that processes MQTT messages and updates the API.
Expand Down Expand Up @@ -212,8 +212,14 @@ impl<S: RackHealthReportSink> HealthUpdater<S> {
Ok(_) => {
emit(MessageProcessed);
}
Err(_) => {
// API call failed - will retry on next message
Err(e) => {
// Surface the persist failure and count it. The value re-arrives
// on the next message, so processing still retries -- but the
// drop is now visible instead of silently discarded.
emit(HealthReportPersistFailed {
rack_id: metadata.rack_id.clone(),
error: e.to_string(),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions crates/dsx-exchange-consumer/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ pub struct MessageDropped;
)]
pub struct MessageDeduplicated;

/// A rack health report could not be persisted to the Carbide API -- either a
/// coolant-leak override insert or its clearing removal failed. The value
/// re-arrives on the next message, so processing still retries, but a rising
/// rate is safety-relevant: leak state is not reaching the API. Logs the
/// failure and moves the counter from one `emit`.
///
/// Unlike the grandfathered counters above, this is a new metric, so it uses
/// the standard checked name: the exporter appends the single `_total` that
/// `/metrics` shows.
#[derive(Event)]
#[event(
name = "carbide_dsx_exchange_consumer_health_report_persist_failures_total",
component = "nico-dsx-exchange-consumer",
log = warn,
metric = counter,
message = "Failed to persist rack health report",
describe = "Number of rack health report persist failures against the Carbide API"
)]
pub struct HealthReportPersistFailed {
/// The rack whose health report could not be persisted.
#[context]
pub rack_id: String,
/// The API error that prevented the persist.
#[context]
pub error: String,
}

/// Consumer metrics that remain hand-rolled OpenTelemetry counters.
///
/// Only `alerts_detected` stays here: its `point_type` label is a
Expand Down
53 changes: 48 additions & 5 deletions crates/dsx-exchange-consumer/tests/metric_exposition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
//! registering and the OTel Prometheus exporter appends exactly one, so
//! `/metrics` shows one suffix, not the historical doubled `_total_total`.
//!
//! One test in its own binary (its own process-global registry) keeps the
//! `counter_delta` measurements deterministic: the crate's other unit tests
//! emit these same events, but from a different test process.
//! These tests live in their own binary (its own process-global registry) to
//! keep the `counter_delta` measurements deterministic: the crate's other unit
//! tests emit these same events -- the message counters here, and the
//! health-report persist failure below -- but from a different test process, so
//! they cannot advance a shared counter between a test's baseline and delta.

use carbide_dsx_exchange_consumer::metrics::{
MessageDeduplicated, MessageDropped, MessageProcessed, MessageReceived,
HealthReportPersistFailed, MessageDeduplicated, MessageDropped, MessageProcessed,
MessageReceived,
};
use carbide_instrument::emit;
use carbide_instrument::testing::{MetricsCapture, capture_logs};
use carbide_instrument::testing::{CapturedLog, MetricsCapture, capture_logs};

/// Emitting each event once moves exactly its counter, under the single
/// `_total` name the OTel Prometheus exporter produces (the framework strips
Expand Down Expand Up @@ -81,3 +84,43 @@ fn message_events_expose_single_total_names_and_are_metric_only() {
// remain.
assert!(logs.is_empty(), "events must be metric-only: {logs:?}");
}

/// A persist failure writes the WARN line carrying the rack id and error, and
/// moves `carbide_dsx_exchange_consumer_health_report_persist_failures_total` once --
/// the "log it AND count it" the safety-relevant drop needs. Isolated here
/// because `health_updater`'s failure-path unit tests emit this same event
/// without a `MetricsCapture`; from a shared process they could advance the
/// zero-label counter between this test's baseline and delta.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#[test]
fn health_report_persist_failed_logs_warn_and_counts() {
let metrics = MetricsCapture::start();
let logs = capture_logs(|| {
emit(HealthReportPersistFailed {
rack_id: "rack-42".to_string(),
error: "API call failed: deadline exceeded".to_string(),
});
});

assert_eq!(logs.len(), 1);
assert_eq!(logs[0].level, tracing::Level::WARN);
assert_eq!(logs[0].message, "Failed to persist rack health report");
fn field<'a>(log: &'a CapturedLog, name: &str) -> Option<&'a str> {
log.fields
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_str())
}
assert_eq!(field(&logs[0], "rack_id"), Some("rack-42"));
assert_eq!(
field(&logs[0], "error"),
Some("API call failed: deadline exceeded")
);

assert_eq!(
metrics.counter_delta(
"carbide_dsx_exchange_consumer_health_report_persist_failures_total",
&[],
),
1.0
);
}
26 changes: 18 additions & 8 deletions crates/health/src/sink/log_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,18 @@ impl SyncLogFileWriter {
tracing::info!(size = self.current_size, "rotating log file");

// flush and drop the current file handle before renaming
if let Some(mut file) = self.current_file.take() {
let _ = file.flush();
if let Some(mut file) = self.current_file.take()
&& let Err(e) = file.flush()
{
tracing::warn!(error = %e, "failed to flush log file before rotation");
}
Comment on lines +211 to 215

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

# Map the file shape first, then inspect the relevant section.
ast-grep outline crates/health/src/sink/log_file.rs --view expanded || true

echo
wc -l crates/health/src/sink/log_file.rs
echo

sed -n '170,290p' crates/health/src/sink/log_file.rs | cat -n

Repository: NVIDIA/infra-controller

Length of output: 7202


🏁 Script executed:

set -euo pipefail

# Inspect the call path and any rotation tests.
sed -n '1,170p' crates/health/src/sink/log_file.rs | cat -n
echo
sed -n '300,560p' crates/health/src/sink/log_file.rs | cat -n
echo
rg -n "rotate_if_needed|write_line\\(" crates/health/src/sink/log_file.rs crates/health/src -g '!target'

Repository: NVIDIA/infra-controller

Length of output: 18896


Do not continue rotation after a flush error.
rotate_if_needed() takes ownership of self.current_file before checking flush(). If that flush fails, the handle is dropped and rotation still proceeds, which can strand any pending bytes and mask the underlying I/O failure. Keep the writer alive, return the error, and only drop it after a successful flush.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/sink/log_file.rs` around lines 211 - 215, The
rotate_if_needed() flush path must stop rotation when file.flush() fails instead
of dropping the handle and continuing. Preserve self.current_file on failure,
propagate the I/O error immediately, and only allow the existing ownership
transfer/drop behavior after a successful flush.


let current_path = self.log_path();

if self.max_backups == 0 {
let _ = fs::remove_file(&current_path);
if let Err(e) = fs::remove_file(&current_path) {
tracing::warn!(error = %e, path = %current_path.display(), "failed to remove current log file during rotation");
}
} else {
self.shift_backups(&current_path);
}
Expand All @@ -229,19 +233,25 @@ impl SyncLogFileWriter {
for i in (1..self.max_backups).rev() {
let from = self.rotated_path(i);
let to = self.rotated_path(i + 1);
if from.exists() {
let _ = fs::rename(&from, &to);
if from.exists()
&& let Err(e) = fs::rename(&from, &to)
{
tracing::warn!(error = %e, from = %from.display(), to = %to.display(), "failed to shift rotated log file");
}
}

// current -> .1
let backup = self.rotated_path(1);
let _ = fs::rename(current_path, &backup);
if let Err(e) = fs::rename(current_path, &backup) {
tracing::warn!(error = %e, from = %current_path.display(), to = %backup.display(), "failed to rotate current log file to backup");
}

// prune the oldest backup beyond the limit
let oldest = self.rotated_path(self.max_backups + 1);
if oldest.exists() {
let _ = fs::remove_file(&oldest);
if oldest.exists()
&& let Err(e) = fs::remove_file(&oldest)
{
tracing::warn!(error = %e, path = %oldest.display(), "failed to prune oldest rotated log file");
Comment on lines +236 to +254

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='crates/health/src/sink/log_file.rs'
echo "== file size =="
wc -l "$file"
echo "== relevant slice =="
sed -n '200,290p' "$file"
echo "== search exists/try_exists in file =="
rg -n 'try_exists|\.exists\(\)|rename\(|remove_file\(' "$file"

Repository: NVIDIA/infra-controller

Length of output: 4000


Avoid gating backup rotation on Path::exists()

exists() turns permission and other metadata failures into false, so shift_backups() can skip a real error without logging it. Call rename/remove_file directly and ignore only NotFound, or switch to try_exists() with explicit Err 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/health/src/sink/log_file.rs` around lines 236 - 254, Update
shift_backups to avoid using Path::exists() before rotating or pruning files.
Call fs::rename and fs::remove_file directly, treating only ErrorKind::NotFound
as an ignorable absence and logging other filesystem errors through the existing
tracing warnings.

}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/machine-controller/src/handler/dpf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ async fn create_and_register_dpudevices_and_dpunode(
.and_then(|x| x.dmi_data.as_ref())
.map(|x| x.product_serial.as_str())
.unwrap_or_default();
if serial_number.is_empty() {
tracing::warn!(
dpu_machine_id = %dpu.id,
host_machine_id = %state.host_snapshot.id,
"DPU product serial is missing; registering DPU device with DPF using an empty serial"
);
}
let device_info = carbide_dpf::DpuDeviceInfo {
device_id: dpf_id(dpu)?,
dpu_bmc_ip: bmc_ip(dpu)?,
Expand Down
Loading
Loading