use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use crate::bridge::config::BridgeApp;
use crate::ilink::types::{HubExt, SendMessageRequest, WeixinMessage};
use super::handle::handle_one_message;
use super::send::{sanitize_errmsg, HubClient, SendOutcome};
use super::BridgeStop;
use crate::bridge::ApprovalBroker;
pub(super) fn session_dispatch_key(msg: &WeixinMessage) -> String {
let ctx = msg.context_token.as_deref().unwrap_or("");
let session_name = msg
.ilink_hub_ext
.as_ref()
.and_then(|e| e.session_name.as_deref())
.unwrap_or("default");
format!("{ctx}:{session_name}")
}
pub(super) async fn run_session_worker(
key: String,
mut rx: mpsc::Receiver<WeixinMessage>,
client: HubClient,
app: Arc<BridgeApp>,
stop_tx: tokio::sync::watch::Sender<Option<BridgeStop>>,
shutdown: CancellationToken,
approval_broker: Arc<ApprovalBroker>,
) {
const SESSION_WORKER_MAX_BACKOFF_SECS: u64 = 60;
let mut consecutive_failures: u32 = 0;
loop {
let msg = tokio::select! {
biased;
_ = shutdown.cancelled() => return,
msg_opt = rx.recv() => match msg_opt {
Some(m) => m,
None => {
info!(session_key = %key, "session worker exiting");
return;
}
},
};
let ctx_for_err = msg.context_token.clone().unwrap_or_default();
let from_for_err = msg.from_user_id.clone().unwrap_or_default();
let session_name_for_err = msg
.ilink_hub_ext
.as_ref()
.and_then(|e| e.session_name.clone())
.filter(|s| !s.trim().is_empty());
let result = tokio::select! {
biased;
_ = shutdown.cancelled() => {
if app.send_error_reply && !ctx_for_err.is_empty() {
let mut req = SendMessageRequest::reply(
ctx_for_err,
"⚠️ 响应中断(服务正在重启),请稍后重发消息".to_string(),
&from_for_err,
);
if let Some(ref sn) = session_name_for_err {
if let Some(ref mut msg) = req.msg {
let ext = msg.ilink_hub_ext.get_or_insert_with(HubExt::default);
ext.session_name = Some(sn.clone());
}
}
match client.sendmessage(req).await {
Ok(SendOutcome::Sent) => {}
Ok(SendOutcome::Throttled { ret, errmsg }) => {
warn!(
ret,
errmsg = sanitize_errmsg(errmsg.as_deref()).as_deref(),
"sendmessage throttled during shutdown error reply; user did NOT receive restart notice — M3 must cover this path when adding buffer+retry"
);
}
Err(e) => warn!(error = %e, "failed to send shutdown error reply"),
}
}
return;
}
r = handle_one_message(&client, &app, msg, shutdown.clone(), &approval_broker) => r,
};
match result {
Ok(()) => {
consecutive_failures = 0;
}
Err(HandleError::Fatal(reason)) => {
error!(session_key = %key, reason = ?reason, "fatal CLI error; signalling bridge stop");
let _ = stop_tx.send(Some(reason));
return;
}
Err(HandleError::Transient(e)) => {
consecutive_failures = consecutive_failures.saturating_add(1);
let backoff_secs =
SESSION_WORKER_MAX_BACKOFF_SECS.min(1_u64 << consecutive_failures.min(63));
error!(
session_key = %key,
error = %e,
consecutive_failures,
backoff_secs,
"message handler failed; backing off before next message"
);
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
}
}
}
}
pub(super) enum HandleError {
Transient(anyhow::Error),
Fatal(BridgeStop),
}
impl From<anyhow::Error> for HandleError {
fn from(e: anyhow::Error) -> Self {
HandleError::Transient(e)
}
}
const DEFAULT_SESSION_QUEUE_SIZE: usize = 200;
const MAX_SESSION_WORKERS: usize = 512;
pub(super) struct SessionDispatcher {
pub(super) senders: std::sync::Mutex<HashMap<String, mpsc::Sender<WeixinMessage>>>,
client: HubClient,
app: Arc<BridgeApp>,
stop_tx: tokio::sync::watch::Sender<Option<BridgeStop>>,
shutdown: CancellationToken,
approval_broker: Arc<ApprovalBroker>,
sessions_dropped_on_cap: Arc<AtomicU64>,
}
impl SessionDispatcher {
pub(super) fn new(
client: HubClient,
app: Arc<BridgeApp>,
stop_tx: tokio::sync::watch::Sender<Option<BridgeStop>>,
shutdown: CancellationToken,
approval_broker: Arc<ApprovalBroker>,
) -> Self {
Self {
senders: std::sync::Mutex::new(HashMap::new()),
client,
app,
stop_tx,
shutdown,
approval_broker,
sessions_dropped_on_cap: Arc::new(AtomicU64::new(0)),
}
}
pub(super) async fn dispatch(&self, msg: WeixinMessage) {
let key = session_dispatch_key(&msg);
if self.approval_broker.deliver(&key, &msg) {
return;
}
let mut senders = self.senders.lock().unwrap_or_else(|e| e.into_inner());
let needs_new = match senders.get(&key) {
Some(tx) => tx.is_closed(),
None => true,
};
if needs_new {
if senders.len() >= MAX_SESSION_WORKERS {
senders.retain(|_, tx| !tx.is_closed());
if senders.len() >= MAX_SESSION_WORKERS {
let total_dropped =
self.sessions_dropped_on_cap.fetch_add(1, Ordering::Relaxed) + 1;
warn!(
session_key = %key,
cap = MAX_SESSION_WORKERS,
active = senders.len(),
sessions_dropped_on_cap = total_dropped,
"session worker cap reached, dropping message"
);
return;
}
}
let (tx, rx) = mpsc::channel(DEFAULT_SESSION_QUEUE_SIZE);
senders.insert(key.clone(), tx.clone());
let client = self.client.clone();
let app = Arc::clone(&self.app);
let stop_tx = self.stop_tx.clone();
let shutdown = self.shutdown.clone();
let approval_broker = Arc::clone(&self.approval_broker);
tokio::spawn(run_session_worker(
key.clone(),
rx,
client,
app,
stop_tx,
shutdown,
approval_broker,
));
}
if let Some(tx) = senders.get(&key) {
match tx.try_send(msg) {
Ok(_) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
warn!(session_key = %key, "session queue full, dropping message");
}
Err(mpsc::error::TrySendError::Closed(_)) => {}
}
}
}
pub(super) fn evict_closed_senders(&self) {
if let Ok(mut senders) = self.senders.lock() {
senders.retain(|_, tx| !tx.is_closed());
}
}
#[cfg(test)]
pub(super) fn sender_keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self
.senders
.lock()
.unwrap_or_else(|e| e.into_inner())
.keys()
.cloned()
.collect();
keys.sort();
keys
}
}