feat: 增强错误处理和日志记录,优雅处理通道关闭情况
This commit is contained in:
parent
1541dd7c10
commit
eebfe0faa5
@ -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);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,6 +247,12 @@ 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 {
|
||||||
|
// Use catch_unwind to prevent a panic in the WeChat SDK (login or
|
||||||
|
// long-poll loop) from crashing the entire process. Any panic is
|
||||||
|
// logged and the channel is cleanly marked as stopped.
|
||||||
|
// AssertUnwindSafe is needed because WeChatBot contains internal
|
||||||
|
// locks (RwLock) that are not RefUnwindSafe.
|
||||||
|
let result = std::panic::AssertUnwindSafe(async {
|
||||||
match bot.login(force_login).await {
|
match bot.login(force_login).await {
|
||||||
Ok(creds) => {
|
Ok(creds) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@ -256,7 +263,6 @@ impl Channel for WechatChannel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
running.store(false, Ordering::SeqCst);
|
|
||||||
tracing::error!(channel = %channel_name, error = %error, "WeChat login failed");
|
tracing::error!(channel = %channel_name, error = %error, "WeChat login failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -265,6 +271,16 @@ impl Channel for WechatChannel {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)]
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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("定时任务执行失败"));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user