|
| 1 | +import datetime |
| 2 | +import os |
| 3 | + |
| 4 | +from django.core.management.base import BaseCommand |
| 5 | +from django.db import transaction |
| 6 | +from django.db.models import Max, Subquery, OuterRef |
| 7 | + |
| 8 | +from app.models.conference import Conference |
| 9 | +from app.models.generic_event import GenericEvent |
| 10 | + |
| 11 | + |
| 12 | +DEFAULT_TIMEOUT_HOURS = 4 |
| 13 | + |
| 14 | + |
| 15 | +class Command(BaseCommand): |
| 16 | + help = 'Close conferences with no activity for longer than the specified hours' |
| 17 | + |
| 18 | + def add_arguments(self, parser): |
| 19 | + parser.add_argument( |
| 20 | + '--hours', |
| 21 | + type=int, |
| 22 | + default=int(os.getenv('CONFERENCE_TIMEOUT_HOURS', DEFAULT_TIMEOUT_HOURS)), |
| 23 | + help=f'Close conferences with no activity for this many hours (default: {DEFAULT_TIMEOUT_HOURS}, env: CONFERENCE_TIMEOUT_HOURS)', |
| 24 | + ) |
| 25 | + parser.add_argument( |
| 26 | + '--dry-run', |
| 27 | + action='store_true', |
| 28 | + help='Show what would be closed without making changes', |
| 29 | + ) |
| 30 | + |
| 31 | + def handle(self, *args, **options): |
| 32 | + hours = options['hours'] |
| 33 | + dry_run = options['dry_run'] |
| 34 | + cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=hours) |
| 35 | + now = datetime.datetime.utcnow() |
| 36 | + |
| 37 | + # Single annotated query to get last activity per conference (avoids N+1) |
| 38 | + ongoing = Conference.objects.filter(ongoing=True).annotate( |
| 39 | + last_event_at=Subquery( |
| 40 | + GenericEvent.objects.filter(conference=OuterRef('pk')) |
| 41 | + .order_by('-created_at') |
| 42 | + .values('created_at')[:1] |
| 43 | + ), |
| 44 | + last_connection_at=Max('connections__created_at'), |
| 45 | + ) |
| 46 | + |
| 47 | + if not ongoing.exists(): |
| 48 | + self.stdout.write('No ongoing conferences found.') |
| 49 | + return |
| 50 | + |
| 51 | + stale = [] |
| 52 | + for conference in ongoing: |
| 53 | + last_activity = conference.last_event_at or conference.last_connection_at or conference.created_at |
| 54 | + |
| 55 | + if last_activity < cutoff: |
| 56 | + idle = now - last_activity |
| 57 | + stale.append((conference, last_activity, idle)) |
| 58 | + |
| 59 | + if not stale: |
| 60 | + self.stdout.write(f'{ongoing.count()} ongoing conferences found, all have recent activity.') |
| 61 | + return |
| 62 | + |
| 63 | + self.stdout.write(f'Found {len(stale)} conferences with no activity for more than {hours} hours.') |
| 64 | + |
| 65 | + closed = 0 |
| 66 | + failed = 0 |
| 67 | + |
| 68 | + for conference, last_activity, idle in stale: |
| 69 | + self.stdout.write(f' {conference.id} ({conference.conference_name or conference.conference_id}) - last activity {idle} ago') |
| 70 | + |
| 71 | + if not dry_run: |
| 72 | + try: |
| 73 | + with transaction.atomic(): |
| 74 | + for connection in conference.connections.filter(end_time__isnull=True): |
| 75 | + connection.end(now) |
| 76 | + connection.save() |
| 77 | + |
| 78 | + for session in conference.sessions.filter(end_time__isnull=True): |
| 79 | + session.should_stop_call(now) |
| 80 | + session.save() |
| 81 | + |
| 82 | + conference.should_stop_call(now) |
| 83 | + conference.save() |
| 84 | + |
| 85 | + closed += 1 |
| 86 | + except Exception as e: |
| 87 | + failed += 1 |
| 88 | + self.stderr.write(f' Error closing {conference.id}: {e}') |
| 89 | + |
| 90 | + if dry_run: |
| 91 | + self.stdout.write(f'\nDry run - no changes made. Run without --dry-run to close these conferences.') |
| 92 | + else: |
| 93 | + self.stdout.write(f'\nClosed {closed} stale conferences.' + (f' {failed} failed.' if failed else '')) |
0 commit comments