Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
"""
|
|
Countdown scheduler for the polarity-room SAVE 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))
|