csi: drive reconnect/guardian detection from the guardian's registry, not nvme list#338
Open
boddumanohar wants to merge 1 commit into
Open
csi: drive reconnect/guardian detection from the guardian's registry, not nvme list#338boddumanohar wants to merge 1 commit into
boddumanohar wants to merge 1 commit into
Conversation
… not nvme list Investigating a flaky SPDKCSI-RECONNECT-GUARDIAN e2e failure (the guardian never restarted a pod after total NVMe-oF path loss) traced the root cause to reconnectSubsystems (the MonitorConnection poll loop): it discovered connections by scanning `nvme list`/`nvme list-subsys` and diffing successive snapshots against a package-level history map. That approach can only ever detect a lost connection if some earlier poll happened to observe it as present first. A volume that connects and loses all its paths faster than one poll interval (~3s+jitter) — as happened in the failing run, where the test's induced disconnect landed ~2.3s after the volume connected — was never recorded as present, so its disappearance had nothing to diff against and was silently missed forever. This isn't limited to that one timing window: any gap that beats the poll cadence has the same failure mode (e.g. the csi-node pod itself restarting and losing its in-memory maps). Replace the discover-then-diff model with one driven by the guardian's own registry: - The guardian already tracks every lvol it has published on this node, synchronously, at NodeStageVolume/NodePublishVolume/NodeUnpublishVolume time (RegisterPublish/RegisterUnpublishByTargetPath) — with no polling involved and no possible race window. - That registry now also records the exact subsystem NQN each lvol was published under (the real value handed to us in the volume context — the same string used for the "nvme connect" that established it, not reconstructed from clusterID+lvolID, which is wrong whenever a subsystem is shared across lvols). - reconnectSubsystems now iterates the guardian's tracked lvols (Guardian.TrackedLvols()) and checks each one's live state directly against a single `nvme list-subsys` snapshot per poll: "should be connected" (guardian registry, race-free) vs. "is connected right now" (one live snapshot) — no historical diffing needed. A lvol missing from that snapshot is immediately actionable, on the very first poll after the loss, regardless of how quickly it happened. - This makes the isManagedLvol PV/PVC ownership filter (added previously to keep the old discovery loop from touching benchmark/ foreign volumes) redundant and removes it: the guardian's registry only ever contains lvols this driver's own NodePublishVolume handled, so foreign/benchmark volumes were never eligible to begin with. - Removes the now-dead devicePath parameter threaded through recoverPathsWithANA/reconcileOptimizedPath/reconcileNonOptimizedPaths (already unused there, flagged by the linter) and the device-path/ sysfs-uuid discovery machinery (getNVMeDeviceInfos, logicalVolumeIdByDevicePath, devicePresentMap/deviceToLvolIDMap) that existed only to support the old discovery loop. Known trade-off: reconnectSubsystems's proactive path-degradation recovery (independent of guardian restarts) now only covers lvols the guardian is tracking, which starts at NodePublishVolume time (needs a per-pod target path to derive a pod UID) rather than NodeStageVolume time (when the device actually connects, but with no pod context yet). In practice this window is milliseconds to low seconds, since kubelet calls Stage then Publish back-to-back for the one pod using a volume, and it doesn't affect guardian-restart behavior at all (there's no pod running yet to restart during that window either way). It could be closed by also registering with the guardian at NodeStageVolume time (clusterID/lvolID/NQN only, no pod UID yet) if full parity with the old recovery scope is wanted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks
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.
Summary
Alternative to #337, for the same root cause:
SPDKCSI-RECONNECT-GUARDIANe2e flakiness where the guardian never restarted a pod after total NVMe-oF path loss.Investigation (k8s pod logs, SPDK target logs, and the raw ginkgo run — simplyBlockDeployGCP run 29477941151) pinned the failing run's timeline precisely:
07:25:42.147–07:25:42.284— volume staged and connected07:25:44.423— test issuesnvme disconnect -n <NQN>to induce total path loss07:25:44.531–.688— SPDK target confirms both paths' queues torn downThe device was alive for only ~2.3 seconds before losing all paths — shorter than the csi-node's
MonitorConnectionpoll cadence (3s base + up to 500ms jitter). The loss was real and permanent, and the storage cluster was healthy throughout — not an infra flake.Root cause:
reconnectSubsystems(theMonitorConnectionpoll loop) discovered connections by scanningnvme list/nvme list-subsysand diffing successive snapshots against a package-level history map. That can only ever detect a lost connection if some earlier poll happened to observe it as present first. A volume that connects and loses all its paths faster than one poll interval was never recorded as present, so its disappearance had nothing to diff against and was silently missed forever. This isn't limited to this one timing window either — any gap that beats the poll cadence has the same failure mode (e.g. the csi-node pod itself restarting and losing its in-memory maps).This PR's approach
Rather than patching the specific race (see #337 for that narrower fix), this replaces the discover-then-diff model entirely with one driven by the guardian's own registry:
NodeStageVolume/NodePublishVolume/NodeUnpublishVolumetime (RegisterPublish/RegisterUnpublishByTargetPath) — no polling involved, no race window.reconnectSubsystemsnow iterates the guardian's tracked lvols (Guardian.TrackedLvols()) and checks each one's live state directly against a singlenvme list-subsyssnapshot per poll: "should be connected" (guardian registry, race-free) vs. "is connected right now" (one live snapshot) — no historical diffing needed at all. A lvol missing from that snapshot is immediately actionable on the very first poll after the loss, regardless of how quickly it happened.isManagedLvolPV/PVC ownership filter (added previously to keep the old discovery loop from touching benchmark/foreign volumes) redundant, and removes it — the guardian's registry only ever contains lvols this driver's ownNodePublishVolumehandled.devicePathparameter threaded throughrecoverPathsWithANA/reconcileOptimizedPath/reconcileNonOptimizedPaths(already unused, flagged by the linter) and the device-path/sysfs-uuid discovery machinery (getNVMeDeviceInfos,logicalVolumeIdByDevicePath,devicePresentMap/deviceToLvolIDMap) that existed only to support the old discovery loop.Trade-off vs. #337
This is a materially larger change than #337, and it's not free of downsides:
reconnectSubsystems's proactive path-degradation recovery (independent of guardian restarts) now only covers lvols the guardian is tracking, which starts atNodePublishVolumetime (needs a per-pod target path to derive a pod UID) rather thanNodeStageVolumetime (when the device actually connects, but with no pod context yet). In practice that window is milliseconds to low seconds — kubelet calls Stage then Publish back-to-back for the one pod using a volume — and it doesn't affect guardian-restart behavior at all (no pod exists yet to restart during that window either way, in old or new design). It could be closed by also registering with the guardian atNodeStageVolumetime (clusterID/lvolID/NQN only, no pod UID yet) if full parity with the old recovery scope is wanted — happy to add that if this approach is preferred.Test plan
go build ./...go test ./pkg/... ./cmd/...golangci-lint run ./pkg/...— no new issuesguardian_test.gocoveringTrackedLvols(): reflectsRegisterPublish/RegisterUnpublishByTargetPath, skips entries missing an NQN, and reportsAlreadyBrokenafterMarkBrokenLvol(removedreconnect_test.go, which tested the now-removedisManagedLvol)SPDKCSI-RECONNECT-GUARDIANe2e a few times to confirm improved reliability (no existing unit test harness drivesreconnectSubsystemsend-to-end, since it shells out to realnvmestate)🤖 Generated with Claude Code