go-test-flaky-race-callback-order
Root cause: The test registered the OnClose callback after calling Close(). The connection's background readLoop snapshots onClose under the mutex the moment it observes the close signal. If the snapshot runs before the late registration lands, the callback is nil and the close signal is silently lost — a classic flake (passes when the readLoop happens to wake up late, fails when it wakes up early).
The fix (test-only): register the callback before triggering the close event. This is deterministic, not just "less racy": OnClose's mutex unlock happens-before Close's close(closeCh), and the readLoop takes the mutex only after observing the signal — so the snapshot cannot miss the registration.
// conn_test.go — FIXED
func TestOnCloseFiresOnClose(t *testing.T) {
c := NewConnection()
fired := make(chan struct{})
c.OnClose(func() { close(fired) }) // 1) register FIRST
c.Close() // 2) then trigger the close event
select {
case <-fired:
case <-time.After(time.Second):
t.Fatal("onClose callback was not invoked")
}
}
// Original buggy test (for contrast) — registered AFTER Close():
c.Close() // close event fired; readLoop may snapshot onClose == nil
c.OnClose(func() { close(fired) }) // too late -> close signal lost
The supporting library code (conn.go) is untouched: OnClose sets the callback under a mutex; readLoop waits on closeCh, snapshots onClose under the same mutex, and invokes it once. A full reproduction is in ~/flaky-race-demo/.
All runs under `-race` on `go1.26.0`: | Check | Command | Result | |---|---|---| | Stress the fixed test | `go test -race -count=20 -run TestOnCloseFiresOnClose .` | **20/20 pass** (`ok flaky-race-demo 1.024s`) | | Full suite | `go test -race -count=1 -v ./...` | **4/4 pass**: `TestOnCloseFiresOnClose`, `TestOnCloseInvokedExactlyOnce`, `TestOnCloseNotFiredBeforeClose`, `TestOnCloseReadLoopTerminates` | | Static check | `go vet ./...` | clean | | Original buggy test (proves the bug is real) | `go test -tags flaky -race -count=3 .` | **3/3 fail**: `BUG reproduced: onClose callback was not invoked (close signal lost)` | Edge cases covered: - **Double `Close()`** — callback fired exactly once, second `Close` is a no-op. - **Callback not fired before `Close`** — no premature delivery. - **Goroutine lifecycle** — read loop terminates after close (no leak). - **Ordering guarantee** — the buggy variant (late registration, widened window) fails deterministically, while the fixed ordering passes every run.
{"model": "deepseek-v4-flash", "problem_class": "go-test-flaky-race-callback-order", "result": "passed", "tests": 4}Root cause: The test registered the OnClose callback after calling Close(). The connection's background readLoop snapshots onClose under the mutex the moment it observes the close signal. If the snapshot runs before the late registration lands, the callback is nil and the close signal is silently lost — a classic flake (passes when the readLoop happens to wake up late, fails when it wakes up early).
The fix (test-only): register the callback before triggering the close event. This is deterministic, not just "less racy": OnClose's mutex unlock happens-before Close's close(closeCh), and the readLoop takes the mutex only after observing the signal — so the snapshot cannot miss the registration.
// conn_test.go — FIXED
func TestOnCloseFiresOnClose(t *testing.T) {
c := NewConnection()
fired := make(chan struct{})
c.OnClose(func() { close(fired) }) // 1) register FIRST
c.Close() // 2) then trigger the close event
select {
case <-fired:
case <-time.After(time.Second):
t.Fatal("onClose callback was not invoked")
}
}
// Original buggy test (for contrast) — registered AFTER Close():
c.Close() // close event fired; readLoop may snapshot onClose == nil
c.OnClose(func() { close(fired) }) // too late -> close signal lost
The supporting library code (conn.go) is untouched: OnClose sets the callback under a mutex; readLoop waits on closeCh, snapshots onClose under the same mutex, and invokes it once. A full reproduction is in ~/flaky-race-demo/.
All runs under `-race` on `go1.26.0`: | Check | Command | Result | |---|---|---| | Stress the fixed test | `go test -race -count=20 -run TestOnCloseFiresOnClose .` | **20/20 pass** (`ok flaky-race-demo 1.024s`) | | Full suite | `go test -race -count=1 -v ./...` | **4/4 pass**: `TestOnCloseFiresOnClose`, `TestOnCloseInvokedExactlyOnce`, `TestOnCloseNotFiredBeforeClose`, `TestOnCloseReadLoopTerminates` | | Static check | `go vet ./...` | clean | | Original buggy test (proves the bug is real) | `go test -tags flaky -race -count=3 .` | **3/3 fail**: `BUG reproduced: onClose callback was not invoked (close signal lost)` | Edge cases covered: - **Double `Close()`** — callback fired exactly once, second `Close` is a no-op. - **Callback not fired before `Close`** — no premature delivery. - **Goroutine lifecycle** — read loop terminates after close (no leak). - **Ordering guarantee** — the buggy variant (late registration, widened window) fails deterministically, while the fixed ordering passes every run.
{"model": "deepseek-v4-flash", "problem_class": "go-test-flaky-race-callback-order", "result": "passed", "tests": 4}