Skip to content

cephfs: avoid hanging os.Stat() in NodeGetVolumeStats after restart - #6418

Open
SanjalKatiyar wants to merge 1 commit into
ceph:develfrom
SanjalKatiyar:fix_stat_block_issue
Open

cephfs: avoid hanging os.Stat() in NodeGetVolumeStats after restart#6418
SanjalKatiyar wants to merge 1 commit into
ceph:develfrom
SanjalKatiyar:fix_stat_block_issue

Conversation

@SanjalKatiyar

@SanjalKatiyar SanjalKatiyar commented Jul 27, 2026

Copy link
Copy Markdown

If a node-plugin pod restarts while the backend (e.g. MDS) is down, NodeGetVolumeStats used to call os.Stat() on the target path immediately after (re)starting the health checker, before the checker had a chance to detect the outage. Since os.Stat() blocks in the kernel on an unresponsive mount, this held the VolumeLock forever and made every later call for that path fail with Aborted.

Return early with an "not yet available" condition instead of calling os.Stat() when the checker was just (re)started, for CephFS.

Issue:

Slow GRPC calls and connection refused errors when nodeplugin pod restarts while MDS is down.

Logs of openshift-storage.cephfs.csi.ceph.com-nodeplugin-<HASH> pod:
I0727 13:34:49.967224       1 cephcsi.go:213] Driver version: canary and Git version: a9058c2e1cb4f4c1e0b35a1c6525adc2d74edef8
I0727 13:34:49.967411       1 cephcsi.go:315] Initial PID limit is set to -1
I0727 13:34:49.967460       1 cephcsi.go:321] Reconfigured PID limit to -1 (max)
I0727 13:34:49.967652       1 cephcsi.go:267] Starting driver type: cephfs with name: openshift-storage.cephfs.csi.ceph.com
I0727 13:34:49.982637       1 volumemounter.go:81] loaded mounter: kernel
I0727 13:34:49.997980       1 volumemounter.go:92] loaded mounter: fuse
I0727 13:34:50.013335       1 mount_linux.go:323] Detected umount with safe 'not mounted' behavior
I0727 13:34:50.013624       1 server.go:123] listening for CSI-Addons requests on address: &net.UnixAddr{Name:"/csi/csi-addons.sock", Net:"unix"}
I0727 13:34:50.013671       1 server.go:131] Listening for connections on address: &net.UnixAddr{Name:"//csi/csi.sock", Net:"unix"}
I0727 13:35:17.235671       1 utils.go:361] ID: 1 GRPC call: /csi.v1.Node/NodeGetCapabilities
I0727 13:35:17.235701       1 utils.go:362] ID: 1 GRPC request: {}
I0727 13:35:17.235916       1 utils.go:368] ID: 1 GRPC response: {"capabilities":[{"rpc":{"type":"STAGE_UNSTAGE_VOLUME"}},{"rpc":{"type":"GET_VOLUME_STATS"}},{"rpc":{"type":"VOLUME_CONDITION"}},{"rpc":{"type":"SINGLE_NODE_MULTI_WRITER"}}]}
I0727 13:35:17.236688       1 utils.go:361] ID: 2 GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0727 13:35:17.236717       1 utils.go:362] ID: 2 GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:35:20.717444       1 utils.go:361] ID: 3 GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0727 13:35:20.717497       1 utils.go:362] ID: 3 GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.cephfs.csi.ceph.com/5c6d82102a843b63051ee6ec16343f5fc0fa5f1cedf5999667e1c9171981cdbe/globalmount"}
I0727 13:35:51.828972       1 utils.go:361] ID: 4 GRPC call: /csi.v1.Node/NodeGetCapabilities
I0727 13:35:51.828996       1 utils.go:362] ID: 4 GRPC request: {}
I0727 13:35:51.829059       1 utils.go:368] ID: 4 GRPC response: {"capabilities":[{"rpc":{"type":"STAGE_UNSTAGE_VOLUME"}},{"rpc":{"type":"GET_VOLUME_STATS"}},{"rpc":{"type":"VOLUME_CONDITION"}},{"rpc":{"type":"SINGLE_NODE_MULTI_WRITER"}}]}
I0727 13:35:51.829782       1 utils.go:361] ID: 5 GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0727 13:35:51.829816       1 utils.go:362] ID: 5 GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
I0727 13:37:47.237573       1 utils.go:394] ID: 2 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (2m29s)
I0727 13:37:47.237626       1 utils.go:396] ID: 2 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:38:17.235439       1 utils.go:394] ID: 2 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (2m59s)
I0727 13:38:17.235488       1 utils.go:396] ID: 2 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:38:21.830237       1 utils.go:394] ID: 5 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (2m29s)
I0727 13:38:21.830287       1 utils.go:396] ID: 5 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
I0727 13:38:40.220107       1 utils.go:361] ID: 6 GRPC call: /csi.v1.Node/NodeGetCapabilities
I0727 13:38:40.220136       1 utils.go:362] ID: 6 GRPC request: {}
I0727 13:38:40.220233       1 utils.go:368] ID: 6 GRPC response: {"capabilities":[{"rpc":{"type":"STAGE_UNSTAGE_VOLUME"}},{"rpc":{"type":"GET_VOLUME_STATS"}},{"rpc":{"type":"VOLUME_CONDITION"}},{"rpc":{"type":"SINGLE_NODE_MULTI_WRITER"}}]}
I0727 13:38:40.221030       1 utils.go:361] ID: 7 GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0727 13:38:40.221060       1 utils.go:362] ID: 7 GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
E0727 13:38:40.221086       1 utils.go:366] ID: 7 GRPC error: rpc error: code = Aborted desc = an operation with the given target path /var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount already exists
I0727 13:38:47.237391       1 utils.go:394] ID: 2 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (3m29s)
I0727 13:38:47.237448       1 utils.go:396] ID: 2 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:38:51.829778       1 utils.go:394] ID: 5 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (2m59s)
I0727 13:38:51.829830       1 utils.go:396] ID: 5 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
I0727 13:38:56.411916       1 utils.go:361] ID: 8 GRPC call: /csi.v1.Node/NodeGetCapabilities
I0727 13:38:56.411941       1 utils.go:362] ID: 8 GRPC request: {}
I0727 13:38:56.411997       1 utils.go:368] ID: 8 GRPC response: {"capabilities":[{"rpc":{"type":"STAGE_UNSTAGE_VOLUME"}},{"rpc":{"type":"GET_VOLUME_STATS"}},{"rpc":{"type":"VOLUME_CONDITION"}},{"rpc":{"type":"SINGLE_NODE_MULTI_WRITER"}}]}
I0727 13:38:56.412680       1 utils.go:361] ID: 9 GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0727 13:38:56.412708       1 utils.go:362] ID: 9 GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
E0727 13:38:56.412730       1 utils.go:366] ID: 9 GRPC error: rpc error: code = Aborted desc = an operation with the given target path /var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount already exists
I0727 13:39:17.236578       1 utils.go:394] ID: 2 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (3m59s)
I0727 13:39:17.236621       1 utils.go:396] ID: 2 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:39:21.830771       1 utils.go:394] ID: 5 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (3m29s)
I0727 13:39:21.830817       1 utils.go:396] ID: 5 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
I0727 13:39:47.236887       1 utils.go:394] ID: 2 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (4m29s)
I0727 13:39:47.236932       1 utils.go:396] ID: 2 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd","volume_path":"/var/lib/kubelet/pods/ca8be956-d2a4-47ed-a4dd-1167af26ebbc/volumes/kubernetes.io~csi/pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2/mount"}
I0727 13:39:51.830219       1 utils.go:394] ID: 5 Slow GRPC call /csi.v1.Node/NodeGetVolumeStats (3m59s)
I0727 13:39:51.830271       1 utils.go:396] ID: 5 Slow GRPC request: {"volume_id":"0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e","volume_path":"/var/lib/kubelet/pods/feaa0cb3-0142-4768-8d2b-77809a508db4/volumes/kubernetes.io~csi/pvc-85f15e58-94ce-4858-9842-19b97a76f042/mount"}
openshift-storage.cephfs.csi.ceph.com-nodeplugin-csi-addonft2qx logs:
2026-07-27T11:52:20.520Z	INFO	client	Probing CSI driver for readiness
2026-07-27T11:52:20.524Z	INFO	setup	The CSI-plugin does not have the CSI-Addons CONTROLLER_SERVICE capability, not running leader election
2026-07-27T11:52:20.708Z	INFO	setup	Starting volume condition reporter	{"driver": "openshift-storage.cephfs.csi.ceph.com"}
2026-07-27T11:52:20.916Z	INFO	csiaddonsnode	Starting watcher for CSIAddonsNode	{"name": "ip-10-0-63-53.ec2.internal-openshift-storage-daemonset-openshift-storage.cephfs.csi.ceph.com-nodeplugin-csi-addons"}
2026-07-27T11:52:21.508Z	INFO	server	Listening for CSI-Addons requests	{"address": "[::]:9071"}
2026-07-27T12:30:20.743Z	INFO	volume-condition	Persistent volume is healthy	{"pvName": "pvc-85f15e58-94ce-4858-9842-19b97a76f042", "message": "volume is in a healthy condition"}
2026-07-27T12:30:20.770Z	INFO	volume-condition	Persistent volume is healthy	{"pvName": "pvc-444f47a0-f2c2-4801-acbc-d12e632bb4c2", "message": "volume is in a healthy condition"}
2026-07-27T13:22:41.170Z	INFO	csiaddonsnode	Watcher exited gracefully, will be restarted soon	{"name": "ip-10-0-63-53.ec2.internal-openshift-storage-daemonset-openshift-storage.cephfs.csi.ceph.com-nodeplugin-csi-addons"}
2026-07-27T13:22:41.199Z	INFO	csiaddonsnode	Starting watcher for CSIAddonsNode	{"name": "ip-10-0-63-53.ec2.internal-openshift-storage-daemonset-openshift-storage.cephfs.csi.ceph.com-nodeplugin-csi-addons"}
2026-07-27T13:34:49.488Z	ERROR	volume-condition	Failed to check if volume is healthy	{"volumeID": "0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e", "error": "failed to call NodeGetVolumeStats: rpc error: code = Unavailable desc = error reading from server: EOF"}
github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition.(*volumeConditionReporter).Run
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition/reporter.go:126
main.main.func3
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/main.go:241
2026-07-27T13:34:49.490Z	ERROR	volume-condition	Failed to check if volume is healthy	{"volumeID": "0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd", "error": "failed to call NodeGetVolumeStats: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial unix /var/lib/kubelet/plugins/openshift-storage.cephfs.csi.ceph.com/csi.sock: connect: connection refused\""}
github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition.(*volumeConditionReporter).Run
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition/reporter.go:126
main.main.func3
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/main.go:241
2026-07-27T13:34:49.499Z	ERROR	volume-condition	Failed to check if volume is healthy	{"volumeID": "0001-0011-openshift-storage-0000000000000001-71e26d97-9597-47b6-8c22-f7856ad8032e", "error": "failed to call NodeGetVolumeStats: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial unix /var/lib/kubelet/plugins/openshift-storage.cephfs.csi.ceph.com/csi.sock: connect: connection refused\""}
github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition.(*volumeConditionReporter).Run
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition/reporter.go:126
main.main.func3
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/main.go:241
2026-07-27T13:34:49.500Z	ERROR	volume-condition	Failed to check if volume is healthy	{"volumeID": "0001-0011-openshift-storage-0000000000000001-2e3d08cb-9516-40d5-822f-de3dfe9cbdbd", "error": "failed to call NodeGetVolumeStats: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial unix /var/lib/kubelet/plugins/openshift-storage.cephfs.csi.ceph.com/csi.sock: connect: connection refused\""}
github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition.(*volumeConditionReporter).Run
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/internal/volume-condition/reporter.go:126
main.main.func3
	/workspace/go/src/github.com/csi-addons/kubernetes-csi-addons/sidecar/main.go:241

