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:
2026-02-18 12:08:32 +02:00
parent 7b7abcfc68
commit 8d5137046c

View File

@@ -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)