From f132aef2958f21d127d614db8ea150d33eef4412 Mon Sep 17 00:00:00 2001 From: Loi Nguyen Date: Fri, 31 Jul 2026 12:38:22 +0700 Subject: [PATCH] vfs: synchronize poll updates with shutdown - fixes #9689 --- vfs/rc.go | 9 +++++++ vfs/rc_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ vfs/vfs.go | 9 ++++--- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/vfs/rc.go b/vfs/rc.go index 02aff0c17..a58200408 100644 --- a/vfs/rc.go +++ b/vfs/rc.go @@ -275,6 +275,8 @@ func getStatus(vfs *VFS, in rc.Params) (out rc.Params, err error) { for k, v := range in { return nil, fmt.Errorf("invalid parameter: %s=%s", k, v) } + vfs.pollMu.Lock() + defer vfs.pollMu.Unlock() return rc.Params{ "enabled": vfs.Opt.PollInterval != 0, "supported": vfs.pollChan != nil, @@ -332,11 +334,14 @@ func rcPollInterval(ctx context.Context, in rc.Params) (out rc.Params, err error for k, v := range in { return nil, fmt.Errorf("invalid parameter: %s=%s", k, v) } + vfs.pollMu.Lock() if vfs.pollChan == nil { + vfs.pollMu.Unlock() return nil, errors.New("poll-interval is not supported by this remote") } if !intervalPresent { + vfs.pollMu.Unlock() return getStatus(vfs, in) } var timeoutHit bool @@ -351,7 +356,11 @@ func rcPollInterval(ctx context.Context, in rc.Params) (out rc.Params, err error vfs.Opt.PollInterval = fs.Duration(interval) case <-timeoutChan: timeoutHit = true + case <-vfs.ctx.Done(): + vfs.pollMu.Unlock() + return nil, errors.New("VFS is shutting down") } + vfs.pollMu.Unlock() out, err = getStatus(vfs, in) if out != nil { out["timeout"] = timeoutHit diff --git a/vfs/rc_test.go b/vfs/rc_test.go index d2105d0b4..4de99d57d 100644 --- a/vfs/rc_test.go +++ b/vfs/rc_test.go @@ -3,6 +3,7 @@ package vfs import ( "context" "testing" + "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/rc" @@ -101,6 +102,75 @@ func TestRcPollInterval(t *testing.T) { // FIXME needs more tests } +func TestRcPollIntervalShutdown(t *testing.T) { + r := fstest.NewRun(t) + features := r.Fremote.Features() + originalChangeNotify := features.ChangeNotify + t.Cleanup(func() { + features.ChangeNotify = originalChangeNotify + }) + + initialIntervalReceived := make(chan struct{}) + features.ChangeNotify = func(_ context.Context, _ func(string, fs.EntryType), pollInterval <-chan time.Duration) { + go func() { + <-pollInterval + close(initialIntervalReceived) + }() + } + + vfs := New(context.Background(), r.Fremote, nil) + t.Cleanup(func() { + if vfs.inUse.Load() > 0 { + vfs.Shutdown() + } + }) + <-initialIntervalReceived + + call := rc.Calls.Get("vfs/poll-interval") + require.NotNil(t, call) + originalInterval := vfs.Opt.PollInterval + + type result struct { + out rc.Params + err error + } + resultCh := make(chan result, 1) + go func() { + out, err := call.Fn(context.Background(), rc.Params{ + "fs": fs.ConfigString(r.Fremote), + "interval": "1h", + }) + resultCh <- result{out: out, err: err} + }() + + select { + case got := <-resultCh: + t.Fatalf("poll interval update returned before shutdown: out=%v err=%v", got.out, got.err) + case <-time.After(100 * time.Millisecond): + } + + shutdownDone := make(chan struct{}) + go func() { + vfs.Shutdown() + close(shutdownDone) + }() + + select { + case <-shutdownDone: + case <-time.After(time.Second): + t.Fatal("VFS shutdown blocked behind poll interval update") + } + + select { + case got := <-resultCh: + require.EqualError(t, got.err, "VFS is shutting down") + assert.Nil(t, got.out) + case <-time.After(time.Second): + t.Fatal("poll interval update did not return after shutdown") + } + assert.Equal(t, originalInterval, vfs.Opt.PollInterval) +} + func TestRcList(t *testing.T) { r, vfs, call := rcNewRun(t, "vfs/list") _ = vfs diff --git a/vfs/vfs.go b/vfs/vfs.go index 8f576d5d5..79c352a54 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -188,6 +188,7 @@ type VFS struct { usageMu sync.Mutex usageTime time.Time usage *fs.Usage + pollMu sync.Mutex pollChan chan time.Duration inUse atomic.Int32 // count of number of opens } @@ -410,13 +411,15 @@ func (vfs *VFS) Shutdown() { vfs.shutdownCache() + // Cancel any background go routines + vfs.cancel() + + vfs.pollMu.Lock() if vfs.pollChan != nil { close(vfs.pollChan) vfs.pollChan = nil } - - // Cancel any background go routines - vfs.cancel() + vfs.pollMu.Unlock() } // CleanUp deletes the contents of the on disk cache