Skip to content

fix(agent): compare all HBN inputs before skipping apply - #4746

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

fix(agent): compare all HBN inputs before skipping apply#4746
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3893

Conversation

@chet

@chet chet commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The agent uses Core version fields to decide when HBN rendering and apply can be skipped, but some rendering inputs are not covered by those versions. The existing fallback hash hand-picked a subset, so a SitePrefix isolation change or another omitted nested input could leave HBN stale even though the desired response changed.

This builds a canonical fingerprint from the complete managed-host network response, removes only fields that cannot affect HBN, and normalizes collections using the renderer's own order semantics. The top-level destructuring is exhaustive, so a future protobuf field must choose a comparison policy at compile time. DHCP remains on its existing unconditional reconciliation path.

Related issues

This supports #3893

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Additional Notes

Passed the four focused comparison tests, all 202 carbide-agent library tests, nightly formatting, full-workspace Clippy, and custom Carbide lints.

@chet
chet requested a review from a team as a code owner August 8, 2026 07:09
@chet

chet commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c2a44487-8543-4112-8284-069212366559

📥 Commits

Reviewing files that changed from the base of the PR and between e81b90e and f8caf2d.

📒 Files selected for processing (1)
  • crates/agent/src/main_loop.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/agent/src/main_loop.rs

Summary by CodeRabbit

  • Bug Fixes
    • Improved detection of configuration changes affecting generated host network settings.
    • Ensured reordered configuration items are recognized consistently.
    • Preserved meaningful ordering when it affects generated network settings.
    • Prevented unrelated configuration changes from triggering unnecessary network updates.
    • Improved network-version matching across managed hosts and instances.
    • Expanded validation for rendering-affecting, order-sensitive, reordered, and ignored configuration changes.

Walkthrough

The agent now computes a canonical fingerprint from all HBN-rendering inputs. It removes non-rendering fields, normalizes unordered collections, preserves meaningful ordering, and compares the fingerprint with managed-host and instance network versions. Tests cover cache invalidation and ignored changes.

Changes

HBN rendering fingerprint

Layer / File(s) Summary
Network version fingerprint tracking
crates/agent/src/main_loop.rs
CurrentNetworkVersion now stores and compares a canonical rendered-input fingerprint.
Canonical rendered-input hashing
crates/agent/src/main_loop.rs
The code clones protobuf inputs, removes non-rendering fields, normalizes unordered collections, preserves significant ordering, and hashes the canonical encoding.
Fingerprint comparison tests
crates/agent/src/main_loop.rs
Fixtures and tests verify invalidation for rendering-affecting changes, stability for unordered changes, sensitivity to ordered rules and interfaces, and exclusion of non-HBN inputs.

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

Sequence Diagram(s)

sequenceDiagram
  participant CurrentNetworkVersion
  participant RenderedInputResponse
  participant ProtobufEncoder
  CurrentNetworkVersion->>RenderedInputResponse: obtain HBN-rendering inputs
  RenderedInputResponse->>RenderedInputResponse: remove non-rendering fields and normalize collections
  RenderedInputResponse->>ProtobufEncoder: encode canonical response
  ProtobufEncoder-->>CurrentNetworkVersion: return encoded bytes
  CurrentNetworkVersion->>CurrentNetworkVersion: compare fingerprint with network versions
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the HBN apply-skipping fix, canonical fingerprinting, field handling, testing, and related issue.
Title check ✅ Passed The title concisely and accurately describes the main change: comparing all HBN inputs before skipping apply.
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 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

@chet I will perform a complete review of pull request #4746.

🐇

✅ 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/agent/src/main_loop.rs (3)

2038-2074: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a scenario for the initial state before any update_from call.

Every row in these tables calls update_from first, so the (_, None) arm at line 522 is never exercised. That arm guarantees that a freshly constructed CurrentNetworkVersion never reports a match, which is what forces the first HBN apply after an agent restart. The arm is the safety-critical default and it currently has no test.

