This commit is contained in:
Slipstream 2025-05-04 14:33:47 -06:00
parent 3178f95dd8
commit d43af26cb3
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
3 changed files with 507 additions and 105 deletions

View File

@ -428,14 +428,41 @@ try:
# Try relative import first
try:
from .cog_management_endpoints import router as cog_management_router
except ImportError:
log.info("Successfully imported cog_management_endpoints via relative import")
except ImportError as e:
log.warning(f"Relative import of cog_management_endpoints failed: {e}")
# Fall back to absolute import
try:
from cog_management_endpoints import router as cog_management_router
log.info("Successfully imported cog_management_endpoints via absolute import")
except ImportError as e2:
log.error(f"Both import attempts for cog_management_endpoints failed: {e2}")
# Try to import the module directly to see what's available
try:
import sys
log.info(f"Python path: {sys.path}")
# Try to find the module in the current directory
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
log.info(f"Current directory: {current_dir}")
files = os.listdir(current_dir)
log.info(f"Files in current directory: {files}")
# Try to import the module with a full path
sys.path.append(current_dir)
import cog_management_endpoints
log.info(f"Successfully imported cog_management_endpoints module")
router = cog_management_endpoints.router
log.info(f"Successfully got router from cog_management_endpoints")
cog_management_router = router
except Exception as e3:
log.error(f"Failed to import cog_management_endpoints module: {e3}")
raise e2
# Add the cog management router to the dashboard API app
dashboard_api_app.include_router(cog_management_router, tags=["Cog Management"])
log.info("Cog management endpoints loaded successfully")
except ImportError as e:
except Exception as e:
log.error(f"Could not import cog management endpoints: {e}")
log.error("Cog management endpoints will not be available")

View File

