feat: 增强错误处理和日志记录,优雅处理通道关闭情况

This commit is contained in:
oudecheng 2026-06-02 15:23:50 +08:00
parent 1541dd7c10
commit eebfe0faa5
8 changed files with 84 additions and 40 deletions

View File

@ -1,3 +1,26 @@
pub fn initialize_process_runtime() { pub fn initialize_process_runtime() {
let _ = rustls::crypto::ring::default_provider().install_default(); let _ = rustls::crypto::ring::default_provider().install_default();
// Install a global panic hook so that any panic in a spawned task
// is logged with a full backtrace before the process exits. Without
// this hook, panics are silent (or print a brief message to stderr)
// which makes root-causing crashes difficult.
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
// Use tracing so the panic appears in the same log stream as everything else.
tracing::error!(
panic.payload = ?info.payload(),
panic.location = ?info.location(),
"FATAL: process panicked — collecting backtrace"
);
// Print a compact backtrace to stderr as well (backtrace is not
// captured by tracing).
let backtrace = std::backtrace::Backtrace::capture();
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
eprintln!("FATAL panic backtrace:\n{}", backtrace);
}
// Delegate to the default hook which prints the panic message and
// optionally the RUST_BACKTRACE-based backtrace.
default_hook(info);
}));
} }

View File

@ -43,18 +43,13 @@ impl MessageBus {
.map_err(|_| BusError::Closed) .map_err(|_| BusError::Closed)
} }
/// Consume a message from the inbound queue /// Consume a message from the inbound queue.
pub async fn consume_inbound(&self) -> InboundMessage { /// Returns `None` when the channel is closed (all senders dropped).
let msg = self pub async fn consume_inbound(&self) -> Option<InboundMessage> {
.inbound_rx let msg = self.inbound_rx.lock().await.recv().await?;
.lock()
.await
.recv()
.await
.expect("bus inbound closed");
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
tracing::debug!(channel = %msg.channel, sender = %msg.sender_id, chat = %msg.chat_id, "Bus: consuming inbound message"); tracing::debug!(channel = %msg.channel, sender = %msg.sender_id, chat = %msg.chat_id, "Bus: consuming inbound message");
msg Some(msg)
} }
/// Publish a message to the outbound queue /// Publish a message to the outbound queue
@ -67,14 +62,10 @@ impl MessageBus {
.map_err(|_| BusError::Closed) .map_err(|_| BusError::Closed)
} }
/// Consume an outbound message from the outbound queue /// Consume an outbound message from the outbound queue.
pub async fn consume_outbound(&self) -> OutboundMessage { /// Returns `None` when the channel is closed (all senders dropped).
self.outbound_rx pub async fn consume_outbound(&self) -> Option<OutboundMessage> {
.lock() self.outbound_rx.lock().await.recv().await
.await
.recv()
.await
.expect("bus outbound closed")
} }
} }

View File

@ -8,6 +8,7 @@ use std::sync::{
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::FutureExt;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use wechatbot::{BotOptions, SendContent, WeChatBot}; use wechatbot::{BotOptions, SendContent, WeChatBot};
@ -246,24 +247,39 @@ impl Channel for WechatChannel {
let running = self.running.clone(); let running = self.running.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
match bot.login(force_login).await { // Use catch_unwind to prevent a panic in the WeChat SDK (login or
Ok(creds) => { // long-poll loop) from crashing the entire process. Any panic is
tracing::info!( // logged and the channel is cleanly marked as stopped.
channel = %channel_name, // AssertUnwindSafe is needed because WeChatBot contains internal
account_id = %creds.account_id, // locks (RwLock) that are not RefUnwindSafe.
user_id = %creds.user_id, let result = std::panic::AssertUnwindSafe(async {
"WeChat login succeeded" match bot.login(force_login).await {
); Ok(creds) => {
tracing::info!(
channel = %channel_name,
account_id = %creds.account_id,
user_id = %creds.user_id,
"WeChat login succeeded"
);
}
Err(error) => {
tracing::error!(channel = %channel_name, error = %error, "WeChat login failed");
return;
}
} }
Err(error) => {
running.store(false, Ordering::SeqCst);
tracing::error!(channel = %channel_name, error = %error, "WeChat login failed");
return;
}
}
if let Err(error) = bot.run().await { if let Err(error) = bot.run().await {
tracing::error!(channel = %channel_name, error = %error, "WeChat channel stopped with error"); tracing::error!(channel = %channel_name, error = %error, "WeChat channel stopped with error");
}
})
.catch_unwind()
.await;
if let Err(_panic) = result {
tracing::error!(
channel = %channel_name,
"WeChat bot task panicked — marking channel as stopped"
);
} }
running.store(false, Ordering::SeqCst); running.store(false, Ordering::SeqCst);

View File

@ -34,7 +34,13 @@ impl OutboundDispatcher {
tracing::info!("OutboundDispatcher started"); tracing::info!("OutboundDispatcher started");
loop { loop {
let msg = self.bus.consume_outbound().await; let msg = match self.bus.consume_outbound().await {
Some(msg) => msg,
None => {
tracing::info!("Outbound bus closed, stopping dispatcher");
break;
}
};
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
tracing::debug!( tracing::debug!(
channel = %msg.channel, channel = %msg.channel,

View File

@ -114,8 +114,14 @@ impl InboundProcessor {
); );
loop { loop {
// 1. 消费消息 // 1. 消费消息 (channel 关闭时返回 None优雅退出)
let inbound = self.bus.consume_inbound().await; let inbound = match self.bus.consume_inbound().await {
Some(msg) => msg,
None => {
tracing::info!("Inbound bus closed, stopping inbound processor");
break;
}
};
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {

View File

@ -1753,7 +1753,8 @@ mod tests {
let msg = tokio::time::timeout(std::time::Duration::from_millis(500), bus.consume_outbound()) let msg = tokio::time::timeout(std::time::Duration::from_millis(500), bus.consume_outbound())
.await .await
.expect("should have received an outbound message"); .expect("timeout waiting for outbound message")
.expect("bus outbound closed");
assert_eq!(msg.event_kind, OutboundEventKind::ToolResult); assert_eq!(msg.event_kind, OutboundEventKind::ToolResult);
} }

View File

@ -138,7 +138,7 @@ mod tests {
} }
); );
let msg = bus.consume_outbound().await; let msg = bus.consume_outbound().await.expect("bus outbound closed");
assert_eq!(msg.content, "hello"); assert_eq!(msg.content, "hello");
assert_eq!(msg.media.len(), 1); assert_eq!(msg.media.len(), 1);
assert_eq!(msg.media[0].media_type, "image"); assert_eq!(msg.media[0].media_type, "image");

View File

@ -1681,7 +1681,8 @@ mod tests {
bus.consume_outbound(), bus.consume_outbound(),
) )
.await .await
.unwrap(); .expect("timeout waiting for outbound message")
.expect("bus outbound closed");
assert_eq!(outbound.channel, "test-channel"); assert_eq!(outbound.channel, "test-channel");
assert_eq!(outbound.chat_id, "oc_demo"); assert_eq!(outbound.chat_id, "oc_demo");
assert!(outbound.content.contains("定时任务执行失败")); assert!(outbound.content.contains("定时任务执行失败"));