💚 Proposed additional test
#[test]
fn current_network_version_never_matches_before_first_update() {
    let config = comparison_network_config();
    assert!(!CurrentNetworkVersion::default().matches_versions_from(&config));
}
🤖 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/agent/src/main_loop.rs` around lines 2038 - 2074, Add a focused test
alongside current_network_version_detects_rendered_input_changes that constructs
comparison_network_config and asserts
CurrentNetworkVersion::default().matches_versions_from(&config) returns false
before any update_from call, covering the initial-state safety behavior.

663-697: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the prefix-entry sort key into one helper.

The comparator |left, right| left.prefix.cmp(&right.prefix) appears three times across two protobuf types. If a future change alters this key in only some call sites, the fingerprint becomes order-sensitive again for the missed collection. Centralize the key so the normalization contract has one definition.

♻️ Proposed extraction
+    fn sort_prefix_entries(entries: &mut [rpc::PrefixFilterPolicyEntry]) {
+        entries.sort_by(|left, right| left.prefix.cmp(&right.prefix));
+    }
+
     fn normalize_interface(interface: &mut rpc::FlatInterfaceConfig) {
         interface.vpc_prefixes.sort_unstable();
         interface.vpc_peer_prefixes.sort_unstable();
         interface.vpc_peer_vnis.sort_unstable();
 
         if let Some(routing_profile) = &mut interface.vpc_routing_profile {
             Self::normalize_routing_profile(routing_profile);
         }
         if let Some(routing_profile) = &mut interface.interface_routing_profile {
-            routing_profile
-                .allowed_anycast_prefixes
-                .sort_by(|left, right| left.prefix.cmp(&right.prefix));
+            Self::sort_prefix_entries(&mut routing_profile.allowed_anycast_prefixes);
         }
     fn normalize_routing_profile(routing_profile: &mut rpc::RoutingProfile) {
         routing_profile
             .route_target_imports
             .sort_by_key(|route_target| (route_target.asn, route_target.vni));
         routing_profile
             .route_targets_on_exports
             .sort_by_key(|route_target| (route_target.asn, route_target.vni));
-        routing_profile
-            .accepted_leaks_from_underlay
-            .sort_by(|left, right| left.prefix.cmp(&right.prefix));
-        routing_profile
-            .allowed_anycast_prefixes
-            .sort_by(|left, right| left.prefix.cmp(&right.prefix));
+        Self::sort_prefix_entries(&mut routing_profile.accepted_leaks_from_underlay);
+        Self::sort_prefix_entries(&mut routing_profile.allowed_anycast_prefixes);
     }
🤖 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/agent/src/main_loop.rs` around lines 663 - 697, Extract the repeated
prefix-entry sort key used by normalize_interface and normalize_routing_profile
into a shared helper, then use that helper for all allowed_anycast_prefixes and
accepted_leaks_from_underlay sorts across both protobuf types. Preserve the
existing prefix-based ordering while ensuring future key changes have a single
definition.

606-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document ntp_servers as a DHCP-owned HBN input. update_dhcp consumes it before the HBN skip check, so its exclusion from the fingerprint is safe. host_interface_id and use_admin_network_changed have no HBN rendering consumers.

🤖 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/agent/src/main_loop.rs` around lines 606 - 633, Update the comments
around ntp_servers in the HBN fingerprint reset logic to explicitly document
that it is a DHCP-owned input consumed by update_dhcp before the HBN skip check,
so excluding it is intentional. Keep host_interface_id and
use_admin_network_changed identified as having no HBN rendering consumers.
🤖 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/agent/src/main_loop.rs`:
- Around line 2038-2074: Add a focused test alongside
current_network_version_detects_rendered_input_changes that constructs
comparison_network_config and asserts
CurrentNetworkVersion::default().matches_versions_from(&config) returns false
before any update_from call, covering the initial-state safety behavior.
- Around line 663-697: Extract the repeated prefix-entry sort key used by
normalize_interface and normalize_routing_profile into a shared helper, then use
that helper for all allowed_anycast_prefixes and accepted_leaks_from_underlay
sorts across both protobuf types. Preserve the existing prefix-based ordering
while ensuring future key changes have a single definition.
- Around line 606-633: Update the comments around ntp_servers in the HBN
fingerprint reset logic to explicitly document that it is a DHCP-owned input
consumed by update_dhcp before the HBN skip check, so excluding it is
intentional. Keep host_interface_id and use_admin_network_changed identified as
having no HBN rendering consumers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5f62124f-b59d-435b-bb3c-13a4583347c8

