diff options
Diffstat (limited to 'internal/game/owner.go')
| -rw-r--r-- | internal/game/owner.go | 36 |
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 } |
