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

import "sync"

// owner is the single execution authority for live game state. Every
// mutation of players, sessions, the Hub membership, runtime maps, and all
// tick callbacks runs on exactly one goroutine (the loop below). Network and
// admin goroutines submit tasks; they never touch live objects directly.
//
// Rules:
//   - Game.run blocks the caller until the closure has executed on the
//     owner goroutine. Connection goroutines use it per input line, which
//     gives natural per-client backpressure without ever blocking another
//     client: the owner stays free because network I/O happens outside
//     (Session writes go through outbound queues, saves serialize copies).
//   - Code already executing on the owner must NOT call Game.run (it would
//     deadlock waiting for itself). Owner-executed code calls functions
//     directly.
//   - Task ordering is FIFO: submitted input lines, disconnect callbacks,
//     and admin mutations execute in submission order. Tick callbacks
//     serialize against these tasks the same way (the engine's ticker
//     goroutine submits each callback), preserving the documented tick
//     order relative to subscriber registration IDs.
type owner struct {
	mu     sync.Mutex
	closed bool
	tasks  chan func()
	done   chan struct{}
}

const ownerTaskQueue = 4096

func newOwner() *owner {
	o := &owner{
		tasks: make(chan func(), ownerTaskQueue),
		done:  make(chan struct{}),
	}
	go o.loop()
	return o
}

func (o *owner) loop() {
	defer close(o.done)
	for task := range o.tasks {
		task()
	}
}

// submit enqueues a task; it returns false once the owner has stopped.
// The closed flag is guarded by the mutex so a submit racing with stop
// cannot send on a closed channel.
func (o *owner) submit(fn func()) bool {
	o.mu.Lock()
	defer o.mu.Unlock()
	if o.closed {
		return false
	}
	select {
	case o.tasks <- fn:
		return true
	default:
		return false
	}
}

func (o *owner) stop() {
	o.mu.Lock()
	if !o.closed {
		o.closed = true
		close(o.tasks)
	}
	o.mu.Unlock()
	<-o.done
}

// run executes fn on the owner goroutine and waits for it. When no owner
// exists (synthetic Games in unit tests) fn runs inline on the caller's
// goroutine; production Games always have one, started by New.
func (g *Game) run(fn func()) {
	o := g.owner
	if o == nil {
		fn()
		return
	}
	task := make(chan struct{})
	if !o.submit(func() {
		fn()
		close(task)
	}) {
		// The owner loop has stopped (graceful shutdown in progress).
		// Admissions are already closed, so only stragglers reach this:
		// execute inline rather than block forever.
		fn()
		return
	}
	<-task
}

// tryRun executes fn on the owner if it is currently idle-draining; it is
// not currently used for correctness paths but exists for future non-blocking
// administrative reads. It returns false when the queue is full.
func (g *Game) tryRun(fn func()) bool {
	o := g.owner
	if o == nil {
		fn()
		return true
	}
	return o.submit(fn)
}

// stopOwner drains and stops the owner loop. Called once during graceful
// shutdown after all entry points have stopped submitting.
func (g *Game) stopOwner() {
	g.ownerStop.Do(func() {
		if g.owner != nil {
			g.owner.stop()
		}
	})
}