Skip to content

fix: revive the shared game ticker if its worker dies mid-game - #12

Open
Rl0007 wants to merge 1 commit into
loadtestfrom
fix/self-healing-ticker
Open

fix: revive the shared game ticker if its worker dies mid-game#12
Rl0007 wants to merge 1 commit into
loadtestfrom
fix/self-healing-ticker

Conversation

@Rl0007

@Rl0007 Rl0007 commented Aug 22, 2026

Copy link
Copy Markdown

Problem

At ~1000 concurrent players a game freezes: the question shows, the timer hits 0, and the quiz stops advancing. Reproduced deterministically with a 1000-player load harness against a small (Frappe Cloud-sized) instance. The session sits Active and frozen (or gets force-ended by the abandoned-guard with current_question = -1), and no error is logged.

Cause

Every live game is driven by a single shared, deduplicated ticker job (run_ticker, job_id="qz_ticker"). On a small instance, the 1000-answer flood at a question close pushes the worker over its memory limit and the kernel OOM-kills the process — which is why there's no Python traceback. Nothing re-enqueues the loop, so every game it was driving freezes exactly where it stood. (A deploy/worker restart has the same effect.)

Fix

ensure_ticker_running() — a deduplicated re-enqueue that is a no-op while the loop is alive, and otherwise starts a fresh ticker that resumes every game from the Redis state it left behind. Driven from:

  • the scheduler (every minute) — baseline recovery, and
  • the host poll (get_host_state) — recovery within a single poll, since that's the most frequent caller.

No state model changes; a revived ticker picks up each session's saved phase/next_ts and continues.

Tests

Added TestTickerRecovery: revives (deduplicated qz_ticker enqueue) when a game is live, no-op when none are.

Not covered here

This restores the driver after a kill; it does not prevent the OOM. Reducing the per-close memory/DB footprint so the worker survives 1000 answers in the first place is a good follow-up.

Every live game is driven by a single shared, deduplicated ticker job. If that
worker is lost mid-game — an OOM under a large answer flood on a small instance,
a deploy or worker restart — nothing advances the games and they freeze exactly
where they stood, with no exception to log (an OOM leaves no traceback). The
host then only sees the game stall, or the abandoned-guard ends it early.

Add ensure_ticker_running(), a deduplicated re-enqueue that is a no-op while the
loop is alive and otherwise starts a fresh one that resumes every game from the
Redis state it left behind. Drive it from the scheduler (every minute) and the
host poll (recovery within a poll), so a killed ticker comes back without a human.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant