@@ -60,16 +60,97 @@ func (s *NodeService) healthMonitorLoop() {
6060}
6161
6262// runHealthCheck performs a single pass of health checking and recovery.
63+ // It first tries to recover storage sessions (iSCSI re-login, NVMe controller reset).
64+ // If devices are completely gone and unrecoverable, it force-unmounts the staging
65+ // paths so kubelet detects the broken state and triggers re-staging.
6366func (s * NodeService ) runHealthCheck () {
6467 ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Minute )
6568 defer cancel ()
6669
70+ // Phase 1: Try to recover transport-level sessions
6771 iscsiRecovered := s .recoverISCSISessions (ctx )
6872 nvmeRecovered := s .recoverNVMeOFConnections (ctx )
6973
7074 if iscsiRecovered > 0 || nvmeRecovered > 0 {
7175 klog .Infof ("Health monitor recovered: iSCSI=%d, NVMe-oF=%d" , iscsiRecovered , nvmeRecovered )
7276 }
77+
78+ // Phase 2: Force-unmount staging paths for volumes whose devices are completely gone.
79+ // This allows kubelet to detect the broken mount and trigger NodeUnstageVolume +
80+ // NodeStageVolume, which will establish a fresh connection.
81+ s .unmountBrokenStagingPaths (ctx )
82+ }
83+
84+ // unmountBrokenStagingPaths finds staging mounts where the source block device
85+ // no longer exists and force-unmounts them so kubelet can re-stage.
86+ func (s * NodeService ) unmountBrokenStagingPaths (ctx context.Context ) {
87+ // Use findmnt to discover all mounts under our CSI staging directory
88+ findmntCtx , cancel := context .WithTimeout (ctx , 10 * time .Second )
89+ defer cancel ()
90+
91+ cmd := exec .CommandContext (findmntCtx , "findmnt" , "-n" , "-o" , "SOURCE,TARGET" , "-t" , "ext4,xfs" , "--raw" )
92+ output , err := cmd .CombinedOutput ()
93+ if err != nil {
94+ klog .V (4 ).Infof ("findmnt for staging path check: %v" , err )
95+ return
96+ }
97+
98+ // Filter for our CSI driver's staging paths
99+ const stagingPathMarker = "/plugins/kubernetes.io/csi/nasty.csi.io/"
100+ for _ , line := range strings .Split (string (output ), "\n " ) {
101+ if ! strings .Contains (line , stagingPathMarker ) {
102+ continue
103+ }
104+
105+ fields := strings .Fields (line )
106+ if len (fields ) < 2 {
107+ continue
108+ }
109+
110+ source := fields [0 ]
111+ target := fields [1 ]
112+
113+ // Check if the source device still exists
114+ if _ , statErr := os .Stat (source ); statErr == nil {
115+ // Device exists — check if it's actually accessible
116+ if isDeviceAccessible (ctx , source ) {
117+ continue // Healthy
118+ }
119+ klog .Warningf ("Device %s exists but is inaccessible (staging: %s)" , source , target )
120+ } else {
121+ klog .Warningf ("Device %s no longer exists (staging: %s)" , source , target )
122+ }
123+
124+ // Device is gone or inaccessible — force lazy unmount the staging path.
125+ // This makes kubelet's mount check fail, triggering the full re-stage flow.
126+ klog .Infof ("Force-unmounting broken staging path %s (source device: %s)" , target , source )
127+ umountCtx , umountCancel := context .WithTimeout (ctx , 15 * time .Second )
128+ umountCmd := exec .CommandContext (umountCtx , "umount" , "-l" , target )
129+ if umountOutput , umountErr := umountCmd .CombinedOutput (); umountErr != nil {
130+ klog .Errorf ("Failed to unmount broken staging path %s: %v (%s)" , target , umountErr , strings .TrimSpace (string (umountOutput )))
131+ } else {
132+ klog .Infof ("Successfully unmounted broken staging path %s — kubelet will re-stage" , target )
133+ }
134+ umountCancel ()
135+ }
136+ }
137+
138+ // isDeviceAccessible checks if a block device can be opened (not just stat'd).
139+ // A device that exists in /dev but has a dead transport will fail to open.
140+ func isDeviceAccessible (ctx context.Context , devicePath string ) bool {
141+ // Use blockdev --getsize64 as a quick accessibility check
142+ checkCtx , cancel := context .WithTimeout (ctx , 3 * time .Second )
143+ defer cancel ()
144+
145+ cmd := exec .CommandContext (checkCtx , "blockdev" , "--getsize64" , devicePath )
146+ output , err := cmd .CombinedOutput ()
147+ if err != nil {
148+ return false
149+ }
150+
151+ // If size is 0, device is likely broken
152+ size := strings .TrimSpace (string (output ))
153+ return size != "" && size != "0"
73154}
74155
75156// recoverVolumes is called asynchronously after the WebSocket connection to
0 commit comments