@@ -81,11 +81,11 @@ func (s *NodeService) stageISCSIVolume(ctx context.Context, req *csi.NodeStageVo
8181 klog .V (4 ).Infof ("Staging iSCSI volume %s (block mode: %v): server=%s:%s, IQN=%s, LUN=%d, dataset=%s" ,
8282 volumeID , isBlockVolume , params .server , params .port , params .iqn , params .lun , datasetName )
8383
84- // Try to reuse existing connection (idempotency)
84+ // Try to reuse existing connection (idempotency), or clean up orphaned sessions.
85+ // NodeStageVolume is only called for new pods, so any existing session for this
86+ // IQN belongs to a deleted pod and is safe to force-kill.
8587 if devicePath , findErr := s .findISCSIDevice (ctx , params ); findErr == nil && devicePath != "" {
8688 // Verify the iSCSI session is healthy before reusing.
87- // After a NAS reboot, the device may still exist in sysfs but the session
88- // is in "transport-offline" state, causing I/O errors.
8989 sessionState , stateErr := getISCSISessionState (ctx , devicePath )
9090 switch {
9191 case stateErr != nil :
@@ -94,15 +94,18 @@ func (s *NodeService) stageISCSIVolume(ctx context.Context, req *csi.NodeStageVo
9494 klog .V (4 ).Infof ("iSCSI device already connected at %s (session healthy) - reusing existing connection" , devicePath )
9595 return s .stageISCSIDevice (ctx , volumeID , devicePath , stagingTargetPath , volumeCapability , isBlockVolume , volumeContext )
9696 default :
97- klog .Warningf ("iSCSI session for %s is %q (not LOGGED_IN) - forcing logout and re-login " , devicePath , sessionState )
97+ klog .Warningf ("iSCSI session for %s is %q (not LOGGED_IN) - forcing cleanup " , devicePath , sessionState )
9898 }
9999
100- // Session is stale — logout before attempting a fresh login
101- if logoutErr := s .logoutISCSITarget (ctx , params ); logoutErr != nil {
102- klog .Warningf ("Failed to logout stale iSCSI session for %s: %v (continuing with fresh login)" , params .iqn , logoutErr )
103- }
104- // Small delay for kernel to clean up the stale SCSI device
105- time .Sleep (2 * time .Second )
100+ // Session is stale (orphaned from a deleted pod). Force-kill it by
101+ // setting replacement_timeout=0 so the kernel gives up immediately,
102+ // then logout and delete the node record.
103+ s .forceKillISCSISession (params )
104+ } else {
105+ // No device found via session -P 3, but there might still be an orphaned
106+ // session in recovery state (e.g., after a force-deleted pod where the
107+ // device has already disappeared from sysfs). Check by IQN and kill it.
108+ s .forceKillISCSISessionByIQN (params )
106109 }
107110
108111 // Check if iscsiadm is installed
@@ -358,6 +361,60 @@ func (s *NodeService) logoutISCSITarget(_ context.Context, params *iscsiConnecti
358361 return nil
359362}
360363
364+ // forceKillISCSISession forces an immediate teardown of an orphaned iSCSI session.
365+ // It sets replacement_timeout=0 so the kernel stops recovery immediately, then
366+ // logs out and deletes the node record. This is only safe for sessions belonging
367+ // to deleted pods — never for sessions serving running workloads.
368+ //
369+ //nolint:contextcheck // intentionally uses Background context
370+ func (s * NodeService ) forceKillISCSISession (params * iscsiConnectionParams ) {
371+ klog .Infof ("Force-killing orphaned iSCSI session for %s" , params .iqn )
372+
373+ // Set replacement_timeout=0 to make the kernel give up recovery immediately.
374+ // Without this, logout hangs until the original replacement_timeout (120s+) expires.
375+ toCtx , toCancel := context .WithTimeout (context .Background (), 5 * time .Second )
376+ defer toCancel ()
377+ toCmd := iscsiadmCmd (toCtx , "-m" , "node" , "-T" , params .iqn ,
378+ "--op" , "update" , "-n" , "node.session.timeo.replacement_timeout" , "-v" , "0" )
379+ if out , err := toCmd .CombinedOutput (); err != nil {
380+ klog .V (4 ).Infof ("Failed to set replacement_timeout=0 for %s (may not exist): %v, output: %s" , params .iqn , err , strings .TrimSpace (string (out )))
381+ }
382+
383+ // Brief pause for the kernel to process the timeout change and fail the session
384+ time .Sleep (1 * time .Second )
385+
386+ // Now logout — should complete immediately since the session is no longer recovering
387+ if logoutErr := s .logoutISCSITarget (context .Background (), params ); logoutErr != nil {
388+ klog .Warningf ("Failed to logout orphaned iSCSI session for %s: %v (continuing with fresh login)" , params .iqn , logoutErr )
389+ }
390+
391+ // Wait for kernel to clean up the SCSI device
392+ time .Sleep (2 * time .Second )
393+ }
394+
395+ // forceKillISCSISessionByIQN checks if any iSCSI session exists for the given IQN
396+ // and force-kills it. This handles the case where findISCSIDevice returns nothing
397+ // (device already gone from sysfs) but a session is still lingering in recovery state.
398+ //
399+ //nolint:contextcheck // intentionally uses Background context
400+ func (s * NodeService ) forceKillISCSISessionByIQN (params * iscsiConnectionParams ) {
401+ // Check if any session exists for this IQN
402+ checkCtx , checkCancel := context .WithTimeout (context .Background (), 5 * time .Second )
403+ defer checkCancel ()
404+ cmd := iscsiadmCmd (checkCtx , "-m" , "session" )
405+ output , err := cmd .CombinedOutput ()
406+ if err != nil {
407+ return // No sessions at all
408+ }
409+
410+ if ! strings .Contains (string (output ), params .iqn ) {
411+ return // No session for this IQN
412+ }
413+
414+ klog .Infof ("Found orphaned iSCSI session for %s (device already gone) — force-killing" , params .iqn )
415+ s .forceKillISCSISession (params )
416+ }
417+
361418// findISCSIDevice finds the device path for an iSCSI LUN.
362419func (s * NodeService ) findISCSIDevice (ctx context.Context , params * iscsiConnectionParams ) (string , error ) {
363420 // Query active sessions with detail level 3 to see attached devices
0 commit comments