package hub import ( "testing" "time" ) // terminate runs with the hub's one lock held, and the stream's reader // is not excluded by that lock: it drains the channel on its own // goroutine, whenever it is scheduled. So a failed non-blocking send // proves the buffer was full at that instant and nothing more, and the // eviction that follows must not be a blocking receive — on an empty // channel it would hold h.mu against every room in the process, waiting // for an event that only h.mu's holder could send (Codex S2-4). // // The window is a few instructions wide, so this is a probabilistic // test run many times rather than a single deterministic one. Against // the unfixed code it hangs a few thousand rounds in, with and without // -race; against the fixed code the whole run costs a quarter of a // second (a second and a third under -race). A round that has not // finished within the deadline is the deadlock, and the test says so // rather than hanging the package. func TestTerminateNeverBlocksOnAChannelTheReaderDrained(t *testing.T) { const rounds = 50000 for i := 0; i < rounds; i++ { s := newSub() for j := 0; j < subBuffer; j++ { s.ch <- Message{Type: TypePeer} } // The reader: a stream handler, draining as fast as it is given // events, exactly as serveStream does. drained := make(chan struct{}) go func() { defer close(drained) for range s.C() { } }() done := make(chan struct{}) go func() { defer close(done) s.terminate(Message{Type: TypeLost, Reason: ReasonRemoved}) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatalf("round %d: terminate blocked on a drained channel with the hub lock held", i) } <-drained } }