wired PICK SKY server-side polarity countdown via threading.Timer (tasks.py); fixed polarity_done overlay gating on refresh; cleared sig-select floats on overlay dismiss; filtered Redact events from Most Recent applet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
95
src/apps/epic/tasks.py
Normal file
95
src/apps/epic/tasks.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Countdown scheduler for the polarity-room TAKE SIG gate.
|
||||
|
||||
Uses threading.Timer so no separate Celery worker is needed in development.
|
||||
Single-process only — swap for a Celery task if production uses multiple
|
||||
web workers (gunicorn -w N with N > 1).
|
||||
"""
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.core.cache import cache
|
||||
|
||||
_LEVITY_ROLES = {'PC', 'NC', 'SC'}
|
||||
_GRAVITY_ROLES = {'BC', 'EC', 'AC'}
|
||||
|
||||
# In-process registry of pending timers: "{room_id}_{polarity}" → Timer
|
||||
_timers = {}
|
||||
|
||||
|
||||
def _cache_key(room_id, polarity):
|
||||
return f'sig_countdown_{room_id}_{polarity}'
|
||||
|
||||
|
||||
def _group_send(room_id, msg):
|
||||
async_to_sync(get_channel_layer().group_send)(f'room_{room_id}', msg)
|
||||
|
||||
|
||||
def _fire(room_id, polarity, token):
|
||||
"""Callback run by threading.Timer after the countdown expires."""
|
||||
# Token guard: if cancelled or superseded, cache entry will differ
|
||||
if cache.get(_cache_key(room_id, polarity)) != token:
|
||||
return
|
||||
|
||||
from apps.epic.models import Room, SigReservation
|
||||
|
||||
try:
|
||||
room = Room.objects.get(id=room_id)
|
||||
except Room.DoesNotExist:
|
||||
return
|
||||
|
||||
if room.table_status != Room.SIG_SELECT:
|
||||
return
|
||||
|
||||
polarity_roles = _LEVITY_ROLES if polarity == SigReservation.LEVITY else _GRAVITY_ROLES
|
||||
|
||||
# Idempotency: seats already assigned
|
||||
if not room.table_seats.filter(role__in=polarity_roles, significator__isnull=True).exists():
|
||||
return
|
||||
|
||||
# Safety: all three must still be ready
|
||||
ready_reservations = list(
|
||||
SigReservation.objects.filter(room=room, polarity=polarity, ready=True)
|
||||
.select_related('seat', 'card')
|
||||
)
|
||||
if len(ready_reservations) < 3:
|
||||
return
|
||||
|
||||
for res in ready_reservations:
|
||||
if res.seat:
|
||||
res.seat.significator = res.card
|
||||
res.seat.save(update_fields=['significator'])
|
||||
|
||||
SigReservation.objects.filter(room=room, polarity=polarity).update(countdown_remaining=None)
|
||||
|
||||
_group_send(room_id, {'type': 'polarity_room_done', 'polarity': polarity})
|
||||
|
||||
if not room.table_seats.filter(significator__isnull=True).exists():
|
||||
Room.objects.filter(id=room_id).update(table_status=Room.SKY_SELECT)
|
||||
_group_send(room_id, {'type': 'pick_sky_available'})
|
||||
|
||||
cache.delete(_cache_key(room_id, polarity))
|
||||
_timers.pop(f'{room_id}_{polarity}', None)
|
||||
|
||||
|
||||
def schedule_polarity_confirm(room_id, polarity, seconds):
|
||||
"""Schedule a polarity confirm `seconds` seconds from now. Cancels any prior timer."""
|
||||
cancel_polarity_confirm(room_id, polarity)
|
||||
|
||||
token = str(uuid.uuid4())
|
||||
cache.set(_cache_key(room_id, polarity), token, timeout=int(seconds) + 60)
|
||||
|
||||
timer = threading.Timer(seconds, _fire, args=[str(room_id), polarity, token])
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
_timers[f'{room_id}_{polarity}'] = timer
|
||||
|
||||
|
||||
def cancel_polarity_confirm(room_id, polarity):
|
||||
"""Cancel any pending confirm for this room + polarity."""
|
||||
timer = _timers.pop(f'{room_id}_{polarity}', None)
|
||||
if timer:
|
||||
timer.cancel()
|
||||
cache.delete(_cache_key(room_id, polarity))
|
||||
Reference in New Issue
Block a user