""" Play locking utilities to prevent concurrent play modifications. This module is separate to avoid circular imports between logic_gameplay and utilities. """ import logging from contextlib import contextmanager from sqlmodel import Session logger = logging.getLogger("discord_app") def release_play_lock(session: Session, play: "Play") -> None: """ Release a play lock and commit the change. This is a safety mechanism to ensure locks are always released, even when exceptions occur during command processing. """ if play.locked: logger.info(f"Releasing lock on play {play.id}") play.locked = False session.add(play) session.commit() @contextmanager def safe_play_lock(session: Session, play: "Play"): """ Context manager for safely handling play locks. Ensures the lock is ALWAYS released even if an exception occurs during command processing. This prevents permanent user lockouts. Usage: with safe_play_lock(session, this_play): # Do command processing # Lock will be released automatically on exception or normal exit pass Note: This only releases the lock on exception. On successful completion, complete_play() should handle releasing the lock and marking the play complete. """ lock_released = False try: yield play except Exception as e: # Release lock on any exception if play.locked and not play.complete: logger.error( f"Exception during play {play.id} processing, releasing lock. Error: {e}" ) play.locked = False session.add(play) try: session.commit() lock_released = True except Exception as commit_error: logger.error( f"Failed to release lock on play {play.id}: {commit_error}" ) raise # Re-raise the original exception finally: # Final safety check if not lock_released and play.locked and not play.complete: logger.warning( f"Play {play.id} still locked after processing, forcing unlock" ) play.locked = False session.add(play) try: session.commit() except Exception as commit_error: logger.error( f"Failed to force unlock play {play.id}: {commit_error}" )