"""Self-rescheduling heartbeat, replacing pplace's standalone `watch_timer` management command with the project's existing self-rescheduling django-tasks pattern (see submissions.tasks.sync_event_game_scopes_task). Resets any event's timer once it expires - even if nobody presses or reconnects to notice - and gives already-connected clients a periodic tick. Note this is a safety net, not the only way the timer gets reset: every press and every new connection (see consumers.py) also calls reset_if_expired(), so the game works correctly even if this chain is never started or dies - connected-but-idle clients just won't see the countdown restart until someone acts. """ from datetime import timedelta from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.core.cache import cache from django.db import transaction from django.tasks import task from django.utils import timezone from games.models import EventGame from .broadcast import clock_group from .models import ClockTimer from .wire import to_epoch_ms HEARTBEAT_INTERVAL_SECONDS = 1 # One chain ticks every event with the game enabled - this lock keeps a # second chain from starting underneath an already-running one (e.g. two # EventGame saves close together). Refreshed every tick, wide enough that a # single slow tick doesn't let a second chain slip in. HEARTBEAT_LOCK_KEY = "clock:heartbeat:lock" HEARTBEAT_LOCK_TIMEOUT_SECONDS = HEARTBEAT_INTERVAL_SECONDS * 5 @task def clock_heartbeat_task(): event_games = list( EventGame.objects.filter(game__path="clock", enabled=True).select_related( "event" ) ) if not event_games: # Nothing left to tick - let the chain die. start_clock_heartbeat() # starts a fresh one next time an event enables the game. cache.delete(HEARTBEAT_LOCK_KEY) return channel_layer = get_channel_layer() for event_game in event_games: event = event_game.event ClockTimer.current(event) with transaction.atomic(): timer = ClockTimer.objects.select_for_update().get(event=event) timer.reset_if_expired() async_to_sync(channel_layer.group_send)( clock_group(event.path), { "type": "tick", "end": to_epoch_ms(timer.end_time), "now": to_epoch_ms(timezone.now()), "pressed": False, }, ) cache.set(HEARTBEAT_LOCK_KEY, 1, timeout=HEARTBEAT_LOCK_TIMEOUT_SECONDS) clock_heartbeat_task.using( run_after=timezone.now() + timedelta(seconds=HEARTBEAT_INTERVAL_SECONDS) ).enqueue() def start_clock_heartbeat(): """Enqueues the heartbeat chain, unless one is already running.""" if cache.add(HEARTBEAT_LOCK_KEY, 1, timeout=HEARTBEAT_LOCK_TIMEOUT_SECONDS): clock_heartbeat_task.enqueue()