@ -459,36 +459,78 @@ async def get_guild_settings(
detail="Settings manager not available"
)
# Get prefix
prefix = await settings_manager.get_guild_prefix(guild_id, DEFAULT_PREFIX)
# Get welcome/goodbye settings
welcome_channel_id = await settings_manager.get_setting(guild_id, 'welcome_channel_id')
welcome_message = await settings_manager.get_setting(guild_id, 'welcome_message')
goodbye_channel_id = await settings_manager.get_setting(guild_id, 'goodbye_channel_id')
goodbye_message = await settings_manager.get_setting(guild_id, 'goodbye_message')
# Get cog enabled statuses
cogs_enabled = await settings_manager.get_all_enabled_cogs(guild_id)
# Get command enabled statuses
commands_enabled = await settings_manager.get_all_enabled_commands(guild_id)
# Construct response
# Initialize settings with defaults
settings = {
"prefix": prefix,
"welcome_channel_id": welcome_channel_id,
"welcome_message": welcome_message,
"goodbye_channel_id": goodbye_channel_id,
"goodbye_message": goodbye_message,
"cogs": cogs_enabled,
"commands": commands_enabled
"prefix": DEFAULT_PREFIX,
"welcome_channel_id": None,
"welcome_message": None,
"goodbye_channel_id": None,
"goodbye_message": None,
"cogs": {},
"commands": {}
}
# Get prefix with error handling
try:
settings["prefix"] = await settings_manager.get_guild_prefix(guild_id, DEFAULT_PREFIX)
except Exception as e:
log.warning(f"Error getting prefix for guild {guild_id}, using default: {e}")
# Keep default prefix
# Get welcome/goodbye settings with error handling
try:
settings["welcome_channel_id"] = await settings_manager.get_setting(guild_id, 'welcome_channel_id')
except Exception as e:
log.warning(f"Error getting welcome_channel_id for guild {guild_id}: {e}")
try:
settings["welcome_message"] = await settings_manager.get_setting(guild_id, 'welcome_message')
except Exception as e:
log.warning(f"Error getting welcome_message for guild {guild_id}: {e}")
try:
settings["goodbye_channel_id"] = await settings_manager.get_setting(guild_id, 'goodbye_channel_id')
except Exception as e:
log.warning(f"Error getting goodbye_channel_id for guild {guild_id}: {e}")
try:
settings["goodbye_message"] = await settings_manager.get_setting(guild_id, 'goodbye_message')
except Exception as e:
log.warning(f"Error getting goodbye_message for guild {guild_id}: {e}")
# Get cog enabled statuses with error handling
try:
settings["cogs"] = await settings_manager.get_all_enabled_cogs(guild_id)
except Exception as e:
log.warning(f"Error getting cog enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for cogs
# Get command enabled statuses with error handling
try:
settings["commands"] = await settings_manager.get_all_enabled_commands(guild_id)
except Exception as e:
log.warning(f"Error getting command enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for commands
return settings
except HTTPException:
# Re-raise HTTP exceptions
raise
except RuntimeError as e:
# Handle event loop errors specifically
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.error(f"Event loop error getting settings for guild {guild_id}: {e}")
# Return a more helpful error message
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database connection error. Please try again."
)
else:
log.error(f"Runtime error getting settings for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting settings: {str(e)}"
)
except Exception as e:
log.error(f"Error getting settings for guild {guild_id}: {e}")
raise HTTPException(
@ -824,8 +866,20 @@ async def update_global_settings(
# --- Cog and Command Management Endpoints ---
# Note: These endpoints have been moved to cog_management_endpoints.py
# --- Cog Management Redirect Endpoints ---
# These endpoints redirect to the cog management endpoints in cog_management_endpoints.py
# --- Cog Management Endpoints ---
# These endpoints provide direct implementation and fallback for cog management
# Define models needed for cog management
class CogCommandInfo(BaseModel):
name: str
description: Optional[str] = None
enabled: bool = True
class CogInfo(BaseModel):
name: str
description: Optional[str] = None
enabled: bool = True
commands: List[Dict[str, Any]] = []
@router.get("/guilds/{guild_id}/cogs", response_model=List[Any])
async def get_guild_cogs_redirect(
@ -833,18 +887,93 @@ async def get_guild_cogs_redirect(
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Redirect to the cog management endpoint."""
"""Get all cogs and their commands for a guild."""
try:
# Import the cog management endpoint
# First try to use the dedicated cog management endpoint
try:
# Try relative import first
from .cog_management_endpoints import get_guild_cogs
except ImportError:
from cog_management_endpoints import get_guild_cogs
log.info(f"Successfully imported get_guild_cogs via relative import")
# Call the cog management endpoint
return await get_guild_cogs(guild_id, _user, _admin)
log.info(f"Calling get_guild_cogs for guild {guild_id}")
result = await get_guild_cogs(guild_id, _user, _admin)
log.info(f"Successfully retrieved cogs for guild {guild_id}")
return result
except ImportError as e:
log.warning(f"Relative import failed: {e}, trying absolute import")
try:
# Fall back to absolute import
from cog_management_endpoints import get_guild_cogs
log.info(f"Successfully imported get_guild_cogs via absolute import")
# Call the cog management endpoint
log.info(f"Calling get_guild_cogs for guild {guild_id}")
result = await get_guild_cogs(guild_id, _user, _admin)
log.info(f"Successfully retrieved cogs for guild {guild_id}")
return result
except ImportError as e2:
log.error(f"Both import attempts failed: {e2}")
log.warning("Falling back to direct implementation")
# Fall back to direct implementation
# Check if bot instance is available via discord_bot_sync_api
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
except ImportError:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot sync API not available"
)
# Get all cogs from the bot
cogs_list = []
for cog_name, cog in bot.cogs.items():
# Get enabled status from settings_manager
is_enabled = await settings_manager.is_cog_enabled(guild_id, cog_name, default_enabled=True)
# Get commands for this cog
commands_list = []
for command in cog.get_commands():
# Get command enabled status
cmd_enabled = await settings_manager.is_command_enabled(guild_id, command.qualified_name, default_enabled=True)
commands_list.append({
"name": command.qualified_name,
"description": command.help or "No description available",
"enabled": cmd_enabled
})
# Add slash commands if any
app_commands = [cmd for cmd in bot.tree.get_commands() if hasattr(cmd, 'cog') and cmd.cog and cmd.cog.qualified_name == cog_name]
for cmd in app_commands:
# Get command enabled status
cmd_enabled = await settings_manager.is_command_enabled(guild_id, cmd.name, default_enabled=True)
if not any(c["name"] == cmd.name for c in commands_list): # Avoid duplicates
commands_list.append({
"name": cmd.name,
"description": cmd.description or "No description available",
"enabled": cmd_enabled
})
cogs_list.append(CogInfo(
name=cog_name,
description=cog.__doc__ or "No description available",
enabled=is_enabled,
commands=commands_list
))
return cogs_list
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error redirecting to cog management endpoint: {e}")
log.error(f"Error getting cogs for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting cogs: {str(e)}"
@ -858,18 +987,84 @@ async def update_cog_status_redirect(
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Redirect to the cog management endpoint for updating cog status."""
"""Enable or disable a cog for a guild."""
try:
# Import the cog management endpoint
# First try to use the dedicated cog management endpoint
try:
# Try relative import first
from .cog_management_endpoints import update_cog_status
except ImportError:
from cog_management_endpoints import update_cog_status
log.info(f"Successfully imported update_cog_status via relative import")
# Call the cog management endpoint
return await update_cog_status(guild_id, cog_name, enabled, _user, _admin)
log.info(f"Calling update_cog_status for guild {guild_id}, cog {cog_name}, enabled={enabled}")
result = await update_cog_status(guild_id, cog_name, enabled, _user, _admin)
log.info(f"Successfully updated cog status for guild {guild_id}, cog {cog_name}")
return result
except ImportError as e:
log.warning(f"Relative import failed: {e}, trying absolute import")
try:
# Fall back to absolute import
from cog_management_endpoints import update_cog_status
log.info(f"Successfully imported update_cog_status via absolute import")
# Call the cog management endpoint
log.info(f"Calling update_cog_status for guild {guild_id}, cog {cog_name}, enabled={enabled}")
result = await update_cog_status(guild_id, cog_name, enabled, _user, _admin)
log.info(f"Successfully updated cog status for guild {guild_id}, cog {cog_name}")
return result
except ImportError as e2:
log.error(f"Both import attempts failed: {e2}")
log.warning("Falling back to direct implementation")
# Fall back to direct implementation
# Check if settings_manager is available
if not settings_manager or not settings_manager.pg_pool:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available"
)
# Check if the cog exists
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
if cog_name not in bot.cogs:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cog '{cog_name}' not found"
)
# Check if it's a core cog
core_cogs = getattr(bot, 'core_cogs', {'SettingsCog', 'HelpCog'})
if cog_name in core_cogs:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Core cog '{cog_name}' cannot be disabled"
)
except ImportError:
# If we can't import the bot, we'll just assume the cog exists
log.warning("Bot sync API not available, skipping cog existence check")
# Update the cog enabled status
success = await settings_manager.set_cog_enabled(guild_id, cog_name, enabled)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update cog '{cog_name}' status"
)
return {"message": f"Cog '{cog_name}' {'enabled' if enabled else 'disabled'} successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error redirecting to cog management endpoint for updating cog status: {e}")
log.error(f"Error updating cog status for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating cog status: {str(e)}"
@ -883,18 +1078,81 @@ async def update_command_status_redirect(
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Redirect to the cog management endpoint for updating command status."""
"""Enable or disable a command for a guild."""
try:
# Import the cog management endpoint
# First try to use the dedicated cog management endpoint
try:
# Try relative import first
from .cog_management_endpoints import update_command_status
except ImportError:
from cog_management_endpoints import update_command_status
log.info(f"Successfully imported update_command_status via relative import")
# Call the cog management endpoint
return await update_command_status(guild_id, command_name, enabled, _user, _admin)
log.info(f"Calling update_command_status for guild {guild_id}, command {command_name}, enabled={enabled}")
result = await update_command_status(guild_id, command_name, enabled, _user, _admin)
log.info(f"Successfully updated command status for guild {guild_id}, command {command_name}")
return result
except ImportError as e:
log.warning(f"Relative import failed: {e}, trying absolute import")
try:
# Fall back to absolute import
from cog_management_endpoints import update_command_status
log.info(f"Successfully imported update_command_status via absolute import")
# Call the cog management endpoint
log.info(f"Calling update_command_status for guild {guild_id}, command {command_name}, enabled={enabled}")
result = await update_command_status(guild_id, command_name, enabled, _user, _admin)
log.info(f"Successfully updated command status for guild {guild_id}, command {command_name}")
return result
except ImportError as e2:
log.error(f"Both import attempts failed: {e2}")
log.warning("Falling back to direct implementation")
# Fall back to direct implementation
# Check if settings_manager is available
if not settings_manager or not settings_manager.pg_pool:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available"
)
# Check if the command exists
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
# Check if it's a prefix command
command = bot.get_command(command_name)
if not command:
# Check if it's an app command
app_commands = [cmd for cmd in bot.tree.get_commands() if cmd.name == command_name]
if not app_commands:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Command '{command_name}' not found"
)
except ImportError:
# If we can't import the bot, we'll just assume the command exists
log.warning("Bot sync API not available, skipping command existence check")
# Update the command enabled status
success = await settings_manager.set_command_enabled(guild_id, command_name, enabled)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update command '{command_name}' status"
)
return {"message": f"Command '{command_name}' {'enabled' if enabled else 'disabled'} successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error redirecting to cog management endpoint for updating command status: {e}")
log.error(f"Error updating command status for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating command status: {str(e)}"

View File

@ -2,6 +2,7 @@ import asyncpg
import redis.asyncio as redis
import os
import logging
import asyncio
from dotenv import load_dotenv
from typing import Dict
@ -57,10 +58,25 @@ async def initialize_pools():
)
log.info(f"PostgreSQL pool connected to {POSTGRES_HOST}/{POSTGRES_DB}")
# Create Redis pool
redis_pool = redis.from_url(REDIS_URL, decode_responses=True)
await redis_pool.ping() # Test connection
# Create Redis pool with connection_cls=None to avoid event loop issues
# This creates a connection pool that doesn't bind to a specific event loop
redis_pool = redis.from_url(
REDIS_URL,
decode_responses=True,
max_connections=20, # Limit max connections
socket_timeout=5.0, # 5 second timeout for operations
socket_connect_timeout=3.0, # 3 second timeout for connections
retry_on_timeout=True, # Retry on timeout
health_check_interval=30 # Check connection health every 30 seconds
)
# Test connection with a timeout
try:
await asyncio.wait_for(redis_pool.ping(), timeout=5.0)
log.info(f"Redis pool connected to {REDIS_HOST}:{REDIS_PORT}")
except asyncio.TimeoutError:
log.error(f"Redis connection timeout when connecting to {REDIS_HOST}:{REDIS_PORT}")
raise
# Initialize database schema
await initialize_database() # Ensure tables exist
@ -258,15 +274,27 @@ async def get_guild_prefix(guild_id: int, default_prefix: str) -> str:
return default_prefix
cache_key = _get_redis_key(guild_id, "prefix")
# Try to get from cache with timeout and error handling
try:
cached_prefix = await redis_pool.get(cache_key)
# Use a timeout to prevent hanging on Redis operations
cached_prefix = await asyncio.wait_for(redis_pool.get(cache_key), timeout=2.0)
if cached_prefix is not None:
log.debug(f"Cache hit for prefix (Guild: {guild_id})")
return cached_prefix
except asyncio.TimeoutError:
log.warning(f"Redis timeout getting prefix for guild {guild_id}, falling back to database")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error for guild {guild_id}, falling back to database: {e}")
else:
log.exception(f"Redis error getting prefix for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Redis error getting prefix for guild {guild_id}: {e}")
# Cache miss or Redis error, get from database
log.debug(f"Cache miss for prefix (Guild: {guild_id})")
try:
async with pg_pool.acquire() as conn:
prefix = await conn.fetchval(
"SELECT setting_value FROM guild_settings WHERE guild_id = $1 AND setting_key = 'prefix'",
@ -275,13 +303,27 @@ async def get_guild_prefix(guild_id: int, default_prefix: str) -> str:
final_prefix = prefix if prefix is not None else default_prefix
# Cache the result (even if it's the default, to avoid future DB lookups)
# Try to cache the result with timeout and error handling
try:
await redis_pool.set(cache_key, final_prefix, ex=3600) # Cache for 1 hour
# Use a timeout to prevent hanging on Redis operations
await asyncio.wait_for(
redis_pool.set(cache_key, final_prefix, ex=3600), # Cache for 1 hour
timeout=2.0
)
except asyncio.TimeoutError:
log.warning(f"Redis timeout setting prefix for guild {guild_id}")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error setting prefix for guild {guild_id}: {e}")
else:
log.exception(f"Redis error setting prefix for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Redis error setting prefix for guild {guild_id}: {e}")
return final_prefix
except Exception as e:
log.exception(f"Database error getting prefix for guild {guild_id}: {e}")
return default_prefix # Fall back to default on database error
async def set_guild_prefix(guild_id: int, prefix: str):
"""Sets the command prefix for a guild and updates the cache."""
@ -326,16 +368,31 @@ async def get_setting(guild_id: int, key: str, default=None):
return default
cache_key = _get_redis_key(guild_id, "setting", key)
# Try to get from cache with timeout and error handling
try:
cached_value = await redis_pool.get(cache_key)
# Use a timeout to prevent hanging on Redis operations
cached_value = await asyncio.wait_for(redis_pool.get(cache_key), timeout=2.0)
if cached_value is not None:
# Note: Redis stores everything as strings. Consider type conversion if needed.
log.debug(f"Cache hit for setting '{key}' (Guild: {guild_id})")
# Handle the None marker
if cached_value == "__NONE__":
return default
return cached_value
except asyncio.TimeoutError:
log.warning(f"Redis timeout getting setting '{key}' for guild {guild_id}, falling back to database")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error for guild {guild_id}, falling back to database: {e}")
else:
log.exception(f"Redis error getting setting '{key}' for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Redis error getting setting '{key}' for guild {guild_id}: {e}")
# Cache miss or Redis error, get from database
log.debug(f"Cache miss for setting '{key}' (Guild: {guild_id})")
try:
async with pg_pool.acquire() as conn:
value = await conn.fetchval(
"SELECT setting_value FROM guild_settings WHERE guild_id = $1 AND setting_key = $2",
@ -347,12 +404,28 @@ async def get_setting(guild_id: int, key: str, default=None):
# Cache the result (even if None or default, cache the absence or default value)
# Store None as a special marker, e.g., "None" string, or handle appropriately
value_to_cache = final_value if final_value is not None else "__NONE__" # Marker for None
# Try to cache the result with timeout and error handling
try:
await redis_pool.set(cache_key, value_to_cache, ex=3600) # Cache for 1 hour
# Use a timeout to prevent hanging on Redis operations
await asyncio.wait_for(
redis_pool.set(cache_key, value_to_cache, ex=3600), # Cache for 1 hour
timeout=2.0
)
except asyncio.TimeoutError:
log.warning(f"Redis timeout setting cache for setting '{key}' for guild {guild_id}")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error setting cache for setting '{key}' for guild {guild_id}: {e}")
else:
log.exception(f"Redis error setting cache for setting '{key}' for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Redis error setting cache for setting '{key}' for guild {guild_id}: {e}")
return final_value
except Exception as e:
log.exception(f"Database error getting setting '{key}' for guild {guild_id}: {e}")
return default # Fall back to default on database error
async def set_setting(guild_id: int, key: str, value: str | None):
@ -411,14 +484,25 @@ async def is_cog_enabled(guild_id: int, cog_name: str, default_enabled: bool = T
return default_enabled
cache_key = _get_redis_key(guild_id, "cog_enabled", cog_name)
# Try to get from cache with timeout and error handling
try:
cached_value = await redis_pool.get(cache_key)
# Use a timeout to prevent hanging on Redis operations
cached_value = await asyncio.wait_for(redis_pool.get(cache_key), timeout=2.0)
if cached_value is not None:
log.debug(f"Cache hit for cog enabled status '{cog_name}' (Guild: {guild_id})")
return cached_value == "True" # Redis stores strings
except asyncio.TimeoutError:
log.warning(f"Redis timeout getting cog enabled status for '{cog_name}' (Guild: {guild_id}), falling back to database")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error for guild {guild_id}, falling back to database: {e}")
else:
log.exception(f"Redis error getting cog enabled status for '{cog_name}' (Guild: {guild_id}): {e}")
except Exception as e:
log.exception(f"Redis error getting cog enabled status for '{cog_name}' (Guild: {guild_id}): {e}")
# Cache miss or Redis error, get from database
log.debug(f"Cache miss for cog enabled status '{cog_name}' (Guild: {guild_id})")
db_enabled_status = None
try:
@ -427,20 +511,31 @@ async def is_cog_enabled(guild_id: int, cog_name: str, default_enabled: bool = T
"SELECT enabled FROM enabled_cogs WHERE guild_id = $1 AND cog_name = $2",
guild_id, cog_name
)
except Exception as e:
log.exception(f"Database error getting cog enabled status for '{cog_name}' (Guild: {guild_id}): {e}")
# Fallback to default on DB error after cache miss
return default_enabled
final_status = db_enabled_status if db_enabled_status is not None else default_enabled
# Cache the result (True or False)
# Try to cache the result with timeout and error handling
try:
await redis_pool.set(cache_key, str(final_status), ex=3600) # Cache for 1 hour
# Use a timeout to prevent hanging on Redis operations
await asyncio.wait_for(
redis_pool.set(cache_key, str(final_status), ex=3600), # Cache for 1 hour
timeout=2.0
)
except asyncio.TimeoutError:
log.warning(f"Redis timeout setting cache for cog enabled status '{cog_name}' (Guild: {guild_id})")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error setting cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {e}")
else:
log.exception(f"Redis error setting cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {e}")
except Exception as e:
log.exception(f"Redis error setting cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {e}")
return final_status
except Exception as e:
log.exception(f"Database error getting cog enabled status for '{cog_name}' (Guild: {guild_id}): {e}")
# Fallback to default on DB error after cache miss
return default_enabled
async def set_cog_enabled(guild_id: int, cog_name: str, enabled: bool):
@ -486,14 +581,25 @@ async def is_command_enabled(guild_id: int, command_name: str, default_enabled:
return default_enabled
cache_key = _get_redis_key(guild_id, "cmd_enabled", command_name)
# Try to get from cache with timeout and error handling
try:
cached_value = await redis_pool.get(cache_key)
# Use a timeout to prevent hanging on Redis operations
cached_value = await asyncio.wait_for(redis_pool.get(cache_key), timeout=2.0)
if cached_value is not None:
log.debug(f"Cache hit for command enabled status '{command_name}' (Guild: {guild_id})")
return cached_value == "True" # Redis stores strings
except asyncio.TimeoutError:
log.warning(f"Redis timeout getting command enabled status for '{command_name}' (Guild: {guild_id}), falling back to database")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error for guild {guild_id}, falling back to database: {e}")
else:
log.exception(f"Redis error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
except Exception as e:
log.exception(f"Redis error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
# Cache miss or Redis error, get from database
log.debug(f"Cache miss for command enabled status '{command_name}' (Guild: {guild_id})")
db_enabled_status = None
try:
@ -502,20 +608,31 @@ async def is_command_enabled(guild_id: int, command_name: str, default_enabled:
"SELECT enabled FROM enabled_commands WHERE guild_id = $1 AND command_name = $2",
guild_id, command_name
)
except Exception as e:
log.exception(f"Database error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
# Fallback to default on DB error after cache miss
return default_enabled
final_status = db_enabled_status if db_enabled_status is not None else default_enabled
# Cache the result (True or False)
# Try to cache the result with timeout and error handling
try:
await redis_pool.set(cache_key, str(final_status), ex=3600) # Cache for 1 hour
# Use a timeout to prevent hanging on Redis operations
await asyncio.wait_for(
redis_pool.set(cache_key, str(final_status), ex=3600), # Cache for 1 hour
timeout=2.0
)
except asyncio.TimeoutError:
log.warning(f"Redis timeout setting cache for command enabled status '{command_name}' (Guild: {guild_id})")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Redis event loop error setting cache for command enabled status '{command_name}' (Guild: {guild_id}): {e}")
else:
log.exception(f"Redis error setting cache for command enabled status '{command_name}' (Guild: {guild_id}): {e}")
except Exception as e:
log.exception(f"Redis error setting cache for command enabled status '{command_name}' (Guild: {guild_id}): {e}")
return final_status
except Exception as e:
log.exception(f"Database error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
# Fallback to default on DB error after cache miss
return default_enabled
async def set_command_enabled(guild_id: int, command_name: str, enabled: bool):