Skip to content

Commit 8dac51c

Browse files
committed
fix: force-kill orphaned iSCSI sessions during volume staging
When a pod using an iSCSI volume is force-deleted, the kernel iSCSI session can linger in "blocked" or "transport-offline" state. When a new pod tries to mount the same volume, NodeStageVolume either reuses the broken session or fails to login because the old session blocks it. Fix: before staging, detect orphaned sessions (by device state or by IQN) and force-kill them by setting replacement_timeout=0 so the kernel releases the session immediately, then logout and re-login. This is safe because NodeStageVolume is only called for new pods — the old pod is already deleted, so the session is definitively orphaned. Running pods with active sessions are never affected. Fixes flaky E2E tests: failover, crash simulation, RWOP, StatefulSet.
1 parent d39705b commit 8dac51c

1 file changed

Lines changed: 67 additions & 10 deletions

File tree

pkg/driver/node_iscsi.go

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
362419
func (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

Comments
 (0)