Improve Redis key handling and spectator sync logic

Enhances Redis key type checks and cleanup in message system, adds periodic cleanup task, and improves error handling for Redis operations. Refines multiplayer and spectator hub logic to better synchronize player states and prevent invalid spectator sessions. Adds more detailed logging for channel/user join/leave events and spectator watch requests.
This commit is contained in:
咕谷酱
2025-08-22 23:53:53 +08:00
committed by MingxuanGame
parent d4f542c64b
commit 5959254de6
5 changed files with 166 additions and 30 deletions

View File

@@ -1018,6 +1018,18 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
played_user,
ex=3600,
)
# Ensure spectator hub is aware of all active players for the new game.
# This helps spectators receive score data for every participant,
# especially in subsequent rounds where state may get out of sync.
for room_user in room.room.users:
if (client := self.get_client_by_id(str(room_user.user_id))) is not None:
try:
await self._sync_with_spectator_hub(client, room)
except Exception as e:
logger.debug(
f"[MultiplayerHub] Failed to resync spectator hub for user {room_user.user_id}: {e}"
)
else:
await room.queue.finish_current_item()

View File

@@ -278,7 +278,19 @@ class SpectatorHub(Hub[StoreClientState]):
user_id = int(client.connection_id)
store = self.get_or_create_state(client)
if store.state is not None:
return
logger.warning(f"[SpectatorHub] User {user_id} began new session without ending previous one; cleaning up")
try:
await self._end_session(user_id, store.state, store)
from app.router.private.stats import remove_playing_user
bg_tasks.add_task(remove_playing_user, user_id)
finally:
store.state = None
store.beatmap_status = None
store.checksum = None
store.ruleset_id = None
store.score_token = None
store.score = None
if state.beatmap_id is None or state.ruleset_id is None:
return
@@ -540,27 +552,33 @@ class SpectatorHub(Hub[StoreClientState]):
try:
# Get target user's current state if it exists
target_store = self.state.get(target_id)
if target_store and target_store.state:
# CRITICAL FIX: Only send state if user is actually playing
# Don't send state for finished/quit games
if target_store.state.state == SpectatedUserState.Playing:
logger.debug(f"[SpectatorHub] {target_id} is currently playing, sending state")
# Send current state to the watcher immediately
await self.call_noblock(
client,
"UserBeganPlaying",
target_id,
target_store.state,
)
else:
logger.debug(
f"[SpectatorHub] {target_id} state is {target_store.state.state}, not sending to watcher"
)
if not target_store or not target_store.state:
logger.info(f"[SpectatorHub] Rejecting watch request for {target_id}: user not playing")
raise InvokeException("Target user is not currently playing")
if target_store.state.state != SpectatedUserState.Playing:
logger.info(
f"[SpectatorHub] Rejecting watch request for {target_id}: state is {target_store.state.state}"
)
raise InvokeException("Target user is not currently playing")
logger.debug(f"[SpectatorHub] {target_id} is currently playing, sending state")
# Send current state to the watcher immediately
await self.call_noblock(
client,
"UserBeganPlaying",
target_id,
target_store.state,
)
except InvokeException:
# Re-raise to inform caller without adding to group
raise
except Exception as e:
# User isn't tracked or error occurred - this is not critical
logger.debug(f"[SpectatorHub] Could not get state for {target_id}: {e}")
raise InvokeException("Target user is not currently playing") from e
# Add watcher to our tracked users
# Add watcher to our tracked users only after validation
store = self.get_or_create_state(client)
store.watched_user.add(target_id)