66 lines
2.4 KiB
Python
66 lines
2.4 KiB
Python
"""Ends the game at PlaceSettings.placement_ends_at: a task queued to run at
|
|
that time broadcasts the wipe to everyone connected, so they all see the
|
|
board cleared at the same moment (no client-side timer to drift).
|
|
|
|
The wipe is only visual. Nothing is deleted, the Redis canvas still holds the
|
|
final board (admin stats, heatmaps, resync keep working), it's just that the
|
|
canvas endpoint serves a blank board once placement is over (see api.py).
|
|
"""
|
|
|
|
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.tasks import task
|
|
from django.utils import timezone
|
|
|
|
from .broadcast import place_group
|
|
from .models import PlaceSettings
|
|
from .wire import to_epoch_ms
|
|
|
|
# Kept a bit past the end time, so re-saving the settings with the same end
|
|
# time (or with only the size changed) doesn't queue the same wipe twice.
|
|
WIPE_GUARD_MARGIN = timedelta(hours=1)
|
|
|
|
|
|
def _wipe_guard_key(settings_id: int, ends_at_ms: int) -> str:
|
|
return f"place:wipe:{settings_id}:{ends_at_ms}"
|
|
|
|
|
|
def schedule_wipe(place_settings: PlaceSettings) -> bool:
|
|
"""Queue the wipe for this end time, unless it's already queued or the end
|
|
time is already past. Safe to call on every save."""
|
|
ends_at = place_settings.placement_ends_at
|
|
if ends_at is None or ends_at <= timezone.now():
|
|
return False
|
|
|
|
ends_at_ms = to_epoch_ms(ends_at)
|
|
timeout = int((ends_at - timezone.now() + WIPE_GUARD_MARGIN).total_seconds())
|
|
if not cache.add(_wipe_guard_key(place_settings.id, ends_at_ms), True, timeout):
|
|
return False
|
|
|
|
wipe_canvas_task.using(run_after=ends_at).enqueue(place_settings.id, ends_at_ms)
|
|
return True
|
|
|
|
|
|
@task
|
|
def wipe_canvas_task(settings_id: int, ends_at_ms: int):
|
|
"""Broadcast the wipe wave, if `ends_at_ms` is still the event's end time -
|
|
an admin moving or clearing it leaves the old job queued, it just stops
|
|
here."""
|
|
place_settings = (
|
|
PlaceSettings.objects.filter(id=settings_id).select_related("event").first()
|
|
)
|
|
if place_settings is None or place_settings.placement_ends_at is None:
|
|
return
|
|
if to_epoch_ms(place_settings.placement_ends_at) != ends_at_ms:
|
|
return
|
|
|
|
channel_layer = get_channel_layer()
|
|
if channel_layer is None:
|
|
return
|
|
async_to_sync(channel_layer.group_send)(
|
|
place_group(place_settings.event.path), {"type": "wipe"}
|
|
)
|