Conversation
…ing fallback
During pod teardown or volume unstage/unpublish operations (NodeUnstageVolume,
NodeUnpublishVolume), if the Lustre filesystem experiences network partitions,
dropped RPCs, or unresponsive backend targets (MDS/OSTs), the synchronous
sys_umount syscall (ll_put_super) blocks in uninterruptible kernel sleep (D state)
while attempting to flush dirty pages (cl_sync_io_wait), cancel locks (ldlm_cli_cancel),
or send disconnect RPCs (obd_disconnect).
Because s.mounter.Unmount(target) internally executed exec.Command("umount", target)
without a timeout or context deadline, the gRPC handler goroutine blocked indefinitely,
preventing defer s.volumeLocks.Release(target) from executing and causing all subsequent
Kubelet retry attempts to fail with "VolumeOperationAlreadyExists".
This change:
1. Implements a three-tier cascading unmount fallback in pkg/csi_driver/unmount.go
(Phase 1 standard umount with 15s timeout -> Phase 2 force umount -f with 10s
timeout -> Phase 3 lazy umount -l with 5s timeout).
2. Propagates ctx context.Context across NodeStageVolume, NodeUnstageVolume,
NodePublishVolume, NodeUnpublishVolume, and IAM teardown paths.
3. Guarantees bounded execution (<= 30s) and deterministic volumeLock release.
4. Adds comprehensive unit tests for cascading fallback and lock release.
There was a problem hiding this comment.
Code Review
This pull request introduces a cascading unmount strategy for Lustre mounts in the CSI driver, falling back from standard to force and lazy unmounts, and adds corresponding unit tests. The code review feedback highlights several critical improvements: preventing indefinite hangs during uninterruptible sleep (D state) by implementing a non-blocking command execution, safely defaulting to unmounting when mount point checks return an error, and using context.Background() instead of the request context for cleanup operations to ensure they run even if the request context is cancelled or timed out.
…and cleanup contexts
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a cascading fallback unmount strategy (standard, force, and lazy unmounts) with bounded timeouts to prevent mount points from getting permanently wedged, along with comprehensive unit tests. The review feedback highlights a critical correctness issue where passing a cancellable context to the unmount phases can bypass the fallback mechanism entirely upon timeout or cancellation. It is recommended to use context.Background() for these timeout contexts and simplify the signatures of unmountPath and related helper functions by removing the unused context parameters. Additionally, a defensive check should be added to execUmount to prevent a potential out-of-bounds panic when arguments are empty.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a cascading unmount strategy in the CSI driver to handle standard, force, and lazy unmounts with bounded timeouts, preventing blocked operations. It adds a new unmount.go file with the unmountPath logic, along with corresponding unit tests, and refactors node.go to use this helper. The review feedback suggests using exec.CommandContext with cmd.Run() for idiomatic command execution, improving the robustness of the *mount.FakeMounter type assertion in tests, making the isNotMounted check case-insensitive, and passing error objects directly to log formatters instead of calling .Error() explicitly.
| cmd := exec.Command("umount", args...) | ||
| var buf bytes.Buffer | ||
| cmd.Stdout = &buf | ||
| cmd.Stderr = &buf | ||
|
|
||
| if err := cmd.Start(); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| done := make(chan error, 1) | ||
| go func() { | ||
| done <- cmd.Wait() | ||
| }() | ||
|
|
||
| select { | ||
| case err := <-done: | ||
| return buf.Bytes(), err | ||
| case <-ctx.Done(): | ||
| if cmd.Process != nil { | ||
| _ = cmd.Process.Kill() | ||
| } | ||
| return nil, ctx.Err() | ||
| } |
There was a problem hiding this comment.
Using exec.CommandContext with cmd.Run() is the standard and idiomatic way to execute external commands with a context timeout in Go. It automatically handles process termination on context cancellation/timeout, eliminating the need for manual goroutine management, channel coordination, and process killing.
cmd := exec.CommandContext(ctx, "umount", args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
return buf.Bytes(), err
}
return buf.Bytes(), nilThere was a problem hiding this comment.
We intentionally use cmd.Start() with a background cmd.Wait() and select on ctx.Done() because cmd.Run() blocks in wait4 if the child umount process enters TASK_UNINTERRUPTIBLE (D) sleep in the kernel. Standard cmd.Run() cannot unblock or return until the kernel D-state wakes up, whereas selecting on ctx.Done() allows the CSI driver to return immediately on timeout.
| } | ||
|
|
||
| // In test environments using FakeMounter, delegate to FakeMounter.Unmount if no custom unmountExec is set. | ||
| if fm, ok := s.mounter.(*mount.FakeMounter); ok { |
There was a problem hiding this comment.
This type assertion only succeeds if s.mounter is exactly of type *mount.FakeMounter. If the test environment wraps or embeds *mount.FakeMounter (such as fakeMounter defined in node_test.go), this check will fail, causing the code to fall back to executing the real umount command on the host. To make this robust and support wrapped mounters in tests without referencing test-only types in production code, consider checking if the mounter implements an unwrapper interface or using reflection to extract the embedded *mount.FakeMounter.
There was a problem hiding this comment.
Resolved in commit 1bb96db. Added extractFakeMounter which uses reflection to inspect structs embedding *mount.FakeMounter.
| func isNotMounted(err error, output []byte) bool { | ||
| if err != nil && strings.Contains(err.Error(), errNotMounted) { | ||
| return true | ||
| } | ||
| if strings.Contains(string(output), errNotMounted) { | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
To make the isNotMounted check more robust against potential variations in error messages across different OS or kernel versions, consider performing a case-insensitive comparison by converting the error and output strings to lowercase before checking for errNotMounted.
| func isNotMounted(err error, output []byte) bool { | |
| if err != nil && strings.Contains(err.Error(), errNotMounted) { | |
| return true | |
| } | |
| if strings.Contains(string(output), errNotMounted) { | |
| return true | |
| } | |
| return false | |
| } | |
| func isNotMounted(err error, output []byte) bool { | |
| if err != nil && strings.Contains(strings.ToLower(err.Error()), errNotMounted) { | |
| return true | |
| } | |
| if strings.Contains(strings.ToLower(string(output)), errNotMounted) { | |
| return true | |
| } | |
| return false | |
| } |
There was a problem hiding this comment.
Resolved in commit 1bb96db. Made isNotMounted check case-insensitive using strings.ToLower.
| klog.Errorf("Mount %q failed on node %s, cleaning up", target, nodeName) | ||
| if unmntErr := mount.CleanupMountPoint(target, s.mounter, false /* extensiveMountPointCheck */); unmntErr != nil { | ||
| if unmntErr := s.unmountPath(target); unmntErr != nil { | ||
| klog.Errorf("Unmount %q failed on node %s: %v", target, nodeName, unmntErr.Error()) |
There was a problem hiding this comment.
Passing unmntErr directly to %v is more idiomatic in Go and avoids calling .Error() explicitly. This is also consistent with how errors are logged elsewhere in this file (e.g., line 410).
| klog.Errorf("Unmount %q failed on node %s: %v", target, nodeName, unmntErr.Error()) | |
| klog.Errorf("Unmount %q failed on node %s: %v", target, nodeName, unmntErr) |
There was a problem hiding this comment.
Resolved in commit 1bb96db. Passed unmntErr directly to %v formatter.
| klog.V(5).Infof("setVolumeOwnershipTopLevel failed for volume %q, path %q, fsGroup %q, cleaning up mount point on node %s", volumeID, targetPath, fsGroup, nodeName) | ||
| if unmntErr := mount.CleanupMountPoint(targetPath, s.mounter, false /* extensiveMountPointCheck */); unmntErr != nil { | ||
| if unmntErr := s.unmountPath(targetPath); unmntErr != nil { | ||
| klog.Errorf("Unmount %q failed on node %s: %v", targetPath, nodeName, unmntErr.Error()) |
There was a problem hiding this comment.
Passing unmntErr directly to %v is more idiomatic in Go and avoids calling .Error() explicitly. This is also consistent with how errors are logged elsewhere in this file (e.g., line 410).
| klog.Errorf("Unmount %q failed on node %s: %v", targetPath, nodeName, unmntErr.Error()) | |
| klog.Errorf("Unmount %q failed on node %s: %v", targetPath, nodeName, unmntErr) |
There was a problem hiding this comment.
Resolved in commit 1bb96db. Passed unmntErr directly to %v formatter.
| klog.Errorf("Mount %q failed on node %s for principal %s, cleaning up", globalMountPath, nodeName, principal) | ||
| if unmntErr := mount.CleanupMountPoint(globalMountPath, s.mounter, false /* extensiveMountPointCheck */); unmntErr != nil { | ||
| if unmntErr := s.unmountPath(globalMountPath); unmntErr != nil { | ||
| klog.Errorf("Unmount %q failed on node %s for principal %s: %v", globalMountPath, nodeName, principal, unmntErr.Error()) |
There was a problem hiding this comment.
Passing unmntErr directly to %v is more idiomatic in Go and avoids calling .Error() explicitly. This is also consistent with how errors are logged elsewhere in this file (e.g., line 410).
| klog.Errorf("Unmount %q failed on node %s for principal %s: %v", globalMountPath, nodeName, principal, unmntErr.Error()) | |
| klog.Errorf("Unmount %q failed on node %s for principal %s: %v", globalMountPath, nodeName, principal, unmntErr) |
There was a problem hiding this comment.
Resolved in commit 1bb96db. Passed unmntErr directly to %v formatter.
… case insensitivity, and log formatting
|
Closing in favor of a clean, squashed PR for review. |
Problem Statement
During pod teardown or volume unstage/unpublish operations (
NodeUnstageVolume,NodeUnpublishVolume), if the Lustre filesystem is experiencing network partitions, dropped RPCs, or unresponsive backend targets (MDS/OSTs), the synchronoussys_umountsyscall (ll_put_super) blocks in uninterruptible kernel sleep (TASK_UNINTERRUPTIBLE/Dstate) while attempting to flush dirty pages (cl_sync_io_wait), cancel locks (ldlm_cli_cancel), or send disconnect RPCs (obd_disconnect).Because
s.mounter.Unmount(target)internally executedexec.Command("umount", target)without a timeout or context deadline:defer s.volumeLocks.Release(target)was never reached.codes.Aborted: An operation with the given volume key <target> already exists.Design & Solution Reasoning
1. Three-Tier Cascading Unmount Fallback (
pkg/csi_driver/unmount.go)We introduce
unmountPath(ctx, target)which applies a bounded, cascading fallback strategy:exec.CommandContext(ctx, "umount", target)with a 15-second bounded timeout (DefaultUnmountTimeout).exec.CommandContext(ctx, "umount", "-f", target)with a 10-second bounded timeout (DefaultForceUnmountTimeout).MNT_DETACH): If Phase 2 also times out or fails, falls back toexec.CommandContext(ctx, "umount", "-l", target)with a 5-second bounded timeout (DefaultLazyUnmountTimeout).2. Guaranteed Lock Release & Subprocess Cleanup
exec.CommandContextguarantees that if a timeout occurs, Go sendsSIGKILLto theumountprocess rather than abandoning an orphaned goroutine.defer s.volumeLocks.Release(target)always executes and freeing the volume lock for future attempts.3. gRPC Context Propagation (
pkg/csi_driver/node.go)NodeStageVolume,NodeUnstageVolume,NodePublishVolume, andNodeUnpublishVolumeto accept and respectctx context.Context.cleanUpIAMReference,cleanUpIAMReferenceForKey).Upgrade Safety
mountPropagation: Bidirectional. Upgrading the CSI driver DaemonSet does not unmount active volumes or interrupt in-flight I/O.lustre-kmod-installerdetects that kernel modules are already loaded and exits immediately without reloading./var/lib/lustre/mounts/...) and survive pod restarts.Verification
pkg/csi_driver/unmount_test.go:volumeLocksrelease verification.