""" 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}")