diff options
Diffstat (limited to 'internal/game/owner_regression_test.go')
| -rw-r--r-- | internal/game/owner_regression_test.go | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/internal/game/owner_regression_test.go b/internal/game/owner_regression_test.go new file mode 100644 index 0000000..1ccd25f --- /dev/null +++ b/internal/game/owner_regression_test.go @@ -0,0 +1,68 @@ +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") + } +} + +// TestStoppedOwnerRunsStragglersInline: after the owner stops (graceful +// shutdown), straggler submissions execute inline rather than blocking +// forever — the documented admissions-closed behavior. +func TestStoppedOwnerRunsStragglersInline(t *testing.T) { + g := &Game{owner: newOwner()} + g.stopOwner() + ran := make(chan struct{}) + go g.run(func() { close(ran) }) + select { + case <-ran: + case <-time.After(5 * time.Second): + t.Fatal("straggler task blocked forever after the owner stopped") + } +} |
