""" 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))