From 7faf931fb21cc5611f686ff29e73f274a89590f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhn-Vitus=20Sa=C3=9F?= Date: Mon, 21 Apr 2025 13:35:51 +0200 Subject: [PATCH] - Entfernen des automatischen Kick-Mechanismus - Fix #105 --- README.md | 7 ++ .../management/commands/cleanup_games.py | 13 ++ django/play/consumers/game.py | 113 +----------------- django/play/consumers/lobby.py | 64 +--------- .../migrations/0011_quizgame_updated_at.py | 18 +++ django/play/models.py | 1 + 6 files changed, 46 insertions(+), 170 deletions(-) create mode 100644 django/library/management/commands/cleanup_games.py create mode 100644 django/play/migrations/0011_quizgame_updated_at.py diff --git a/README.md b/README.md index bfeb1ba..9f38559 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,13 @@ Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP - `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces - `-p 8000`: Port (anpassbar) +4. Cronjob für Bereinigung der Spiele in der Datenbank + - Zum bearbeiten der Cronjobs: + ```sh + crontab -e + ``` + - Dann `0 0 * * * /path/to/venv/bin/python /path/to/your_project/manage.py cleanup_games` einfügen, damit um Mitternacht alle Spiele gelöscht werden + Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden! ## Superuser erstellen diff --git a/django/library/management/commands/cleanup_games.py b/django/library/management/commands/cleanup_games.py new file mode 100644 index 0000000..e5f1d42 --- /dev/null +++ b/django/library/management/commands/cleanup_games.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand +from play.models import QuizGame +from django.utils import timezone + +class Command(BaseCommand): + help = 'Bereinigt beendete Spiele' + + def handle(self, *args, **kwargs): + expired_games = QuizGame.objects.filter( + updated_at__lt=timezone.now() - timezone.timedelta(minutes=5)) + deleted_count = expired_games.count() + expired_games.delete() + self.stdout.write(f'{deleted_count} alte Spiele gelöscht.') diff --git a/django/play/consumers/game.py b/django/play/consumers/game.py index 841a8a9..b764851 100644 --- a/django/play/consumers/game.py +++ b/django/play/consumers/game.py @@ -9,75 +9,7 @@ from datetime import timedelta from play.models import QuizGame, QuizGameParticipant, QuizAnswer -# Dictionary to track inactive check tasks per game -game_check_tasks = {} - - class GameConsumer(AsyncWebsocketConsumer): - @classmethod - async def start_inactive_check(cls, join_code, channel_layer): - """Start the inactive player check for a game if not already running.""" - if join_code not in game_check_tasks or game_check_tasks[join_code].done(): - async def check_inactive_players(): - game_group_name = f'game_{join_code}' - while True: - try: - await asyncio.sleep(30) # Check every 30 seconds - try: - game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code) - cutoff_time = timezone.now() - timedelta(minutes=1) - - # Get active and inactive participants - all_participants = await database_sync_to_async(lambda: list( - game.participants.all().values('participant_id', 'display_name', 'last_heartbeat') - ))() - - # Konvertiere datetime zu ISO Format String - for p in all_participants: - if p['last_heartbeat']: - p['last_heartbeat'] = p['last_heartbeat'].isoformat() - - active_participants = [p for p in all_participants if p['last_heartbeat'] and timezone.datetime.fromisoformat(p['last_heartbeat']) >= cutoff_time] - inactive_participants = [p for p in all_participants if not p['last_heartbeat'] or timezone.datetime.fromisoformat(p['last_heartbeat']) < cutoff_time] - - # Remove inactive participants - if inactive_participants: - for p in inactive_participants: - # Benachrichtige andere über den gekickten Spieler - await channel_layer.group_send( - game_group_name, - { - 'type': 'player_left', - 'player_name': p['display_name'], - 'was_kicked': True - } - ) - # Lösche den inaktiven Teilnehmer - await database_sync_to_async(QuizGameParticipant.objects.filter( - participant_id=p['participant_id'] - ).delete)() - - # Broadcast update to all clients - if active_participants or inactive_participants: - await channel_layer.group_send( - game_group_name, - { - 'type': 'participant_list_update', - 'participants': active_participants - } - ) - except QuizGame.DoesNotExist: - break # Stop checking if game no longer exists - except Exception as e: - print(f'Error checking inactive players in game: {e}') - await asyncio.sleep(5) # Wait before retry - except asyncio.CancelledError: - break - except Exception as e: - print(f'Error in game inactive check loop: {e}') - await asyncio.sleep(5) - - game_check_tasks[join_code] = asyncio.create_task(check_inactive_players()) async def connect(self): self.join_code = self.scope['url_route']['kwargs']['join_code'] @@ -90,9 +22,6 @@ class GameConsumer(AsyncWebsocketConsumer): ) await self.accept() - - # Ensure inactive check is running for this game - await self.start_inactive_check(self.join_code, self.channel_layer) async def disconnect(self, close_code): # Leave game group @@ -145,21 +74,6 @@ class GameConsumer(AsyncWebsocketConsumer): except QuizGameParticipant.DoesNotExist: pass return - - if message_type == 'heartbeat': - participant_id = text_data_json.get('participant_id') - if participant_id: - await self.update_participant_heartbeat(participant_id) - # Update participants list after heartbeat - participants = await self.get_participants() - await self.channel_layer.group_send( - self.game_group_name, - { - 'type': 'participant_list_update', - 'participants': participants - } - ) - return if message_type == 'submit_rating': participant_id = text_data_json['participant_id'] @@ -250,17 +164,6 @@ class GameConsumer(AsyncWebsocketConsumer): 'redirect_url': event.get('redirect_url') })) - async def update_participant_heartbeat(self, participant_id): - """Update the last heartbeat timestamp for a participant.""" - try: - participant = await database_sync_to_async(QuizGameParticipant.objects.get)( - participant_id=participant_id - ) - participant.last_heartbeat = timezone.now() - await database_sync_to_async(participant.save)() - except QuizGameParticipant.DoesNotExist: - pass - @database_sync_to_async def verify_host(self, host_id): try: @@ -368,21 +271,17 @@ class GameConsumer(AsyncWebsocketConsumer): def cleanup_game(self): try: game = QuizGame.objects.get(join_code=self.join_code) - - # Get all active participants (heartbeat within last minute) - from django.utils import timezone + active_participants = QuizGameParticipant.objects.filter( - quiz_game=game, - last_heartbeat__gte=timezone.now() - timezone.timedelta(minutes=1) + quiz_game=game ).count() - - # If no active participants and game is finished for more than 5 minutes, delete it - if active_participants == 0 and game.current_state == 'finished': - if game.question_start_time and (timezone.now() - game.question_start_time).total_seconds() > 300: - game.delete() + + if active_participants == 0: + game.delete() except QuizGame.DoesNotExist: pass + @database_sync_to_async def get_participants(self): try: diff --git a/django/play/consumers/lobby.py b/django/play/consumers/lobby.py index cccbd9f..8fd72c1 100644 --- a/django/play/consumers/lobby.py +++ b/django/play/consumers/lobby.py @@ -9,47 +9,6 @@ game_check_tasks = {} game_connections = {} class LobbyConsumer(AsyncWebsocketConsumer): - @classmethod - async def start_inactive_check(cls, join_code, channel_layer): - """Start the inactive player check for a game if not already running.""" - if join_code not in game_check_tasks or game_check_tasks[join_code].done(): - async def check_inactive_players(): - room_group_name = f'lobby_{join_code}' - while True: - try: - await asyncio.sleep(10) # Check every 10 seconds to match frontend heartbeat - # Get participants list and broadcast update - try: - from django.utils import timezone - from datetime import timedelta - game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code) - cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats) - # Get current active participants with full info - active_participants = await database_sync_to_async(lambda: list( - game.participants.filter(last_heartbeat__gte=cutoff_time) - .values('participant_id', 'display_name') - ))() - - # Send update to all clients - await channel_layer.group_send( - room_group_name, - { - 'type': 'participants_list_update', - 'participants': active_participants - } - ) - except QuizGame.DoesNotExist: - break # Stop checking if game no longer exists - except Exception as e: - print(f'Error checking inactive players: {e}') - await asyncio.sleep(5) # Wait before retry - except asyncio.CancelledError: - break - except Exception as e: - print(f'Error in inactive check loop: {e}') - await asyncio.sleep(5) - - game_check_tasks[join_code] = asyncio.create_task(check_inactive_players()) async def connect(self): self.join_code = self.scope['url_route']['kwargs']['join_code'] @@ -63,9 +22,6 @@ class LobbyConsumer(AsyncWebsocketConsumer): game_connections[self.join_code] = set() game_connections[self.join_code].add(self.channel_name) - # Start inactive check task if not already running - await self.start_inactive_check(self.join_code, self.channel_layer) - # Join room group await self.channel_layer.group_add( self.room_group_name, @@ -107,11 +63,6 @@ class LobbyConsumer(AsyncWebsocketConsumer): })) return - if message_type == 'heartbeat': - participant_id = data.get('participant_id') - if participant_id: - await self.update_participant_heartbeat(participant_id) - # Don't update participants list after heartbeat - let the background task handle it elif message_type in ['leave_game', 'kick_player']: participant_id = data.get('participant_id') if participant_id: @@ -180,26 +131,13 @@ class LobbyConsumer(AsyncWebsocketConsumer): } ) - @database_sync_to_async - def update_participant_heartbeat(self, participant_id): - try: - participant = QuizGameParticipant.objects.get(participant_id=participant_id) - from django.utils import timezone - participant.last_heartbeat = timezone.now() - participant.save() - return True - except QuizGameParticipant.DoesNotExist: - return False - async def get_participants_list(self): try: from django.utils import timezone from datetime import timedelta game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code) - # Only return participants that have been seen in the last minute - cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats) participants = await database_sync_to_async(lambda: list( - game.participants.filter(last_heartbeat__gte=cutoff_time) + game.participants .values('participant_id', 'display_name') ))() diff --git a/django/play/migrations/0011_quizgame_updated_at.py b/django/play/migrations/0011_quizgame_updated_at.py new file mode 100644 index 0000000..4068218 --- /dev/null +++ b/django/play/migrations/0011_quizgame_updated_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-21 11:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('play', '0010_alter_quizgameparticipant_last_heartbeat'), + ] + + operations = [ + migrations.AddField( + model_name='quizgame', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/django/play/models.py b/django/play/models.py index 33feaf1..c63736d 100644 --- a/django/play/models.py +++ b/django/play/models.py @@ -19,6 +19,7 @@ class QuizGame(models.Model): current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby') current_question_index = models.IntegerField(default=0) question_start_time = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): if not self.join_code: