fix(shutdown): implement graceful async shutdown handler
Replace the minimal sync-only shutdown (which only saved autonomous state) with a comprehensive async graceful_shutdown() coroutine that: 1. Ends active voice sessions (disconnect, release GPU locks, cleanup audio) 2. Saves autonomous engine state 3. Stops the APScheduler 4. Cancels all tracked background tasks (from task_tracker) 5. Closes the Discord gateway connection Signal handlers (SIGTERM/SIGINT) now schedule the async shutdown on the running event loop. The atexit handler is kept as a last-resort sync fallback. Resolves #5, also addresses #4 (voice cleanup at shutdown)
This commit is contained in:
78
bot/bot.py
78
bot/bot.py
@@ -874,10 +874,82 @@ def save_autonomous_state():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save autonomous context on shutdown: {e}")
|
logger.error(f"Failed to save autonomous context on shutdown: {e}")
|
||||||
|
|
||||||
# Register shutdown handlers
|
async def graceful_shutdown():
|
||||||
|
"""
|
||||||
|
Perform a full async cleanup before the bot exits.
|
||||||
|
|
||||||
|
Shutdown sequence:
|
||||||
|
1. End active voice sessions (disconnect, release GPU locks)
|
||||||
|
2. Save autonomous engine state
|
||||||
|
3. Stop the APScheduler
|
||||||
|
4. Cancel all tracked background tasks
|
||||||
|
5. Close the Discord gateway connection
|
||||||
|
"""
|
||||||
|
logger.warning("🛑 Graceful shutdown initiated...")
|
||||||
|
|
||||||
|
# 1. End active voice session (cleans up audio, STT, GPU locks, etc.)
|
||||||
|
try:
|
||||||
|
from utils.voice_manager import VoiceSessionManager
|
||||||
|
session_mgr = VoiceSessionManager()
|
||||||
|
if session_mgr.active_session:
|
||||||
|
logger.info("🎙️ Ending active voice session...")
|
||||||
|
await session_mgr.end_session()
|
||||||
|
logger.info("✓ Voice session ended")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error ending voice session during shutdown: {e}")
|
||||||
|
|
||||||
|
# 2. Persist autonomous engine state
|
||||||
|
save_autonomous_state()
|
||||||
|
|
||||||
|
# 3. Shut down the APScheduler
|
||||||
|
try:
|
||||||
|
if globals.scheduler.running:
|
||||||
|
globals.scheduler.shutdown(wait=False)
|
||||||
|
logger.info("✓ Scheduler stopped")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping scheduler: {e}")
|
||||||
|
|
||||||
|
# 4. Cancel all tracked background tasks
|
||||||
|
try:
|
||||||
|
from utils.task_tracker import _active_tasks
|
||||||
|
pending = [t for t in _active_tasks if not t.done()]
|
||||||
|
if pending:
|
||||||
|
logger.info(f"Cancelling {len(pending)} background tasks...")
|
||||||
|
for t in pending:
|
||||||
|
t.cancel()
|
||||||
|
await asyncio.gather(*pending, return_exceptions=True)
|
||||||
|
logger.info("✓ Background tasks cancelled")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cancelling background tasks: {e}")
|
||||||
|
|
||||||
|
# 5. Close the Discord gateway connection
|
||||||
|
try:
|
||||||
|
if not globals.client.is_closed():
|
||||||
|
await globals.client.close()
|
||||||
|
logger.info("✓ Discord client closed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error closing Discord client: {e}")
|
||||||
|
|
||||||
|
logger.warning("🛑 Graceful shutdown complete")
|
||||||
|
|
||||||
|
def _handle_shutdown_signal(sig, _frame):
|
||||||
|
"""Schedule the async shutdown from a sync signal handler."""
|
||||||
|
sig_name = signal.Signals(sig).name
|
||||||
|
logger.warning(f"Received {sig_name}, scheduling graceful shutdown...")
|
||||||
|
# Schedule the coroutine on the running event loop
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
if loop.is_running():
|
||||||
|
loop.create_task(graceful_shutdown())
|
||||||
|
else:
|
||||||
|
# Fallback: just save state synchronously
|
||||||
|
save_autonomous_state()
|
||||||
|
|
||||||
|
# Register signal handlers (async-aware)
|
||||||
|
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
|
||||||
|
signal.signal(signal.SIGINT, _handle_shutdown_signal)
|
||||||
|
|
||||||
|
# Keep atexit as a last-resort sync fallback
|
||||||
atexit.register(save_autonomous_state)
|
atexit.register(save_autonomous_state)
|
||||||
signal.signal(signal.SIGTERM, lambda s, f: save_autonomous_state())
|
|
||||||
signal.signal(signal.SIGINT, lambda s, f: save_autonomous_state())
|
|
||||||
|
|
||||||
threading.Thread(target=start_api, daemon=True).start()
|
threading.Thread(target=start_api, daemon=True).start()
|
||||||
globals.client.run(globals.DISCORD_BOT_TOKEN)
|
globals.client.run(globals.DISCORD_BOT_TOKEN)
|
||||||
|
|||||||
Reference in New Issue
Block a user