@@ -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:
|
||||
|
||||
@@ -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')
|
||||
))()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user