Skip to content

fix: AdminForceDeleteMachine can deadlock#3364

Merged
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3339
Jul 10, 2026
Merged

fix: AdminForceDeleteMachine can deadlock#3364
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3339

Conversation

@chet

@chet chet commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Caught this during PR #3293 CICD -- test-release-container-services failed twice in one day, both times with Postgres killing a deadlock inside AdminForceDeleteMachine or the discovery flow racing it (run was 29035250694):

ERROR request: db: Database Error: error returned from database: deadlock detected at line 1147 file=crates/api-db/src/explored_managed_host.rs line=132 query=DELETE FROM explored_managed_hosts WHERE host_bmc_ip = $1. ... rpc.method="AdminForceDeleteMachine"

The root cause is lock ordering: force-delete took machine-interface rows before the explored tables and segment locks, while every writer it races -- site-explorer's persistence pass, DPU discovery, the allocator/reconcile paths -- acquires them the other way around. This PR gives all of those flows one global lock order -- segment advisory locks first, then interface/address rows, with the explored-table cleanup ahead of any interface work -- so the deadlock class is gone, not just the captured instance.

Core changes

  • admin_force_delete_machine opens its transaction by advisory-locking every admin segment (the new db::machine_interface::lock_all_admin_segments) and moves the explored_managed_hosts/explored_endpoints cleanup ahead of all interface deletes, matching site-explorer's write order.
  • The flows that wrote interface rows before their reconcile pass took the segment locks now hoist the admin-segment locks to their transaction start: DPU discovery (machine_discovery.rs), site-explorer machine creation (machine_creator.rs), and set_primary_interface (managed_host.rs).
  • The DHCP SLAAC replacement locks its segment before dropping the address row.
  • lock_network_segments_exclusive is now the one home for the segment lock key and ordering; lock_network_segment_exclusive and load_and_lock_all_admin_segments funnel through it.

Regression test

  • Drives the real force-delete RPC against an exploration-order transaction, synchronized on pg_stat_activity lock waits scoped to the test's database.
  • Reproduces the exact CI deadlock against the previous ordering, and completes cleanly with the fix.

Tests added!

This supports #3339

Closes #3339

@chet chet requested a review from a team as a code owner July 10, 2026 15:48
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when transitioning interfaces from SLAAC to stateful DHCPv6 by ensuring the delete-then-allocate sequence runs under the correct segment lock order.
    • Prevented potential deadlocks by enforcing consistent advisory lock ordering during machine discovery, admin reconciliation, primary interface updates, and force deletion.
    • Improved deterministic cleanup of explored host/endpoint data during force deletion.
  • Tests
    • Added a concurrency regression test covering lock ordering between force deletion and exploration activity.

Walkthrough

The changes centralize advisory locking for network segments, apply admin-segment locks before interface and reconciliation operations, reorder force-delete explored-data cleanup, and add concurrency coverage for exploration and deletion lock ordering.

Changes

Lock ordering and transaction cleanup

Layer / File(s) Summary
Centralized segment lock helpers
crates/api-db/src/machine_interface.rs
Segment locks are sorted and de-duplicated, all-admin locking is exposed, and reconciliation uses the shared multi-segment helper.
Lock entrypoints before interface writes
crates/api-core/src/handlers/machine_discovery.rs, crates/api-core/src/handlers/managed_host.rs, crates/site-explorer/src/machine_creator.rs
Discovery, primary-interface updates, and managed-host creation lock admin segments before interface writes and reconciliation.
Ordered force-delete cleanup and concurrency validation
crates/api-core/src/handlers/machine.rs, crates/api-core/src/tests/machine_admin_force_delete.rs
Force-delete locks admin segments before explored-data cleanup, and a SQLx test validates concurrent exploration and deletion behavior.
Lock SLAAC-to-DHCP transition
crates/api-core/src/dhcp/discover.rs
IPv6 SLAAC removal and DHCP allocation now follow an exclusive network-segment lock.

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

Sequence Diagram(s)

sequenceDiagram
  participant ForceDelete
  participant Exploration
  participant PostgreSQL
  ForceDelete->>PostgreSQL: Lock admin segments
  ForceDelete->>PostgreSQL: Delete explored rows
  Exploration->>PostgreSQL: Lock explored_managed_hosts
  ForceDelete-->>Exploration: Wait for explored-row lock
  Exploration->>PostgreSQL: Update machine_interfaces
  Exploration->>PostgreSQL: Commit
  PostgreSQL-->>ForceDelete: Complete deletion
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a deadlock fix in AdminForceDeleteMachine.
Description check ✅ Passed The description is directly related to the changes and explains the deadlock fix and regression test.
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.

@chet

chet commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@chet, you're welcome! I'll redo the full review of the PR now, covering the updated lock ordering across admin_force_delete_machine, machine_discovery.rs, machine_creator.rs, managed_host.rs, and the DHCP SLAAC path, along with the new regression test.

(=^・ω・^=) ノ

✅ Action performed

Full review finished.

@chet chet changed the title fix: end AdminForceDeleteMachine deadlocks with one global lock order fix: AdminForceDeleteMachine deadlocks in CICD Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/api-core/src/handlers/machine.rs (1)

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

Emit cleanup identifiers as structured tracing fields.

These interpolated values will not be independently searchable as logfmt attributes.

Proposed structured logs
-        tracing::info!("Cleaning up explored endpoint at {addr} {}", machine.id);
+        tracing::info!(%addr, machine_id = %machine.id, "Cleaning up explored endpoint");
...
-            tracing::info!("Cleaning up explored endpoint at {addr} {}", dpu_machine.id);
+            tracing::info!(%addr, machine_id = %dpu_machine.id, "Cleaning up explored endpoint");

As per coding guidelines, common log data must be passed as tracing attributes rather than string interpolation.

Also applies to: 652-652

🤖 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/machine.rs` at line 640, Update the cleanup logs
in the explored-endpoint cleanup handler, including the messages around the
referenced `tracing::info!` calls, to emit `addr` and `machine.id` as structured
tracing fields rather than interpolating them into the message; keep the message
static and pass both identifiers as named tracing attributes.

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/api-core/src/handlers/machine_discovery.rs`:
- Around line 292-296: Move the lock_all_admin_segments call earlier in the
transaction, before associate_interface_with_dpu_machine performs its
interface-row write. Ensure every execution path that reaches that write
acquires the segment advisory locks first, and remove the later redundant lock
call while preserving allocator lock ordering through reconciliation.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 1895-1900: Stabilize the Admin segment set before reconciliation
by serializing Admin-segment create, update, and delete operations with the same
catalog-level advisory lock used by load_and_lock_all_admin_segments, or
otherwise use a transaction-wide stable snapshot. Update the relevant Admin
segment mutation paths and load_and_lock_all_admin_segments so later reconcile
reads cannot discover newly committed segments after interface row locks are
acquired.

---

Nitpick comments:
In `@crates/api-core/src/handlers/machine.rs`:
- Line 640: Update the cleanup logs in the explored-endpoint cleanup handler,
including the messages around the referenced `tracing::info!` calls, to emit
`addr` and `machine.id` as structured tracing fields rather than interpolating
them into the message; keep the message static and pass both identifiers as
named tracing attributes.
🪄 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: 262ca78a-0652-40e3-9696-f2735b3142c2

📥 Commits

Reviewing files that changed from the base of the PR and between d00d667 and 4abf2d0.

📒 Files selected for processing (7)
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/managed_host.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-db/src/machine_interface.rs
  • crates/site-explorer/src/machine_creator.rs

Comment thread crates/api-core/src/handlers/machine_discovery.rs Outdated
Comment thread crates/api-db/src/machine_interface.rs
@chet chet changed the title fix: AdminForceDeleteMachine deadlocks in CICD fix: AdminForceDeleteMachine can deadlock Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/api-core/src/handlers/machine_discovery.rs (1)

291-297: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Segment lock is acquired after an earlier interface-row write, re-opening the deadlock window.

The comment asserts the lock precedes "the interface-row writes below", but when machine_discovery_info.create_machine is true, associate_interface_with_dpu_machine at Lines 202-207 already writes (and row-locks) the interface inside this same transaction — well before Line 296. A concurrent reconcile/allocator transaction holds the admin-segment advisory locks first and then contends for interface rows; this path holds an interface row and then waits on the segment locks, which is the exact inverted ordering the PR set out to eliminate.

Acquire the admin-segment locks once, immediately after txn_begin() (Line 118), so every interface write in the DPU path is ordered behind them.

