fix(model): properly health-check up-to-date folders (fixes #10546) (#10773)

Previously we'd skip the health check for an up-to-date folder, so it
would continue looking up-to-date even when the folder path missing.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
Jakob Borg
2026-07-20 22:54:31 +02:00
committed by GitHub
parent d710874903
commit 9f7b4d4104
5 changed files with 41 additions and 30 deletions
+41 -26
View File
@@ -195,19 +195,23 @@ func (f *folder) Serve(ctx context.Context) error {
case <-f.pullScheduled: case <-f.pullScheduled:
if f.PullerDelayS > 0 { if f.PullerDelayS > 0 {
// Wait for incoming updates to settle before doing the // Wait for incoming updates to settle before doing the
// actual pull. Only set the state to SyncWaiting if we have // actual pull.
// reason to believe there is something to sync, to avoid //
// unnecessary flashing in the GUI. // Only set the state to SyncWaiting if we are Idle (and
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 { // not, e.g., errored) and have reason to believe there is
f.setState(FolderSyncWaiting) // something to sync, to avoid unnecessary flashing in the
// GUI.
if cur, _, _ := f.getState(); cur == FolderIdle {
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
f.setState(FolderSyncWaiting)
}
} }
pullTimer.Reset(time.Duration(float64(time.Second) * f.PullerDelayS)) pullTimer.Reset(time.Duration(float64(time.Second) * f.PullerDelayS))
} else { continue
_, err = f.pull(ctx)
} }
_, err = f.pull(ctx)
case <-pullTimer.C: case <-pullTimer.C:
f.setState(FolderIdle)
_, err = f.pull(ctx) _, err = f.pull(ctx)
case <-f.pullFailTimer.C: case <-f.pullFailTimer.C:
@@ -238,10 +242,12 @@ func (f *folder) Serve(ctx context.Context) error {
case next := <-f.scanDelay: case next := <-f.scanDelay:
f.sl.DebugContext(ctx, "Delaying scan") f.sl.DebugContext(ctx, "Delaying scan")
f.scanTimer.Reset(next) f.scanTimer.Reset(next)
continue
case <-f.scanScheduled: case <-f.scanScheduled:
f.sl.DebugContext(ctx, "Scan was scheduled") f.sl.DebugContext(ctx, "Scan was scheduled")
f.scanTimer.Reset(0) f.scanTimer.Reset(0)
continue
case fsEvents := <-f.watchChan: case fsEvents := <-f.watchChan:
f.sl.DebugContext(ctx, "Scan due to watcher") f.sl.DebugContext(ctx, "Scan due to watcher")
@@ -252,16 +258,20 @@ func (f *folder) Serve(ctx context.Context) error {
err = f.restartWatch(ctx) err = f.restartWatch(ctx)
case <-f.versionCleanupTimer.C: case <-f.versionCleanupTimer.C:
if _, _, healthErr := f.getState(); healthErr != nil {
continue
}
f.sl.DebugContext(ctx, "Doing version cleanup") f.sl.DebugContext(ctx, "Doing version cleanup")
f.versionCleanupTimerFired(ctx) f.versionCleanupTimerFired(ctx)
} }
if err != nil { if svcutil.IsFatal(err) {
if svcutil.IsFatal(err) { return err
return err
}
f.setError(ctx, err)
} }
// Set the state to FolderError when err is non-nil, otherwise reset
// it back to FolderIdle.
f.setError(ctx, err)
} }
} }
@@ -403,6 +413,15 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
} }
}() }()
// Abort early (before acquiring a limiter token) if there's a folder
// error. This must happen before the "nothing to do" check below so
// that up-to-date folders are also periodically health-checked.
err = f.getHealthErrorWithoutIgnores()
if err != nil {
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
return false, err
}
// If there is nothing to do, don't even enter sync-waiting state. // If there is nothing to do, don't even enter sync-waiting state.
needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID) needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID)
if err != nil { if err != nil {
@@ -416,13 +435,6 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
return true, nil return true, nil
} }
// Abort early (before acquiring a token) if there's a folder error
err = f.getHealthErrorWithoutIgnores()
if err != nil {
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
return false, err
}
// Send only folder doesn't do any io, it only checks for out-of-sync // Send only folder doesn't do any io, it only checks for out-of-sync
// items that differ in metadata and updates those. // items that differ in metadata and updates those.
if f.Type != config.FolderTypeSendOnly { if f.Type != config.FolderTypeSendOnly {
@@ -487,7 +499,6 @@ func (f *folder) scanSubdirs(ctx context.Context, subDirs []string) error {
}() }()
f.setState(FolderScanWaiting) f.setState(FolderScanWaiting)
defer f.setState(FolderIdle)
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil { if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
return err return err
@@ -993,7 +1004,6 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
func (f *folder) versionCleanupTimerFired(ctx context.Context) { func (f *folder) versionCleanupTimerFired(ctx context.Context) {
f.setState(FolderCleanWaiting) f.setState(FolderCleanWaiting)
defer f.setState(FolderIdle)
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil { if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
return return
@@ -1184,8 +1194,13 @@ func (f *folder) setError(ctx context.Context, err error) {
default: default:
} }
_, _, oldErr := f.getState() state, _, oldErr := f.getState()
if (err != nil && oldErr != nil && oldErr.Error() == err.Error()) || (err == nil && oldErr == nil) { switch {
case err != nil && oldErr != nil && oldErr.Error() == err.Error():
// The error is the same as the old error, no change is required
return
case err == nil && oldErr == nil && state == FolderIdle:
// No error before or now, and we are already Idle
return return
} }
@@ -1195,7 +1210,7 @@ func (f *folder) setError(ctx context.Context, err error) {
} else { } else {
f.sl.InfoContext(ctx, "Folder error changed", slogutil.Error(err), slog.Any("previously", oldErr)) f.sl.InfoContext(ctx, "Folder error changed", slogutil.Error(err), slog.Any("previously", oldErr))
} }
} else { } else if oldErr != nil {
f.sl.InfoContext(ctx, "Folder error cleared") f.sl.InfoContext(ctx, "Folder error cleared")
f.SchedulePull() f.SchedulePull()
} }
@@ -1203,7 +1218,7 @@ func (f *folder) setError(ctx context.Context, err error) {
if f.FSWatcherEnabled { if f.FSWatcherEnabled {
if err != nil { if err != nil {
f.stopWatch() f.stopWatch()
} else { } else if oldErr != nil {
f.scheduleWatchRestart() f.scheduleWatchRestart()
} }
} }
-1
View File
@@ -44,7 +44,6 @@ func (f *receiveEncryptedFolder) revert(ctx context.Context) error {
f.sl.InfoContext(ctx, "Reverting unexpected items") f.sl.InfoContext(ctx, "Reverting unexpected items")
f.setState(FolderScanning) f.setState(FolderScanning)
defer f.setState(FolderIdle)
batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error { batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
f.updateLocalsFromScanning(fs) f.updateLocalsFromScanning(fs)
-1
View File
@@ -74,7 +74,6 @@ func (f *receiveOnlyFolder) revert(ctx context.Context) error {
f.sl.InfoContext(ctx, "Reverting folder") f.sl.InfoContext(ctx, "Reverting folder")
f.setState(FolderScanning) f.setState(FolderScanning)
defer f.setState(FolderIdle)
scanChan := make(chan string) scanChan := make(chan string)
go f.pullScannerRoutine(ctx, scanChan) go f.pullScannerRoutine(ctx, scanChan)
-1
View File
@@ -98,7 +98,6 @@ func (f *sendOnlyFolder) override(ctx context.Context) error {
f.sl.InfoContext(ctx, "Overriding global state ") f.sl.InfoContext(ctx, "Overriding global state ")
f.setState(FolderScanning) f.setState(FolderScanning)
defer f.setState(FolderIdle)
batch := NewFileInfoBatch(func(files []protocol.FileInfo) error { batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
f.updateLocalsFromScanning(files) f.updateLocalsFromScanning(files)
-1
View File
@@ -168,7 +168,6 @@ func (f *sendReceiveFolder) pull(ctx context.Context) (bool, error) {
go f.pullScannerRoutine(ctx, scanChan) go f.pullScannerRoutine(ctx, scanChan)
defer func() { defer func() {
close(scanChan) close(scanChan)
f.setState(FolderIdle)
}() }()
metricFolderPulls.WithLabelValues(f.ID).Inc() metricFolderPulls.WithLabelValues(f.ID).Inc()