vfs: make poll shutdown lifecycle deterministic

This commit is contained in:
Loi Nguyen
2026-08-01 12:44:42 +01:00
committed by Nick Craig-Wood
parent d5275c4ebb
commit 1d03a77172
2 changed files with 68 additions and 27 deletions
+34 -22
View File
@@ -288,6 +288,32 @@ func getStatus(vfs *VFS, in rc.Params) (out rc.Params, err error) {
}, nil
}
func setPollInterval(vfs *VFS, interval, timeout time.Duration) (timeoutHit bool, err error) {
vfs.pollMu.Lock()
defer vfs.pollMu.Unlock()
if vfs.ctx.Err() != nil {
return false, errors.New("VFS is shutting down")
}
if vfs.pollChan == nil {
return false, errors.New("poll-interval is not supported by this remote")
}
var timeoutChan <-chan time.Time
if timeout > 0 {
timer := time.NewTimer(timeout)
defer timer.Stop()
timeoutChan = timer.C
}
select {
case vfs.pollChan <- interval:
vfs.Opt.PollInterval = fs.Duration(interval)
case <-timeoutChan:
timeoutHit = true
case <-vfs.ctx.Done():
return false, errors.New("VFS is shutting down")
}
return timeoutHit, nil
}
func init() {
rc.Add(rc.Call{
Path: "vfs/poll-interval",
@@ -334,33 +360,19 @@ 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.Lock()
supported := vfs.pollChan != nil
vfs.pollMu.Unlock()
if !supported {
return nil, errors.New("poll-interval is not supported by this remote")
}
return getStatus(vfs, in)
}
var timeoutHit bool
var timeoutChan <-chan time.Time
if timeout > 0 {
timer := time.NewTimer(timeout)
defer timer.Stop()
timeoutChan = timer.C
timeoutHit, err := setPollInterval(vfs, interval, timeout)
if err != nil {
return nil, err
}
select {
case vfs.pollChan <- interval:
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
+34 -5
View File
@@ -2,6 +2,7 @@ package vfs
import (
"context"
"runtime"
"testing"
"time"
@@ -110,6 +111,20 @@ func newTestPollVFS(t *testing.T, changeNotify func(context.Context, func(string
return r, vfs, call
}
func waitForPollLock(t *testing.T, vfs *VFS) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if vfs.pollMu.TryLock() {
vfs.pollMu.Unlock()
runtime.Gosched()
continue
}
return
}
t.Fatal("poll interval update did not acquire poll lock")
}
func TestRcPollInterval(t *testing.T) {
r, vfs, call := rcNewRun(t, "vfs/poll-interval")
_ = vfs
@@ -147,11 +162,7 @@ func TestRcPollIntervalShutdown(t *testing.T) {
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):
}
waitForPollLock(t, vfs)
shutdownDone := make(chan struct{})
go func() {
@@ -175,6 +186,24 @@ func TestRcPollIntervalShutdown(t *testing.T) {
assert.Equal(t, originalInterval, vfs.Opt.PollInterval)
}
func TestSetPollIntervalAfterShutdown(t *testing.T) {
initialIntervalReceived := make(chan struct{})
_, vfs, _ := newTestPollVFS(t, func(_ context.Context, _ func(string, fs.EntryType), pollInterval <-chan time.Duration) {
go func() {
<-pollInterval
close(initialIntervalReceived)
for range pollInterval {
}
}()
})
<-initialIntervalReceived
vfs.Shutdown()
timeoutHit, err := setPollInterval(vfs, time.Hour, 0)
require.EqualError(t, err, "VFS is shutting down")
assert.False(t, timeoutHit)
}
func TestRcPollIntervalUpdate(t *testing.T) {
intervals := make(chan time.Duration, 2)
r, vfs, call := newTestPollVFS(t, func(_ context.Context, _ func(string, fs.EntryType), pollInterval <-chan time.Duration) {