paper-dynasty-discord/exceptions.py
Cal Corum 90d7345850 Implement play locking to prevent concurrent command processing
Adds idempotency guard to prevent race conditions when multiple users
submit commands for the same play simultaneously.

Changes:
- Add PlayLockedException for locked play detection
- Implement lock check in checks_log_interaction()
- Acquire lock (play.locked = True) before processing commands
- Release lock (play.locked = False) after play completion
- Add warning logs for rejected duplicate submissions
- Add /diagnostics endpoint to health server for debugging

This prevents database corruption and duplicate processing when users
spam commands like "log xcheck" while the first is still processing.

Tested successfully in Discord - duplicate commands now properly return
PlayLockedException with instructions to wait.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 23:13:40 -06:00

143 lines
2.6 KiB
Python

import logging
from typing import Literal
logger = logging.getLogger("discord_app")
def log_errors(func):
"""
This wrapper function will force all exceptions to be logged with execution and stack info.
"""
def wrap(*args, **kwargs):
try:
result = func(*args, **kwargs)
except Exception as e:
logger.error(func.__name__)
log_exception(e)
return result # type: ignore
return wrap
def log_exception(
e: Exception,
msg: str = "",
level: Literal["debug", "error", "info", "warn"] = "error",
):
if level == "debug":
logger.debug(msg, exc_info=True, stack_info=True)
elif level == "error":
logger.error(msg, exc_info=True, stack_info=True)
elif level == "info":
logger.info(msg, exc_info=True, stack_info=True)
else:
logger.warning(msg, exc_info=True, stack_info=True)
# Check if 'e' is an exception class or instance
if isinstance(e, Exception):
raise e # If 'e' is already an instance of an exception
else:
raise e(msg) # If 'e' is an exception class
class GameException(Exception):
pass
class LineupsMissingException(GameException):
pass
class CardLegalityException(GameException):
pass
class CardNotFoundException(GameException):
pass
class GameNotFoundException(GameException):
pass
class TeamNotFoundException(GameException):
pass
class PlayNotFoundException(GameException):
pass
class PlayerNotFoundException(GameException):
pass
class PlayInitException(GameException):
pass
class DatabaseError(GameException):
pass
class APITimeoutError(DatabaseError):
"""Raised when an API call times out after all retries."""
pass
class PositionNotFoundException(GameException):
pass
class NoPlayerResponseException(GameException):
pass
class MultipleHumanTeamsException(GameException):
pass
class NoHumanTeamsException(GameException):
pass
class GoogleSheetsException(GameException):
pass
class InvalidResultException(GameException):
pass
class ButtonOptionNotChosen(Exception):
pass
class MissingRoleException(GameException):
pass
class MissingRosterException(GameException):
pass
class LegalityCheckNotRequired(GameException):
pass
class InvalidResponder(GameException):
pass
class PlayLockedException(GameException):
"""
Raised when attempting to process a play that is already locked by another interaction.
This prevents concurrent modification of the same play record, which could cause
database deadlocks or data corruption.
"""
pass