HOTFIX: Production bot failed to start due to circular import. Root cause: utilities/dropdown.py importing from command_logic/logic_gameplay.py while logic_gameplay.py imports from utilities/dropdown.py. Solution: Created play_lock.py as standalone module containing: - release_play_lock() - safe_play_lock() Both modules now import from play_lock.py instead of each other. Error message: ImportError: cannot import name 'release_play_lock' from partially initialized module 'command_logic.logic_gameplay' (most likely due to a circular import) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""
|
|
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}"
|
|
)
|