Skip to content

flaky: TestDebounce fails intermittently in CI on wall-clock sleeps #135

Description

@JordanCoin

TestDebounce (watch/watch_test.go:258) fails intermittently in CI and costs contributors time on unrelated PRs. Seen on #122, where the PR's only change near the debouncer is a comment reword.

Symptom

One job of twelve fails — Test (ubuntu-latest, 1.25) — while Go 1.24 and 1.26 pass on the same image, and all three macOS versions pass. A failure isolated to one Go minor with passing neighbours is a timing artifact, not a code difference.

Not reproducible locally: -count=12 passes, -count=6 -race passes.

Cause

The test coordinates with a live fsnotify daemon using fixed sleeps:

daemon.Start()
time.Sleep(100 * time.Millisecond)          // assume the watcher is registered
for i := 0; i < 5; i++ { os.WriteFile(testFile, ...) }
time.Sleep(300 * time.Millisecond)          // assume debounce flushed
events := daemon.GetEvents(100)
if writeCount == 0 || writeCount > 2 { t.Errorf(...) }
if lastWriteLines != 6 { t.Errorf(...) }

Two independent races on a contended runner:

  • the 100 ms start-up window elapses before addWatchDirs has actually registered, so some writes are never observed → writeCount == 0
  • the 300 ms window doesn't cover debounce-flush (100 ms) plus event processing → the trailing event isn't recorded yet → lastWriteLines != 6

Both are load-dependent, which matches the observed behaviour: it moves between runs, not between code versions.

Suggested fix

The repo already has the right primitive — waitForWatchCondition(t, timeout, cond) in watch/more_test.go, used by the newer watch tests. Converting this test to poll for the condition instead of sleeping toward it removes both races without weakening the assertion:

  • replace the 100 ms start-up sleep with a poll until the daemon reports it is watching
  • replace the 300 ms settle sleep with waitForWatchCondition on writeCount >= 1 && lastWriteLines == 6, keeping the writeCount <= 2 bound as the real assertion

That preserves exactly what the test is for — a burst of writes must collapse to at most one trailing event carrying the latest contents — while letting a slow runner take the time it needs.

Worth auditing the other sleep-based tests in watch/ at the same time; this is the one that has surfaced, not necessarily the only one.

Why it matters beyond the noise

A red check that isn't the contributor's costs more than the minute it takes to re-run: it trains people to ignore red. On a repo where CI is the main signal for external contributions, that is expensive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions