fix(csi-driver): repair paths before connecting them, not after - #533
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect same-tick reconnection, orchestration coverage, and transport-specific grace handling.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR reorders CSI NVMe path repair before reconciliation and adds a grace period for newly connected controllers.
Changes:
- Repairs paths before reconnect passes.
- Tracks recent connects with a 5-second grace window.
- Adds regression coverage for deferred repair.
File summaries
| File | Summary |
|---|---|
csi-driver/pkg/util/nvmerepair.go |
Adds grace tracking; update stale comments and include transport in controller keys. |
csi-driver/pkg/util/nvmerepair_test.go |
Tests grace-period deferral and eventual repair. |
csi-driver/pkg/util/initiator.go |
Reorders recovery and records connects; refresh post-repair inputs and add orchestration coverage. |
Review details
Suppressed comments (4)
csi-driver/pkg/util/initiator.go:1186
- This comment still says the repair pass runs later in the same tick, but
recoverPathsWithANAnow calls repair before both reconcile passes. Please describe this as protecting a later repair pass (including a subsequent monitor tick) so the comment does not document the old ordering.
// A controller that has just appeared has not attached its namespace yet.
// Record it so the repair pass later in this same tick defers its verdict
// instead of tearing down what this call created.
defaultRepairer.noteConnect(conn.Nqn, conn.IP, conn.Port, time.Now())
csi-driver/pkg/util/initiator.go:1384
- The relative order here is the core production fix, but the added regression test calls
healSubsystemdirectly and never exercisesrecoverPathsWithANA. A future move ofhealMonitoredVolumebelow these calls would therefore leave the test suite green while reintroducing the incident; add an orchestration seam/test (or equivalent integration assertion) that records and enforces repair-before-both-reconciles.
reconcileOptimizedPath(sbcClient, nodeInfo, devicePath, optConn, activeOpt, ctrlLossTmo)
reconcileNonOptimizedPaths(sbcClient, nodeInfo, devicePath, nonOptConns, activeNonOpt, ctrlLossTmo)
csi-driver/pkg/util/nvmerepair.go:534
- Expired entries are pruned only when
noteConnectruns. After the last successful connect, this process-wide map retains endpoints for every old NQN indefinitely; repeated volume attach/detach cycles can therefore grow the node plugin's memory. Prune expired entries while holding the lock insettling(or otherwise give these timestamps a bounded lifecycle).
now := r.now()
r.mu.Lock()
defer r.mu.Unlock()
var longest time.Duration
for _, ctrl := range d.Controllers {
csi-driver/pkg/util/nvmerepair.go:504
- This new comment ends with an incomplete sentence, so it does not clearly state what callers no longer need to remember.
// monitor's reconcile from having to remember to.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
noctarius
force-pushed
the
fix/nvmerepair-connect-race
branch
from
September 13, 2026 10:50
831a709 to
14340c5
Compare
The connection monitor ran its two reconcile passes and then the repair pass, so the repair diagnosed controllers the reconcile had created microseconds earlier. A controller that has just connected has not attached its namespace to the head yet, which is indistinguishable from one that never will, so the repair tore down the path it had just made and left the reconnect to "the next reconcile" -- a reconcile that had already run, since nothing follows the repair pass. k8s_native_rapid_failover_no_gap 2026-09-12, lvol 9ab68184 on worker-3: reconcileNonOptimizedPaths connected 192.168.10.12 at 05:10:42.422 and the repair tore it down at 05:10:42.519, 97ms later. The next tick never restored it -- the namespace head went away and took the per-volume monitor with it -- so the path came back 68 minutes later. The volume ran at 2 of 3 paths, and nine seconds after the teardown its primary and secondary both went down: a write blocked 41s against fio's 40s max_latency and failed the run at iteration 30. Across that run the six node plugins tore down and deferred 1,670 times, every one of them a redundancy hole until the following tick. Run the repair first. Whatever it tears down, the two reconcile passes below connect again in the same tick, through the connect path that already owns the reachability and node-online checks -- so the teardown becomes a repair rather than a hole, and no blocking work is added to a loop that walks every device on the node in sequence. Subtract the teardown from the path snapshot before those passes run. activePaths is what the monitor captured before recoverPathsWithANA was called, and missingEndpoints counts an endpoint as attached on presence alone, regardless of state: a controller just torn down would still look attached, its connect would be skipped, and the reorder would buy nothing. healMonitoredVolume returns the endpoints it removed and withoutEndpoints takes them out. The ordering closes the race within a tick. defaultConnectGrace (5s) closes it across ticks, for a controller connected by the previous pass that is still attaching: a defect whose teardown set contains one is deferred, with the reason reported like any other policy refusal, and repaired once the window passes. The value has to clear one monitor tick (monitorBaseInterval plus monitorJitter) or the skip is a no-op; the observed false verdict came 97ms after connect; and the cost of waiting is a genuinely broken path repaired 5s later against a 60s ctrl_loss_tmo. The controller's age is the kernel's own answer rather than bookkeeping this driver keeps. An NVMe controller publishes no "connected at" attribute -- checked against a live 5.14 node, which carries only address, cntlid, state, transport and friends -- but kernfs stamps the controller's sysfs directory when it creates it, and does not disturb that stamp when the namespaces attach underneath. On that node the mtime landed 2ms before `nvme connect` returned and was unchanged 81s later with the volume mounted and in use. nvme.Controller.CreatedAt carries it. Reading it beats recording connects here: it covers a connect this driver did not make, and it survives a node plugin restart, which a map in this process does not. It also needs no exception for the "already connected" path, where nothing is created and refreshing an age would defer forever the repair of a controller stuck contributing no path -- the spin nvmerepair exists to break. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noctarius
force-pushed
the
fix/nvmerepair-connect-race
branch
from
September 13, 2026 10:54
14340c5 to
e21e03f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The connection monitor ran its two reconcile passes and then the repair pass, so the repair diagnosed controllers the reconcile had created microseconds earlier. A controller that has just connected has not attached its namespace to the head yet, which is indistinguishable from one that never will — so the repair tore down the path it had just made, and deferred the reconnect to "the next reconcile", a reconcile that had already run.
What it cost
k8s_native_rapid_failover_no_gap2026-09-12, lvol9ab68184on worker-3:The path came back at 06:19 — 68 minutes later, because the namespace head went away at 05:11:34 and took the per-volume monitor with it. The volume ran at 2 of 3 paths, and nine seconds after the teardown its primary and secondary both went down. A write blocked 41s against fio's
max_latency=40sand failed the run at iteration 30.Across that 9-hour run the six node plugins tore down and deferred 1,670 times — every one a redundancy hole until the following tick. Worst-case latency for all other volumes sits on a hard ~10s ceiling (18 pods at 9,995–10,013 ms); this was the only sample above 20s.
The fix
Run the repair first. Whatever it tears down, the two reconcile passes below connect again in the same tick, through the connect path that already owns the reachability and node-online checks. The teardown becomes a repair rather than a hole, and no blocking work is added to a loop that walks every device on the node in sequence —
nvme connectcosts up to 40s per call against a target that is down.defaultConnectGrace(5s) closes the same race across ticks, for a controller connected by the previous pass that is still attaching. A defect whose teardown set contains one is deferred — reported as askippedreason like any other policy refusal — and repaired once the window passes.Why 5s: it has to clear one monitor tick (
monitorBaseInterval+monitorJitter) or the skip is a no-op; the observed false verdict came 97 ms after connect, so 5s is ~50× the normal attach; and the cost of waiting is a genuinely broken path repaired 5s later against a 60sctrl_loss_tmo.Subtract the teardown from the path snapshot.
activePathsis what the monitor captured beforerecoverPathsWithANAran, andmissingEndpointscounts an endpoint as attached on presence alone regardless of state — so without this a controller just torn down still looks attached, its connect is skipped, and the reorder buys nothing.healMonitoredVolumereturns the endpoints it removed andwithoutEndpointstakes them out. (Caught by Copilot review; it was a real defect.)The age comes from the kernel, not from bookkeeping
An NVMe controller publishes no
connected atattribute — checked against a live 5.14 node, which carries onlyaddress cntlid cntrltype ctrl_loss_tmo dctype ... state subsysnqn transport. But kernfs stamps the controller's sysfs directory when it creates it, and does not disturb that stamp when namespaces attach underneath. Measured on that node during a realNodeStageVolume:nvme010:44:15.04877010:44:15.046347nvme110:44:15.09766810:44:15.095348The mtime precedes the command's own return by ~2 ms, and both were unchanged 81 s later with the namespace child present and the volume mounted and in use.
nvme.Controller.CreatedAtnow carries it.Reading the kernel's answer beats recording connects in this process: it covers a connect this driver did not make, and it survives a node-plugin restart, which an in-process map does not. It also needs no exception for the
"already connected"path, where nothing is created and refreshing an age would defer forever the repair of a controller stuck contributing no path — the spinnvmerepairexists to break.Testing
TestHealSubsystem_LeavesAJustConnectedControllerAlone, markedRegression: 2026-09-13-nvmerepair-tears-down-its-own-connect. Verified red on the unfixed tree for the right reason (tore down [nvme1]; a controller connected 97ms ago must be left to attach), and re-verified red after the rewrite by stubbing the check out. It also asserts the grace defers rather than abandons: once the window passes, a controller that still contributes nothing is still repaired.TestWithoutEndpoints_RepairedPathIsSeenAsMissingAgainpins the snapshot subtraction — red on the unfixed tree withmissing = [], want only the torn-down 10.0.0.2.TestScanControllers_CreatedAtIsTheDirectoryMtimepins the atlas-lib side: the scannedCreatedAtis the directory's mtime, and an unreadable one stays zero so the policy layer can tell "young" from "unknown".go test -race -count=1 -shuffle=on ./pkg/util/,make -C csi-driver unit-testandmake -C atlas-lib testall green.Reviewer notes
recoverPathsWithANAneeds a live control-plane client and real NVMe devices, so the call order is enforced by construction and by the comment. Its two consequences are pinned — the grace and the snapshot subtraction — but the honest verification for the ordering is the next rapid-failover run. The snapshot defect Copilot found lived precisely in that gap, so treat it as a known weak spot in this change.test-plan-drain-remove.mdandtest-plan-storagenode-ops.md). Happy to add a minimal one here or file it againstmain.promoting first_secand never promotes the tertiary when the acting secondary is already offline. Tracked separately.🤖 Generated with Claude Code