📥 Commits

Reviewing files that changed from the base of the PR and between d77bf0f and e81b90e.

📒 Files selected for processing (1)
  • crates/agent/src/main_loop.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/agent/src/main_loop.rs (4)

663-682: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated prefix-entry sort into one helper.

allowed_anycast_prefixes is sorted by the same comparator in two places: inline at Lines 672-674 and inside normalize_routing_profile at Lines 694-696. The two sites must stay identical for the fingerprint to be canonical. Nothing currently enforces that.

The inline form is correct today because FlatInterfaceRoutingProfile carries only allowed_anycast_prefixes. A single helper keeps both call sites aligned and makes a future field addition to that type easier to spot.

♻️ Proposed refactor
+    fn sort_prefix_entries(prefixes: &mut [rpc::PrefixFilterPolicyEntry]) {
+        prefixes.sort_by(|left, right| left.prefix.cmp(&right.prefix));
+    }
+
     fn normalize_interface(interface: &mut rpc::FlatInterfaceConfig) {
         if let Some(routing_profile) = &mut interface.interface_routing_profile {
-            routing_profile
-                .allowed_anycast_prefixes
-                .sort_by(|left, right| left.prefix.cmp(&right.prefix));
+            Self::sort_prefix_entries(&mut routing_profile.allowed_anycast_prefixes);
         }
-        routing_profile
-            .accepted_leaks_from_underlay
-            .sort_by(|left, right| left.prefix.cmp(&right.prefix));
-        routing_profile
-            .allowed_anycast_prefixes
-            .sort_by(|left, right| left.prefix.cmp(&right.prefix));
+        Self::sort_prefix_entries(&mut routing_profile.accepted_leaks_from_underlay);
+        Self::sort_prefix_entries(&mut routing_profile.allowed_anycast_prefixes);
🤖 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/agent/src/main_loop.rs` around lines 663 - 682, Extract the shared
allowed-anycast-prefix sorting comparator from normalize_interface and
normalize_routing_profile into a single helper, then call that helper at both
sites. Preserve sorting by each entry’s prefix and ensure both normalization
paths use the same implementation for canonical fingerprints.

499-499: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Optional: store the canonical bytes instead of a 64-bit hash.

DefaultHasher produces a 64-bit value. A collision makes matches_versions_from return true for a changed configuration, and the agent then skips an HBN apply silently. The probability is very small, but the failure mode is a silent stale render.

The encoded canonical response is a few kilobytes at most and is already materialized inside hash_rendered_inputs. Storing Option<Vec<u8>> and comparing with == removes the collision class entirely and also removes the DefaultHasher import from this path.

♻️ Proposed change
-    rendered_inputs_hash: Option<u64>,
+    rendered_inputs: Option<Vec<u8>>,
-    fn hash_rendered_inputs(conf: &ManagedHostNetworkConfigResponse) -> u64 {
+    fn canonical_rendered_inputs(conf: &ManagedHostNetworkConfigResponse) -> Vec<u8> {
         let mut canonical = conf.clone();
         Self::remove_non_rendered_inputs(&mut canonical);
         Self::normalize_set_like_inputs(&mut canonical);
-
-        let mut hasher = DefaultHasher::new();
-        canonical.encode_to_vec().hash(&mut hasher);
-
-        hasher.finish()
+        canonical.encode_to_vec()
     }
🤖 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/agent/src/main_loop.rs` at line 499, Replace the rendered_inputs_hash
state with Option<Vec<u8>> and update hash_rendered_inputs and
matches_versions_from to retain and compare the canonical encoded response bytes
directly using equality. Remove the DefaultHasher-based hashing and its import
from this path while preserving the existing version-matching behavior.

512-520: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider counting fallback invalidations with an Event.

This branch is the safety net described in the TODO at Line 535. Its firing rate is the signal that tells you whether Core-side version invalidation is still missing. A plain tracing::info! gives no rate or count.

Declare a carbide_instrument::Event with a carbide_-prefixed _total counter so the fallback rate is observable, and keep the human-readable message as the log line.

Based on the coding guideline "define and emit a carbide_instrument::Event when an event deserves a count, rate, or duration".

🤖 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/agent/src/main_loop.rs` around lines 512 - 520, In the rendered-inputs
mismatch branch of the config-version validation logic, replace the standalone
tracing call with a carb​​ide_instrument::Event definition using a
carbide_-prefixed _total counter, then emit that event while retaining the
existing human-readable info log. Keep the fallback invalidation behavior
unchanged.

Source: Coding guidelines


607-624: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Clarify the ntp_servers ownership comment. ntp_servers feeds update_dhcp, which runs before the HBN skip check. instance feeds metadata and FMDS only, not update_nvue or update_dhcp. Change the comment to state that DHCP configuration, including NTP servers, is reconciled before the skip decision.

🤖 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/agent/src/main_loop.rs` around lines 607 - 624, Update the comment
immediately before ntp_servers.clear() to state that DHCP configuration,
including NTP servers, is reconciled before the HBN skip decision. Keep the
ownership comments for instance and the other fields unchanged.
🤖 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/agent/src/main_loop.rs`:
- Around line 704-718: The direction key in normalize_security_group_rule_order
must distinguish every direction variant: replace the boolean Ingress comparison
with a deterministic integer representation of attributes.direction(). In
crates/agent/src/main_loop.rs lines 1784-1811, update
comparison_security_group_rule to accept a direction parameter, include at least
one Egress fixture, and add an OrderSensitiveInputReordering or
RenderedInputChange scenario that flips a rule’s direction; both sites require
changes.

---

Nitpick comments:
In `@crates/agent/src/main_loop.rs`:
- Around line 663-682: Extract the shared allowed-anycast-prefix sorting
comparator from normalize_interface and normalize_routing_profile into a single
helper, then call that helper at both sites. Preserve sorting by each entry’s
prefix and ensure both normalization paths use the same implementation for
canonical fingerprints.
- Line 499: Replace the rendered_inputs_hash state with Option<Vec<u8>> and
update hash_rendered_inputs and matches_versions_from to retain and compare the
canonical encoded response bytes directly using equality. Remove the
DefaultHasher-based hashing and its import from this path while preserving the
existing version-matching behavior.
- Around line 512-520: In the rendered-inputs mismatch branch of the
config-version validation logic, replace the standalone tracing call with a
carb​​ide_instrument::Event definition using a carbide_-prefixed _total counter,
then emit that event while retaining the existing human-readable info log. Keep
the fallback invalidation behavior unchanged.
- Around line 607-624: Update the comment immediately before ntp_servers.clear()
to state that DHCP configuration, including NTP servers, is reconciled before
the HBN skip decision. Keep the ownership comments for instance and the other
fields unchanged.
🪄 Autofix

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: ee911cdc-2f79-4784-b029-93dd834900ca

📥 Commits

Reviewing files that changed from the base of the PR and between d77bf0f and e81b90e.

📒 Files selected for processing (1)
  • crates/agent/src/main_loop.rs

Comment thread crates/agent/src/main_loop.rs
The agent used Core's versions plus a partial hash to decide whether HBN could be left alone. Unversioned changes outside that list, including SitePrefix isolation inputs, could therefore miss an apply.

Build the fingerprint from the complete response, remove only fields that cannot affect HBN, and normalize order-insensitive collections to match the renderer. The exhaustive field classification makes future protobuf additions choose a comparison policy at compile time.

This supports NVIDIA#3893

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet

chet commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the initial-cache safety test and shared prefix-entry sorting helper in f8caf2d. I left the NTP comment unchanged because it already states the relevant ownership boundary: DHCP is reconciled before the HBN skip decision on every iteration.

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