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>
143 lines
2.6 KiB
Python
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
|