Fix watcher recovery after atomic saves (#909)

* fix(watcher): delay rewatch after atomic saves

* fix(watcher): retry watcher.Add with exponential backoff on atomic saves

A single Add attempt after a fixed 100ms delay could fail if the file
is momentarily absent (rename-away before recreate on slow or Docker
bind-mount filesystems), leaving the file permanently unwatched.

Retry up to 5 times with doubling delays (100ms → 1600ms); log only on
final failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xiantang
2026-05-21 13:38:44 +08:00
committed by GitHub
parent 55232d5571
commit 97072102b3
+18 -3
View File
@@ -273,6 +273,23 @@ func (e *Engine) cacheFileChecksums(root string) error {
})
}
func (e *Engine) rewatchFile(name string) {
delay := time.Millisecond * 100
maxRetries := 5
for i := 0; i < maxRetries; i++ {
time.Sleep(delay)
err := e.watcher.Add(name)
if err == nil {
return
}
delay *= 2
}
e.watcherLog("failed to rewatch %s after %d retries", name, maxRetries)
}
func (e *Engine) watchPath(path string) error {
if err := e.watcher.Add(path); err != nil {
e.watcherLog("failed to watch %s, error: %s", path, err.Error())
@@ -323,9 +340,7 @@ func (e *Engine) watchPath(path string) error {
// Rewatch the file if the editor is using atomic saving.
if renameOrRemoveEvent(ev) {
if e.checkIncludeFile(ev.Name) {
if err := e.watcher.Add(ev.Name); err != nil {
e.watcherLog("error rewatching file: %v", err)
}
go e.rewatchFile(ev.Name)
}
}
e.watcherDebug("%s has changed", e.config.rel(ev.Name))