diff options
| author | historia <[not public]> | 2026-09-16 14:41:15 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-16 14:41:15 -0400 |
| commit | 9192d94dea6be5498ab080f3e2df5667af871d7a (patch) | |
| tree | e7aef3089d5d1e8da686c6b6ca9d4faeacb8b20b /internal/net/session_close_test.go | |
| parent | 2f34e75f27e5f99cfc1c5f8eaf6ab4ff2fddb1d9 (diff) | |
| download | thehouseoficarus-9192d94dea6be5498ab080f3e2df5667af871d7a.tar.gz | |
fix: casino room eviction, session close race fix, open-payout recovery evidence
Diffstat (limited to 'internal/net/session_close_test.go')
| -rw-r--r-- | internal/net/session_close_test.go | 73 |
1 files changed, 73 insertions, 0 deletions
diff --git a/internal/net/session_close_test.go b/internal/net/session_close_test.go new file mode 100644 index 0000000..7cb54b6 --- /dev/null +++ b/internal/net/session_close_test.go @@ -0,0 +1,73 @@ +package net + +// Close/broadcast race regression: a broadcast racing Session.Close() used +// to send on the outbound channel while Close closed the same channel — a +// genuine data race (closechan vs chansend) that the old panic-recover only +// hid. Closing is now signaled through a separate channel that is only ever +// closed, never sent to. + +import ( + stdnet "net" + "sync" + "testing" + "time" +) + +// newTestSocketPair returns a synchronous, unbuffered connection pair: a +// write blocks until the peer reads, which is exactly the stall condition +// the echo test has to reproduce. +func newTestSocketPair() (stdnet.Conn, stdnet.Conn) { return stdnet.Pipe() } + +func testTimeout() <-chan time.Time { return time.After(200 * time.Millisecond) } + +func TestConcurrentWriteCloseIsRaceFree(t *testing.T) { + for i := 0; i < 50; i++ { + sess := &Session{Conn: &testConn{}} + sess.StartWriter() + var wg sync.WaitGroup + wg.Add(2) + start := make(chan struct{}) + go func() { + defer wg.Done() + <-start + for j := 0; j < 100; j++ { + sess.WriteLine("broadcast") + } + }() + go func() { + defer wg.Done() + <-start + sess.Close() + }() + close(start) + wg.Wait() + <-sess.writeDone + } +} + +// Echo negotiation regression: login flips telnet echo on the game owner's +// goroutine. Writing the negotiation inline made that write a direct socket +// I/O, so a client that never read (full receive window, stalled socket) +// blocked the entire game loop behind one connection. Queued sessions must +// route the negotiation through the outbound writer instead. +func TestSetEchoQueuedDoesNotBlockCaller(t *testing.T) { + local, peer := newTestSocketPair() + defer local.Close() + defer peer.Close() + sess := &Session{Conn: newTCPConn(local)} + sess.StartWriter() + defer sess.Close() + + done := make(chan struct{}) + go func() { + sess.SetEcho(false) // what the login path calls for password entry + close(done) + }() + select { + case <-done: + case <-testTimeout(): + peer.Close() // unblock the writer so the test can exit + <-done + t.Fatal("SetEcho blocked the caller instead of queueing the negotiation") + } +} |