🔒 Proposed lock placement
     let mut txn = api.txn_begin().await?;
+    if hardware_info.is_dpu() {
+        // Segment advisory locks first, before any interface-row write in this
+        // transaction (e.g. associate_interface_with_dpu_machine below), so the
+        // allocator lock order holds through the reconcile pass.
+        db::machine_interface::lock_all_admin_segments(&mut txn).await?;
+    }
     tracing::debug!(
         ?remote_ip,
         ?interface_id,
         "discover_machine loading interface"
     );

Then drop the later acquisition at Lines 292-296.

As per path instructions: prefer findings about behavior, concurrency, resource lifetimes over style-only comments.

#!/bin/bash
# Confirm associate_interface_with_dpu_machine performs an interface-row write/lock.
rg -nP --type=rust -C8 'async fn associate_interface_with_dpu_machine' crates/api-db/src/machine_interface.rs
🤖 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/machine_discovery.rs` around lines 291 - 297,
Move the DPU admin-segment lock acquisition from the later hardware-info block
to immediately after txn_begin(), before any interface-row operations such as
associate_interface_with_dpu_machine. Remove the later lock_all_admin_segments
call and its outdated comment, ensuring all DPU-path writes follow the
segment-then-interface lock order.

Source: Path instructions

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

Duplicate comments:
In `@crates/api-core/src/handlers/machine_discovery.rs`:
- Around line 291-297: Move the DPU admin-segment lock acquisition from the
later hardware-info block to immediately after txn_begin(), before any
interface-row operations such as associate_interface_with_dpu_machine. Remove
the later lock_all_admin_segments call and its outdated comment, ensuring all
DPU-path writes follow the segment-then-interface lock order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5cdb9628-069d-4125-8d46-827322e72057

📥 Commits

Reviewing files that changed from the base of the PR and between d00d667 and 4abf2d0.

📒 Files selected for processing (7)
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/managed_host.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-db/src/machine_interface.rs
  • crates/site-explorer/src/machine_creator.rs

Machine force-deletion, DPU discovery, exploration, and the DHCP SLAAC-replacement path now hold their locks in one global order -- segment advisory locks first, then machine interface/address rows, with the explored-table cleanup ahead of any interface work -- so the 40P01 deadlocks between `AdminForceDeleteMachine` and its concurrent writers are gone. Those surfaced as gRPC `Internal` and took down PR NVIDIA#3293's CI twice in one day (run 29035250694, victims at `explored_managed_host.rs:132` and `machine_interface.rs:2554`/`:1868`).

- `admin_force_delete_machine` opens its transaction by advisory-locking every admin segment (the new `db::machine_interface::lock_all_admin_segments`) and moves the `explored_managed_hosts`/`explored_endpoints` cleanup ahead of all interface deletes, matching site-explorer's write order -- the two captured cycles.
- The flows that wrote interface rows before their reconcile pass took the segment locks -- DPU discovery (`machine_discovery.rs`), site-explorer machine creation (`machine_creator.rs`), and `set_primary_interface` (`managed_host.rs`) -- hoist the admin-segment locks to their transaction start, and the DHCP SLAAC replacement locks its segment before dropping the address row, so no writer holds interface rows while reaching for a segment lock another transaction owns.
- `lock_network_segments_exclusive` is now the one home for the segment lock key and ordering; `lock_network_segment_exclusive` and `load_and_lock_all_admin_segments` funnel through it.
- The regression test drives the real force-delete RPC against an exploration-order transaction, synchronized on `pg_stat_activity` lock waits scoped to the test's database; it reproduces the exact CI deadlock against the previous ordering and completes cleanly now.

Tests added!

This supports NVIDIA#3339

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

chet commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@chet, you're welcome! I'll redo the full review of the PR again, re-examining the lock ordering changes across admin_force_delete_machine, machine_discovery.rs, machine_creator.rs, managed_host.rs, the DHCP SLAAC path, and the new regression test.

(=^・ω・^=) ノ

✅ 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 (1)
crates/api-core/src/handlers/machine_discovery.rs (1)

118-127: 🚀 Performance & Scalability | 🔵 Trivial

Lock placement is correct; flag the serialization cost on this hot path.

The advisory lock now sits immediately after txn_begin(), ahead of every interface-row write on both the DPU and non-DPU paths, correctly aligning discover_machine with the allocator lock order. This properly resolves the deadlock window.

One consideration worth confirming: lock_all_admin_segments acquires exclusive advisory locks on every admin segment and holds them for the full transaction (through reconcile_admin_addresses_for_host and up to commit at Line 446). Since it is now unconditional, all concurrent discover_machine calls are fully serialized against one another — and against the allocator, reconcile, and force-delete flows — regardless of which segment a given discovery actually touches. On the DPU path this merely widens the pre-existing reconcile hold-window; on the non-DPU path it introduces serialization that did not previously exist. This is a defensible deadlock-safety tradeoff and consistent with the rest of the cohort, but discovery is a frequently-exercised RPC, so it is worth verifying the expected steady-state concurrency and monitoring advisory-lock wait time on this path.

Please confirm the anticipated discovery throughput tolerates full serialization across all admin segments. It may be worth adding a metric on lock-acquisition latency for this transaction to catch contention regressions early.

🤖 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/machine_discovery.rs` around lines 118 - 127,
Add a metric around lock acquisition in discover_machine, specifically timing
db::machine_interface::lock_all_admin_segments(&mut txn).await, and record the
wait duration with appropriate success/error labeling. Confirm via benchmarks or
capacity analysis that serializing all discovery transactions across admin
segments meets expected throughput, and document or test the expected contention
behavior.
🤖 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/api-core/src/handlers/machine_discovery.rs`:
- Around line 118-127: Add a metric around lock acquisition in discover_machine,
specifically timing db::machine_interface::lock_all_admin_segments(&mut
txn).await, and record the wait duration with appropriate success/error
labeling. Confirm via benchmarks or capacity analysis that serializing all
discovery transactions across admin segments meets expected throughput, and
document or test the expected contention behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4ec97c36-eb1a-460e-8add-9c18210a0e89

📥 Commits

Reviewing files that changed from the base of the PR and between 4abf2d0 and 0649cf4.

📒 Files selected for processing (7)
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/managed_host.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-db/src/machine_interface.rs
  • crates/site-explorer/src/machine_creator.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/site-explorer/src/machine_creator.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/managed_host.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-core/src/tests/machine_admin_force_delete.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.

🧹 Nitpick comments (1)
crates/api-core/src/handlers/machine_discovery.rs (1)

118-127: 🚀 Performance & Scalability | 🔵 Trivial

Lock placement is correct; note the global serialization it introduces on the discovery hot path.

Acquiring all admin-segment advisory locks unconditionally at transaction start is the right, conservative choice for lock-order safety. Be aware, however, that this now fully serializes every discover_machine call — DPU and non-DPU alike — against one another and against the allocator/reconcile/force-delete transactions. Under high concurrent discovery load this becomes a single contention point. Recommend adding lock-wait/duration observability around this path so the serialization cost is measurable before it manifests as latency in production.

🤖 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/machine_discovery.rs` around lines 118 - 127,
Add lock-wait and acquisition-duration observability around
lock_all_admin_segments in discover_machine, measuring from transaction start
through successful lock acquisition and logging the elapsed time with
appropriate context. Preserve the existing unconditional lock placement and
error propagation while making contention visible for discovery, allocator,
reconcile, and force-delete interactions.
🤖 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/api-core/src/handlers/machine_discovery.rs`:
- Around line 118-127: Add lock-wait and acquisition-duration observability
around lock_all_admin_segments in discover_machine, measuring from transaction
start through successful lock acquisition and logging the elapsed time with
appropriate context. Preserve the existing unconditional lock placement and
error propagation while making contention visible for discovery, allocator,
reconcile, and force-delete interactions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 309df0ca-af8b-4b35-85dc-129d941b8bb3

📥 Commits

Reviewing files that changed from the base of the PR and between d00d667 and 0649cf4.

📒 Files selected for processing (7)
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/managed_host.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-db/src/machine_interface.rs
  • crates/site-explorer/src/machine_creator.rs

@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 merged commit 8024a15 into NVIDIA:main Jul 10, 2026
58 checks passed
jw-nvidia pushed a commit to jw-nvidia/infra-controller that referenced this pull request Jul 10, 2026
@chet chet deleted the gh-issue-3339 branch July 11, 2026 00:26
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.

AdminForceDeleteMachine deadlocks against concurrent exploration and discovery

2 participants