From 04d1e0ece10bc84ec6ec36e85d5ff6eb37ab4dff Mon Sep 17 00:00:00 2001 From: ruandan Date: Fri, 10 Apr 2026 19:24:23 +0800 Subject: [PATCH] feat: broadcast shutdown notification to active threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the Phase 1 portion of RFC #78 ยง1d (graceful shutdown). On SIGINT or SIGTERM, the broker now iterates active sessions and posts a neutral notification to each Discord thread before clearing the pool. Users get a clear signal that the broker is restarting instead of conversations dying silently. Changes: - Add `SessionPool::active_thread_ids()` returning a snapshot of tracked thread IDs (read-only lookup, no write lock held). - Main shutdown task now listens for SIGTERM in addition to SIGINT via `tokio::signal::unix`. Previously only Ctrl+C triggered the graceful path; `systemctl stop` / `docker stop` / `kill` all bypassed it entirely, which made the broadcast valuable for production but invisible in dev. - Before `shard_manager.shutdown_all()`, iterate the snapshot and post "๐Ÿ”„ Broker restarting. You can continue the conversation when the broker is back." to each thread. Failures are warned, not fatal โ€” a dead Discord connection shouldn't block pool cleanup. Design notes: - Wording is intentionally neutral. "Will resume" would imply state restoration, which is Phase 2 of ยง1d (persistence) and out of scope here. - No grace period for in-flight streams. Keeping the PR minimal; a configurable drain window can be a follow-up if needed. - `active_thread_ids()` takes a snapshot via `.read()` so the shutdown loop does not contend with the `shutdown()` write lock on the same map. Tested locally against `config.toml` with 1 active session: SIGTERM produces the expected log sequence (`shutdown signal received` โ†’ `broadcasting shutdown notification count=1` โ†’ `pool shutdown complete count=1`) and the notification arrives in the thread before the process exits. --- src/acp/pool.rs | 6 ++++++ src/main.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/acp/pool.rs b/src/acp/pool.rs index a2c8a06c..58e53678 100644 --- a/src/acp/pool.rs +++ b/src/acp/pool.rs @@ -95,6 +95,12 @@ impl SessionPool { } } + /// Return the thread IDs of all currently tracked sessions. + /// Used by the shutdown path to broadcast notifications before the pool is cleared. + pub async fn active_thread_ids(&self) -> Vec { + self.connections.read().await.keys().cloned().collect() + } + pub async fn shutdown(&self) { let mut conns = self.connections.write().await; let count = conns.len(); diff --git a/src/main.rs b/src/main.rs index 05bbfd84..ac257d8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,9 +68,37 @@ async fn main() -> anyhow::Result<()> { // Run bot until SIGINT/SIGTERM let shard_manager = client.shard_manager.clone(); let shutdown_pool = pool.clone(); + let broadcast_pool = pool.clone(); + let shutdown_http = client.http.clone(); tokio::spawn(async move { - tokio::signal::ctrl_c().await.ok(); + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("install SIGTERM handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = sigterm.recv() => {} + } info!("shutdown signal received"); + + // Broadcast shutdown notification to active threads before closing the pool. + // Neutral wording โ€” we don't promise automatic resume; Phase 2 of RFC #78 1d + // (session persistence) is a separate follow-up. + let thread_ids = broadcast_pool.active_thread_ids().await; + info!(count = thread_ids.len(), "broadcasting shutdown notification"); + for thread_id in thread_ids { + if let Ok(id) = thread_id.parse::() { + let channel = serenity::model::id::ChannelId::new(id); + if let Err(e) = channel + .say( + &shutdown_http, + "๐Ÿ”„ Broker restarting. You can continue the conversation when the broker is back.", + ) + .await + { + tracing::warn!(thread_id, error = %e, "failed to post shutdown notification"); + } + } + } + shard_manager.shutdown_all().await; });