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): } }