aboutsummaryrefslogtreecommitdiff
path: root/internal/game/owner_regression_test.go
blob: 3ba4bd4a70d3a643749ec9f775a6e2c9ebb6d243 (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
package game

// Single-owner serialization regressions: a full task queue applies
// backpressure instead of bypassing the owner, and the owner-stop path
// keeps its documented inline fallback for shutdown stragglers only.

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

// TestFullOwnerQueueAppliesBackpressure: when the owner's task queue is
// full and the owner is blocked inside another task, run() must NOT execute
// the new task inline on the caller's goroutine — that would mutate live
// state concurrently with the owner. It blocks until the owner drains.
func TestFullOwnerQueueAppliesBackpressure(t *testing.T) {
	g := &Game{owner: newOwner()}
	release := make(chan struct{})
	started := make(chan struct{})
	g.owner.submit(func() {
		close(started)
		<-release
	})
	<-started
	var once sync.Once
	t.Cleanup(func() {
		once.Do(func() { close(release) })
		g.stopOwner()
	})

	for i := 0; i < ownerTaskQueue; i++ {
		if !g.owner.submit(func() {}) {
			t.Fatal("setup: could not fill the owner queue")
		}
	}

	ran := make(chan struct{})
	go g.run(func() { close(ran) })
	select {
	case <-ran:
		t.Fatal("task executed inline while the owner was still running other tasks")
	case <-time.After(100 * time.Millisecond):
	}

	// Release the owner: the queued work drains and the blocked task runs.
	once.Do(func() { close(release) })
	select {
	case <-ran:
	case <-time.After(5 * time.Second):
		t.Fatal("task never ran after the owner drained its queue")
	}
}

// TestStoppedOwnerDropsStragglers: after the owner stops (graceful
// shutdown), straggler submissions are dropped, never executed inline on the
// caller's goroutine (which would mutate live state concurrently with the
// shutdown sequence and after the final-save barrier) and never block
// forever.
func TestStoppedOwnerDropsStragglers(t *testing.T) {
	g := &Game{owner: newOwner()}
	g.stopOwner()
	ran := make(chan struct{})
	go g.run(func() { close(ran) })
	select {
	case <-ran:
		t.Fatal("straggler task executed after the owner stopped; it must be dropped")
	case <-time.After(100 * time.Millisecond):
	}
}