From e724790620d390447f5553bdad16eda4e8584db9 Mon Sep 17 00:00:00 2001 From: ferrumclaudepilgrim Date: Tue, 11 Aug 2026 22:15:33 -0500 Subject: [PATCH] vfs/vfscache: fix hang when the cache cleaner is disabled KickCleaner sets the out of space flag, kicks the cleaner and then waits for that flag to clear. Only the cleaner clears it, and the cleaner returns immediately when the cache poll interval is not positive, so when it is disabled nothing ever reads the kick or clears the flag and the caller waits forever. It now returns straight away in that case, under the same condition the cleaner itself uses to decide it is disabled. Callers already retry a bounded number of times and then report the error, which is the right outcome when nothing is going to free space. --- vfs/vfscache/cache.go | 8 ++++++++ vfs/vfscache/cache_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/vfs/vfscache/cache.go b/vfs/vfscache/cache.go index eaa17b8a0..ca79f228b 100644 --- a/vfs/vfscache/cache.go +++ b/vfs/vfscache/cache.go @@ -563,7 +563,15 @@ func (c *Cache) reload(ctx context.Context) error { } // KickCleaner kicks cache cleaner upon out of space situation +// +// This does nothing when the cleaner is disabled. Only the cleaner clears the +// out of space condition, so with no cleaner running the wait below would never +// return. func (c *Cache) KickCleaner() { + if c.opt.CachePollInterval <= 0 { + return + } + /* Use a separate kicker mutex for the kick to go through without waiting for the cache mutex to avoid letting a thread kick again after the clearer just finished cleaning and unlock the cache mutex. */ diff --git a/vfs/vfscache/cache_test.go b/vfs/vfscache/cache_test.go index a86cb3c50..092999b27 100644 --- a/vfs/vfscache/cache_test.go +++ b/vfs/vfscache/cache_test.go @@ -655,6 +655,34 @@ func TestCacheCleaner(t *testing.T) { assert.False(t, found) } +func TestCacheKickCleaner(t *testing.T) { + // kickCleaner runs KickCleaner and reports whether it returned in time. + kickCleaner := func(t *testing.T, c *Cache) bool { + t.Helper() + done := make(chan struct{}) + go func() { + defer close(done) + c.KickCleaner() + }() + select { + case <-done: + return true + case <-time.After(2 * time.Second): + return false + } + } + + // Only the cleaner clears the out of space condition, so with no cleaner + // running a KickCleaner which waited for it would never return. + t.Run("CleanerDisabled", func(t *testing.T) { + opt := vfscommon.Opt + opt.CachePollInterval = 0 + _, c := newTestCacheOpt(t, opt) + + assert.True(t, kickCleaner(t, c), "KickCleaner did not return with the cleaner disabled") + }) +} + func TestCacheSetModTime(t *testing.T) { _, c := newTestCache(t)