aboutsummaryrefslogtreecommitdiff
path: root/internal/game/owner_concurrency_test.go
blob: 58e426bede5e7dab2110a92b00e4bb626c94c6bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package game

// Owner-loop concurrency regressions. These exercise the competing runtime
// entry points (connection input, ticks, disconnects, admin mutations) the
// way production does and must pass under -race now that all live-state
// mutation executes on the owner goroutine. The pre-owner baselines live in
// baseline_race_test.go behind the baselinerace build tag.

import (
	"sync"
	"testing"
	"time"
)

// TestOwnerSerializesEntryPoints runs session input, the ticker goroutine,
// and hub disconnects concurrently.
func TestOwnerSerializesEntryPoints(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice", "pw", false)
	h.seedAccount("bob", "pw", false)
	h.seedCharacter("alice", "alice")
	h.seedCharacter("bob", "bob")

	aSess, _ := h.connect("alice")
	bSess, _ := h.connect("bob")

	aLines := h.goInput(aSess, 1)
	bLines := h.goInput(bSess, 1)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 200; i++ {
			aLines <- "look"
		}
		close(aLines)
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 200; i++ {
			bLines <- "say hi"
		}
		close(bLines)
	}()

	go func() {
		for i := 0; i < 50; i++ {
			h.hub.PlayersInRoom(1)
			if i == 25 {
				h.hub.Remove(bSess)
			}
		}
	}()

	wg.Wait()
	h.barrier()
}

// TestOwnerAdminRewriteWhilePlaying runs Game.RewriteRoomIDs (admin entry)
// concurrently with live movement and ticking.
func TestOwnerAdminRewriteWhilePlaying(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("carol", "pw", false)
	h.seedCharacter("carol", "carol")
	sess, _ := h.connect("carol")
	lines := h.goInput(sess, 1)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 100; i++ {
			lines <- "look"
		}
		close(lines)
	}()
	go func() {
		defer wg.Done()
		// Rewrite 2->4, then back; exercise both directions while the
		// player commands run.
		for i := 0; i < 10; i++ {
			h.g.RunOnOwner(func() { h.g.RewriteRoomIDs(map[int]int{2: 4}) })
			h.g.RunOnOwner(func() { h.g.RewriteRoomIDs(map[int]int{4: 2}) })
		}
	}()
	wg.Wait()
	h.barrier()
}

// TestSlowClientDoesNotStallTheWorld proves the write-queue boundary: a
// session whose transport blocks forever on Write must not prevent other
// players from receiving output and progressing.
func TestSlowClientDoesNotStallTheWorld(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice", "pw", false)
	h.seedAccount("bob", "pw", false)
	h.seedCharacter("alice", "alice")
	h.seedCharacter("bob", "bob")

	// Alice's writer goroutine blocks at the transport (full socket buffer
	// equivalent); her queue absorbs or disconnects her, but never blocks
	// the owner goroutine.
	aSess, aConn := h.connect("alice")
	bSess, bConn := h.connect("bob")
	aConn.writeGate = make(chan struct{})

	bLines := h.goInput(bSess, 1)

	for i := 0; i < 300; i++ {
		aSess.WriteLine("spam for the stalled client")
	}
	// Bob keeps playing and must still see output while alice is stuck.
	bLines <- "say hello"
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		if bConn.Contains("hello") {
			return
		}
		time.Sleep(5 * time.Millisecond)
	}
	t.Fatalf("bob's output never arrived while alice's client was stalled")
}

// TestOwnerDisconnectDuringInput removes a session from a separate goroutine
// while its input goroutine is mid-stream, proving the remove callback
// (save + registry cleanup) serializes with session input on the owner.
func TestOwnerDisconnectDuringInput(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("dave", "pw", false)
	h.seedCharacter("dave", "dave")
	sess, conn := h.connect("dave")

	lines := h.goInput(sess, 1)
	done := make(chan struct{})
	go func() {
		defer close(done)
		for i := 0; i < 50; i++ {
			lines <- "look"
		}
		close(lines)
	}()
	// Hard-remove while input is streaming; the onRemove callback mutates
	// player state and must not race with HandleSession.
	for i := 0; i < 50; i++ {
		h.hub.PlayersInRoom(1)
		if i == 10 {
			h.hub.HardRemove(sess)
		}
	}
	<-done
	h.barrier()
	_ = conn
}