@SanjalKatiyar
SanjalKatiyar marked this pull request as draft July 27, 2026 15:19
@SanjalKatiyar SanjalKatiyar changed the title fs: avoid hanging os.Stat() in NodeGetVolumeStats after restart filesystem: avoid hanging os.Stat() in NodeGetVolumeStats after restart Jul 27, 2026
@SanjalKatiyar SanjalKatiyar changed the title filesystem: avoid hanging os.Stat() in NodeGetVolumeStats after restart cephfs: avoid hanging os.Stat() in NodeGetVolumeStats after restart Jul 27, 2026
@mergify mergify Bot added the component/cephfs Issues related to CephFS label Jul 27, 2026
@SanjalKatiyar
SanjalKatiyar force-pushed the fix_stat_block_issue branch 2 times, most recently from 02b1603 to 826812b Compare July 27, 2026 19:53
Comment thread internal/cephfs/nodeserver.go Outdated
}

// warning: stat() may hang on an unhealthy volume
// no indefinite block/hang, if flow reaches here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is not very descriptive anymore. maybe add a warning, or at least something like 'should not be reachable if volume is unhealthy'.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated...

// checker (re)start scenarios, e.g. following a node-plugin restart
// where the volume was already mounted.
checked bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't lastUpdate not be used for this? Maybe it should not be initialized to time.Now()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nixpanic lastUpdate and checked has different meanings.
Currently lastUpdate is set only when os.Stat() succeeds (https://github.com/ceph/ceph-csi/blob/devel/internal/health-checker/statchecker.go#L63), whereas checked doesn't care about the result, it only cares that os.Stat() call has completed (successful or not) atleast once.

That said, we can definitely re-purpose lastUpdate, but there will be some behavioural changes:

  1. lastUpdate will update on os.Stat() completion (successful or not).
  2. Currently, lastUpdate is initialised to time.Now() and the volume is Intermediately "healthy". If os.Stat() is still stuck, after c.interval + c.timeout (75s) the delay check (https://github.com/ceph/ceph-csi/blob/devel/internal/health-checker/checker.go#L95) kicks-in and marks the volume "unhealthy" automatically, which is a desired behaviour so far.
    But, with re-purposed lastUpdate (initialised to zero or nil), as long as os.Stat() is stuck, we will have to agree upon either of the two behaviours:
    a. We always keep on returning health: true, error: fmt.Errorf("health-check has not completed its first check yet").
    b. Or, always keep on returning health: false, error: fmt.Errorf("health-check has not responded for %f seconds", delay.Seconds()).

If that's acceptable, I can remove checked and shift to lastUpdate instead, but please let me know whether you prefer behaviour 2a or 2b mentioned above. Or, maybe some other suggestion which I might be missing ??

@SanjalKatiyar SanjalKatiyar Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nixpanic I tested the checked flag flow proposed in this PR and it works as expected. I did notice one related behaviour worth mentioning, we can decide whether to address it or accept it as-is.

The scenario this PR fixes: If the MDS goes down and the nodeplugin restarts, checked resets to false, so NodeGetVolumeStats returns early without calling the synchronous os.Stat(). The volumes are correctly reported as unhealthy once the health-checker's 75s timeout elapses. No hangs, no blocks, exactly as desired.

A related scenario that still exists: If the MDS goes down while the nodeplugin is already running, the health-checker has already completed its first cycle (so checked is true) and last reported the volume as healthy. During the window between the last successful health-check and the next timeout detection (~75s), NodeGetVolumeStats sees a healthy result and falls through to the synchronous os.Stat(), which blocks on the unresponsive mount while holding the VolumeLock. This causes subsequent calls for that path to fail with "Aborted". Though, good news is, I noticed the SlowGRPCRestart mechanism (https://github.com/ceph/ceph-csi/blob/devel/internal/csi-common/utils.go#L148) automatically kills the process after 10mins of a stuck gRPC calls, K8s restarts the container, and the fix from this PR then kicks in, so the issue is self-healing (eventually), just with a 10min degraded window.

Even "re-purposed lastUpdate" fix suggested above won't solve this issue, so IMHO we should live with this edge case as-is, it will anyway eventually gets auto-fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks for considering

@SanjalKatiyar
SanjalKatiyar force-pushed the fix_stat_block_issue branch from 826812b to 3a20768 Compare July 28, 2026 10:44
@SanjalKatiyar
SanjalKatiyar marked this pull request as ready for review July 28, 2026 14:05
@SanjalKatiyar
SanjalKatiyar requested a review from nixpanic July 28, 2026 14:05
Comment thread internal/cephfs/nodeserver.go Outdated
Comment on lines 944 to 946

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If healthy and an error is returned, it means either the checker
// has not been started, or it is running but has not completed its
// first health-check cycle yet.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

Comment thread internal/cephfs/nodeserver.go Outdated
// holding the VolumeLock (acquired above) and preventing all future
// calls for this path from reaching isHealthy().
// The background checker will do the stat(), the next periodic
// call will pick up the result (or detect the 75s timeout).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// call will pick up the result (or detect the 75s timeout).
// call will pick up the result (or detect the timeout).

Doing this will allow us to update timeouts without rotting this comment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, make sense...

Comment thread internal/health-checker/checker.go
@SanjalKatiyar
SanjalKatiyar force-pushed the fix_stat_block_issue branch from 3a20768 to 019a465 Compare July 29, 2026 07:11
If a node-plugin pod restarts while the backend (e.g. MDS) is down,
NodeGetVolumeStats used to call os.Stat() on the target path
immediately after (re)starting the health checker, before the
checker had a chance to detect the outage. Since os.Stat() blocks
in the kernel on an unresponsive mount, this held the VolumeLock
forever and made every later call for that path fail with Aborted.

Return early with an "not yet available" condition instead of
calling os.Stat() when the checker was just (re)started, for
CephFS.

Signed-off-by: SanjalKatiyar <sanjaldhir@gmail.com>
Comment thread internal/cephfs/nodeserver.go
// checker (re)start scenarios, e.g. following a node-plugin restart
// where the volume was already mounted.
checked bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks for considering

@nixpanic
nixpanic requested a review from a team July 30, 2026 08:24
// FileChecker is started with the stagingTargetPath, but we can't
// get the stagingPath from the request easily.
// TODO: resolve the stagingPath like rbd.getStagingPath() does
// NOTE: rbd.getStagingPath() uses os.Stat() internally which

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why note about rbd here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve the stagingPath like rbd.getStagingPath() does

this comment was already present in the cephfs flow and suggested taking rbd.getStagingPath as a reference from the RBD implementation. I added an additional "NOTE" on it to highlight that following the same implementation here could again lead to issues or hangs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the CephFS NodeGetVolumeStats path to avoid potentially indefinite blocking on os.Stat() right after a node-plugin restart, by returning an early “not yet available” volume condition until the background health checker has completed at least one check cycle.

Changes:

  • Track whether a health checker has completed its first cycle (checked) and report a distinct “not yet checked” status from isHealthy().
  • Update CephFS NodeGetVolumeStats to return early (instead of calling synchronous os.Stat()) when the checker is newly started / not yet checked.
  • Add/adjust unit tests for the “not yet checked” behavior across checkers.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/health-checker/checker.go Adds checked state and updates isHealthy() semantics for first-cycle completion.
internal/health-checker/checker_test.go Adds tests validating “not yet checked” and timeout behaviors.
internal/health-checker/statchecker.go Marks checked on first cycle and refactors stat error handling.
internal/health-checker/statchecker_test.go Updates tests to assert “not yet checked” behavior before the first tick.
internal/health-checker/filechecker.go Marks checked on cycles (including error paths).
internal/health-checker/filechecker_test.go Updates tests to assert “not yet checked” behavior before the first tick.
internal/cephfs/nodeserver.go Avoids synchronous os.Stat() after restart by returning early “status not yet available”.
Comments suppressed due to low confidence (6)

internal/health-checker/filechecker.go:69

  • Update lastUpdate when the check cycle fails. Without refreshing lastUpdate on readTimestamp() errors, isHealthy() can start reporting a timeout even though the checker is still actively running and returning errors.
					fc.mutex.Lock()
					fc.checked = true
					fc.healthy = false
					fc.err = err
					fc.mutex.Unlock()

internal/health-checker/filechecker.go:80

  • Update lastUpdate when the check cycle fails. If the timestamp mismatch happens quickly but lastUpdate isn't refreshed, isHealthy() can later return a generic "has not responded" timeout instead of the actual mismatch error.
					fc.mutex.Lock()
					fc.checked = true
					fc.healthy = false
					fc.err = errors.New("timestamp read from file does not match what was written")
					fc.mutex.Unlock()

internal/health-checker/statchecker_test.go:37

  • These sleeps make the unit test take ~15s (and it’s easy to accidentally sleep past the first tick, defeating the "before first tick" assertion). Consider using a sub-second interval and short polling to keep the test fast and deterministic.

This issue also appears on line 55 of the same file.

	checker.interval = time.Second * 3

	// start the checker
	checker.start()

internal/health-checker/statchecker_test.go:58

  • The per-iteration 1s sleep makes this test significantly slower without adding much signal (the checker state should remain healthy between reads). A short sleep (or none) is usually enough here.
		healthy, msg = checker.isHealthy()
		if !healthy || msg != nil {
			t.Errorf("volume is unhealthy after %d tries", i+1)
		}

internal/health-checker/filechecker_test.go:37

  • These sleeps make the unit test take multiple seconds. Consider using a sub-second interval and short polling to keep the test fast and avoid accidentally sleeping past the first tick (which would defeat the "before first tick" assertion).

This issue also appears on line 55 of the same file.

	checker.interval = time.Second * 3

	// start the checker
	checker.start()

internal/health-checker/filechecker_test.go:58

  • The per-iteration 1s sleep makes this test significantly slower without adding much signal. A short sleep (or none) is usually enough once the first check cycle has completed.
		healthy, msg = checker.isHealthy()
		if !healthy || msg != nil {
			t.Error("volume is unhealthy")
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 52 to +56
sc.mutex.Lock()
sc.healthy = true
sc.err = nil
sc.lastUpdate = now
sc.checked = true
if statErr != nil {
sc.healthy = false
sc.err = statErr

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing behaviour (not introduced by this PR).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we should have a GH issue instead ??

Comment on lines 54 to 58
fc.mutex.Lock()
fc.checked = true
fc.healthy = false
fc.err = err
fc.mutex.Unlock()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing behaviour (not introduced by this PR).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we should have a GH issue instead ??

@Madhu-1

Madhu-1 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@SanjalKatiyar please open required github issues and fix the CI lint failures? we are good to get this merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/cephfs Issues related to CephFS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants