1919from loguru import logger
2020
2121from tux .core .bot import Tux
22- from tux .database .utils import get_db_controller_from
2322from tux .help import TuxHelp
2423from tux .services .sentry_manager import SentryManager
2524from tux .shared .config import CONFIG
2625
2726
2827async def get_prefix (bot : Tux , message : discord .Message ) -> list [str ]:
29- """Get the command prefix for a guild.
28+ """Get the command prefix for a guild using the prefix manager .
3029
31- This function retrieves the guild-specific prefix from the database,
32- falling back to `CONFIG.get_prefix()` when the guild is unavailable or the database
33- cannot be resolved.
30+ This function uses the in-memory prefix cache for optimal performance,
31+ falling back to the default prefix when the guild is unavailable.
32+
33+ If BOT_INFO__PREFIX is set in environment variables, all guilds will use
34+ that prefix, ignoring database settings.
3435 """
35- if not message .guild :
36+ # Check if prefix override is enabled by environment variable
37+ if CONFIG .is_prefix_override_enabled ():
3638 return [CONFIG .get_prefix ()]
3739
38- prefix : str | None = None
39-
40- try :
41- controller = get_db_controller_from (bot , fallback_to_direct = False )
42- if controller is None :
43- logger .warning ("Database unavailable; using default prefix" )
44- else :
45- # Ensure the guild exists in the database first
46- await controller .guild .get_or_create_guild (message .guild .id )
47-
48- # Get or create guild config with default prefix
49- guild_config = await controller .guild_config .get_or_create_config (
50- message .guild .id ,
51- prefix = CONFIG .get_prefix (), # Use the default prefix as the default value
52- )
53- if guild_config and hasattr (guild_config , "prefix" ):
54- prefix = guild_config .prefix
40+ if not message .guild :
41+ return [CONFIG .get_prefix ()]
5542
56- except Exception as e :
57- logger .error (f"❌ Error getting guild prefix: { type (e ).__name__ } " )
58- logger .info ("💡 Using default prefix due to database or configuration error" )
43+ # Use the prefix manager for efficient prefix resolution
44+ if hasattr (bot , "prefix_manager" ) and bot .prefix_manager :
45+ prefix = await bot .prefix_manager .get_prefix (message .guild .id )
46+ return [prefix ]
5947
60- return [prefix or CONFIG .get_prefix ()]
48+ # Fallback to default prefix if prefix manager is not available
49+ return [CONFIG .get_prefix ()]
6150
6251
6352class TuxApp :
@@ -82,7 +71,29 @@ def run(self) -> None:
8271
8372 This is the synchronous entrypoint typically invoked by the CLI.
8473 """
85- asyncio .run (self .start ())
74+ try :
75+ # Use a more direct approach to handle signals
76+ loop = asyncio .new_event_loop ()
77+ asyncio .set_event_loop (loop )
78+
79+ try :
80+ # Run the bot with the event loop
81+ loop .run_until_complete (self .start ())
82+ finally :
83+ loop .close ()
84+
85+ except KeyboardInterrupt :
86+ logger .info ("Application interrupted by user" )
87+ except RuntimeError as e :
88+ # Handle event loop stopped errors gracefully (these are expected during shutdown)
89+ if "Event loop stopped" in str (e ):
90+ logger .debug ("Event loop stopped during shutdown" )
91+ else :
92+ logger .error (f"Application error: { e } " )
93+ raise
94+ except Exception as e :
95+ logger .error (f"Application error: { e } " )
96+ raise
8697
8798 def setup_signals (self , loop : asyncio .AbstractEventLoop ) -> None :
8899 """Register signal handlers for graceful shutdown.
@@ -100,21 +111,39 @@ def setup_signals(self, loop: asyncio.AbstractEventLoop) -> None:
100111
101112 def _sigterm () -> None :
102113 SentryManager .report_signal (signal .SIGTERM , None )
103- # Trigger graceful shutdown by closing the bot
114+ logger .info ("SIGTERM received, forcing shutdown..." )
115+ # Set shutdown event for the monitor
116+ if hasattr (self , "_shutdown_event" ):
117+ self ._shutdown_event .set ()
118+ # Cancel ALL tasks in the event loop
119+ for task in asyncio .all_tasks (loop ):
120+ if not task .done ():
121+ task .cancel ()
122+ # Force close the bot connection if it exists
104123 if hasattr (self , "bot" ) and self .bot and not self .bot .is_closed ():
105- # Schedule the close operation in the event loop
106- bot = self .bot # Type narrowing
107- with contextlib .suppress (Exception ):
108- loop .call_soon_threadsafe (lambda : asyncio .create_task (bot .close ()))
124+ close_task = asyncio .create_task (self .bot .close ())
125+ # Store reference to prevent garbage collection
126+ _ = close_task
127+ # Stop the event loop
128+ loop .call_soon_threadsafe (loop .stop )
109129
110130 def _sigint () -> None :
111131 SentryManager .report_signal (signal .SIGINT , None )
112- # Trigger graceful shutdown by closing the bot
132+ logger .info ("SIGINT received, forcing shutdown..." )
133+ # Set shutdown event for the monitor
134+ if hasattr (self , "_shutdown_event" ):
135+ self ._shutdown_event .set ()
136+ # Cancel ALL tasks in the event loop
137+ for task in asyncio .all_tasks (loop ):
138+ if not task .done ():
139+ task .cancel ()
140+ # Force close the bot connection if it exists
113141 if hasattr (self , "bot" ) and self .bot and not self .bot .is_closed ():
114- # Schedule the close operation in the event loop
115- bot = self .bot # Type narrowing
116- with contextlib .suppress (Exception ):
117- loop .call_soon_threadsafe (lambda : asyncio .create_task (bot .close ()))
142+ close_task = asyncio .create_task (self .bot .close ())
143+ # Store reference to prevent garbage collection
144+ _ = close_task
145+ # Stop the event loop
146+ loop .call_soon_threadsafe (loop .stop )
118147
119148 try :
120149 loop .add_signal_handler (signal .SIGTERM , _sigterm )
@@ -124,7 +153,9 @@ def _sigint() -> None:
124153 # Fallback for platforms that do not support add_signal_handler (e.g., Windows)
125154 def _signal_handler (signum : int , frame : FrameType | None ) -> None :
126155 SentryManager .report_signal (signum , frame )
127- # For Windows fallback, just log the signal
156+ logger .info (f"Signal { signum } received, shutting down..." )
157+ # For Windows fallback, raise KeyboardInterrupt to stop the event loop
158+ raise KeyboardInterrupt
128159
129160 signal .signal (signal .SIGTERM , _signal_handler )
130161 signal .signal (signal .SIGINT , _signal_handler )
@@ -175,8 +206,26 @@ async def start(self) -> None:
175206 )
176207
177208 try :
178- # Start the bot normally - this handles login() + connect() properly
179- await self .bot .start (CONFIG .BOT_TOKEN , reconnect = True )
209+ # Use login() + connect() separately to avoid blocking
210+ logger .info ("🔐 Logging in to Discord..." )
211+ await self .bot .login (CONFIG .BOT_TOKEN )
212+
213+ logger .info ("🌐 Connecting to Discord..." )
214+ # Create a task for the connection
215+ self ._connect_task = asyncio .create_task (self .bot .connect (reconnect = True ), name = "bot_connect" )
216+
217+ # Create a task to monitor for shutdown signals
218+ shutdown_task = asyncio .create_task (self ._monitor_shutdown (), name = "shutdown_monitor" )
219+
220+ # Wait for either the connection to complete or shutdown to be requested
221+ done , pending = await asyncio .wait ([self ._connect_task , shutdown_task ], return_when = asyncio .FIRST_COMPLETED )
222+
223+ # Cancel any pending tasks
224+ for task in pending :
225+ task .cancel ()
226+ with contextlib .suppress (asyncio .CancelledError ):
227+ await task
228+
180229 except asyncio .CancelledError :
181230 # Handle cancellation gracefully
182231 logger .info ("Bot startup was cancelled" )
@@ -188,6 +237,16 @@ async def start(self) -> None:
188237 finally :
189238 await self .shutdown ()
190239
240+ async def _monitor_shutdown (self ) -> None :
241+ """Monitor for shutdown signals while the bot is running."""
242+ # Create an event to track shutdown requests
243+ self ._shutdown_event = asyncio .Event ()
244+
245+ # Wait for shutdown event
246+ await self ._shutdown_event .wait ()
247+
248+ logger .info ("Shutdown requested via monitor" )
249+
191250 async def shutdown (self ) -> None :
192251 """Gracefully shut down the bot and flush telemetry.
193252
0 commit comments