Early returns in log_chaos, log_sac_bunt, and log_stealing left play locks permanently stuck because the lock was acquired but never released. The new locked_play async context manager wraps checks_log_interaction() and guarantees lock release on exception, early return, or normal exit. Migrated all 18 locking commands in gameplay.py and removed redundant double-locking in end_game_command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
65 lines
2.7 KiB
Python
65 lines
2.7 KiB
Python
"""
|
|
Async context manager for play locking. Wraps checks_log_interaction()
|
|
and guarantees lock release on exception, early return, or normal exit.
|
|
|
|
This module sits above play_lock and logic_gameplay to avoid circular imports.
|
|
"""
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from sqlmodel import Session
|
|
|
|
from play_lock import release_play_lock
|
|
from command_logic.logic_gameplay import checks_log_interaction
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def locked_play(session: Session, interaction, command_name: str):
|
|
"""
|
|
Async context manager that acquires a play lock via checks_log_interaction
|
|
and guarantees release when the block exits.
|
|
|
|
Usage:
|
|
with Session(engine) as session:
|
|
async with locked_play(session, interaction, 'log flyball') as (game, team, play):
|
|
play = await flyballs(session, interaction, play, flyball_type)
|
|
await self.complete_and_post_play(session, interaction, play, ...)
|
|
|
|
Behavior:
|
|
- If checks_log_interaction raises (PlayLockedException, etc.), no lock
|
|
was acquired and the exception propagates naturally.
|
|
- If complete_play() was called, play.complete=True, so finally is a no-op.
|
|
- If the block raises an exception, the lock is released and the exception re-raised.
|
|
- If the block returns early (e.g. validation failure), the lock is released.
|
|
"""
|
|
# If this raises, no lock was acquired - exception propagates naturally
|
|
this_game, owner_team, this_play = await checks_log_interaction(
|
|
session, interaction, command_name=command_name, lock_play=True
|
|
)
|
|
|
|
try:
|
|
yield this_game, owner_team, this_play
|
|
except Exception as e:
|
|
if this_play.locked and not this_play.complete:
|
|
logger.error(
|
|
f"Exception in '{command_name}' on play {this_play.id}, releasing lock: {e}"
|
|
)
|
|
try:
|
|
release_play_lock(session, this_play)
|
|
except Exception as release_err:
|
|
logger.error(f"Failed to release lock on play {this_play.id}: {release_err}")
|
|
raise
|
|
finally:
|
|
# Catches early returns (the primary bug fix)
|
|
# No-op if complete_play() already set complete=True and locked=False
|
|
if this_play.locked and not this_play.complete:
|
|
logger.warning(
|
|
f"Play {this_play.id} still locked after '{command_name}', "
|
|
f"releasing (likely early return)"
|
|
)
|
|
try:
|
|
release_play_lock(session, this_play)
|
|
except Exception as release_err:
|
|
logger.error(f"Failed to force release lock on play {this_play.id}: {release_err}")
|