aboutsummaryrefslogtreecommitdiff
path: root/internal/net/session_close_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/net/session_close_test.go')
-rw-r--r--internal/net/session_close_test.go73
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")
+ }
+}