aboutsummaryrefslogtreecommitdiff
path: root/internal/game/owner.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/owner.go')
-rw-r--r--internal/game/owner.go36
1 files changed, 31 insertions, 5 deletions
diff --git a/internal/game/owner.go b/internal/game/owner.go
index df87653..2fb4777 100644
--- a/internal/game/owner.go
+++ b/internal/game/owner.go
@@ -23,6 +23,7 @@ import "sync"
// order relative to subscriber registration IDs.
type owner struct {
mu sync.Mutex
+ cond *sync.Cond
closed bool
tasks chan func()
done chan struct{}
@@ -35,6 +36,7 @@ func newOwner() *owner {
tasks: make(chan func(), ownerTaskQueue),
done: make(chan struct{}),
}
+ o.cond = sync.NewCond(&o.mu)
go o.loop()
return o
}
@@ -43,12 +45,15 @@ func (o *owner) loop() {
defer close(o.done)
for task := range o.tasks {
task()
+ // A queue slot freed up; wake one blocked submitter waiting for
+ // room (see enqueue).
+ o.cond.Signal()
}
}
-// 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.
+// submit enqueues without blocking. It returns false when the owner has
+// stopped or the queue is full; callers that need guaranteed delivery use
+// enqueue instead.
func (o *owner) submit(fn func()) bool {
o.mu.Lock()
defer o.mu.Unlock()
@@ -63,11 +68,31 @@ func (o *owner) submit(fn func()) bool {
}
}
+// enqueue submits with backpressure: it blocks while the queue is full and
+// returns false only when the owner has stopped. A full queue must NEVER
+// bypass the owner: running a task on the caller's goroutine while the
+// owner keeps executing other tasks breaks the single-owner invariant that
+// serializes all live state mutation.
+func (o *owner) enqueue(fn func()) bool {
+ o.mu.Lock()
+ defer o.mu.Unlock()
+ for !o.closed {
+ select {
+ case o.tasks <- fn:
+ return true
+ default:
+ o.cond.Wait()
+ }
+ }
+ return false
+}
+
func (o *owner) stop() {
o.mu.Lock()
if !o.closed {
o.closed = true
close(o.tasks)
+ o.cond.Broadcast()
}
o.mu.Unlock()
<-o.done
@@ -83,13 +108,14 @@ func (g *Game) run(fn func()) {
return
}
task := make(chan struct{})
- if !o.submit(func() {
+ if !o.enqueue(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.
+ // execute inline rather than block forever. A full queue never
+ // takes this path: enqueue applies backpressure instead.
fn()
return
}