Skip to content

watcher: 5s readiness-timeout arm returns with no poller fallback #721

Description

@zzet

Watcher.Start's readiness wait has three exits, and only two of them fall back to the adaptive poller.

// internal/indexer/watcher.go
case err := <-errc:
    if isInotifyExhausted(err) || isFDExhausted(err) {
        w.noteWatchDegraded(err)
        w.degradedNoFsnotify = true
        // ... closes fsw, starts the poller, returns nil
    }
    return err
case <-time.After(5 * time.Second):
    cancel()
    return errors.New("watcher: backend did not become ready within 5s")   // <-- no poller
}

The inotify/FD-exhaustion arm degrades gracefully. The plain timeout arm does not: it returns an error, MultiWatcher records a start failure, and the repo ends up with no fsnotify and no poller — no update mechanism at all until a manual untrack + track.

Why this matters

This is the exact failure mode reported from a live NFS-mounted multi-repo daemon in #697. That PR fixes it for Linux by teaching slowWatchMount to recognise NFS_SUPER_MAGIC, so the mount is detected before fsnotify is attempted and the poller is chosen up front. That's the right fix and it should land.

But it is a per-filesystem allow-list, so it only covers what the list knows about:

  • slow_mount_other.go returns false unconditionally, so macOS and Windows get no slow-mount protection at all — a Mac with a repo on an NFS or SMB mount hits the timeout arm and dies silently.
  • Any network or virtualised filesystem whose magic isn't in the switch has the same outcome on Linux.

Falling back to the poller on the timeout arm closes the whole class at once, independently of magic-number coverage. A backend that cannot become ready in 5s is, by definition, one whose events we cannot rely on — which is precisely the condition the poller exists to cover.

Proposed change

Treat the timeout the same way the exhaustion arm is treated: log the degraded reason, set degradedNoFsnotify, tear down the half-started backend, start the poller, return nil.

The one thing to get right is that this must stay visible rather than becoming a silent success. Start returning nil makes MultiWatcher mark the repo started, so WatchedRepos() counts it live. The repo genuinely is being watched (by the poller), so that isn't a lie — but the degraded reason needs to reach noteWatchDegraded and the daemon status output the way the exhaustion path already does, so an operator can tell poller-only from fsnotify-backed.

Worth a test that drives a backend which never signals ready and asserts the poller comes up.

Follow-up to #697; not a blocker for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions