给 Direct 回合的静默早退补上诊断留痕
TurnReservation 兜底收口时写应用日志,并用 thread::panicking 区分 panic 与 future 被丢弃/取消 新增 Direct 回合 panic hook:task-local 携带回合身份,panic 时记录位置与负载 两处均 cfg(not(test)),不污染用例与本机 AppData
This commit is contained in:
@@ -84,13 +84,80 @@ impl Drop for TurnReservation {
|
||||
fn drop(&mut self) {
|
||||
// 兜底:任务 panic、future 被丢弃、或今后在终态之前新增的 `?` 早退。
|
||||
// 这类失败说不出原因,只给分类;能说清原因的错误必须由调用方在更早的地方显式收口。
|
||||
let _ = self.finish_if_unfinished(DirectTurnTerminal::host_dropped());
|
||||
let finalized_by_guard = self.finish_if_unfinished(DirectTurnTerminal::host_dropped());
|
||||
// 兜底一旦真的收口,就说明这一轮**从未写下终态**:深层既没成功也没失败地退出了。
|
||||
// 这条必须留应用日志,否则离线只剩一个 `phase=working` 的账本,无从判断是哪一层
|
||||
// 提前退出(真实案例:2026-10-02 连续两轮只留下 working 账本,errors/ 与
|
||||
// application.log 全空)。测试态不写,避免用例污染本机 AppData。
|
||||
#[cfg(not(test))]
|
||||
if finalized_by_guard {
|
||||
let _ = crate::append_application_log(
|
||||
"error".to_string(),
|
||||
"direct-turn".to_string(),
|
||||
format!(
|
||||
"agent.direct_turn.host_dropped threadId={} turnToken={} userItemId={} panicking={}",
|
||||
self.thread_id,
|
||||
self.token,
|
||||
self.user_item_id.as_deref().unwrap_or("-"),
|
||||
// true = 这一轮是被 panic 带走的;false = future 被丢弃或取消。
|
||||
std::thread::panicking()
|
||||
),
|
||||
);
|
||||
}
|
||||
// 占用的释放就是"这个项目腾出了跑回合的位置":踢一脚,队里排着的消息不必等下一次用户动作。
|
||||
// panic 也走这里,所以队列不会因为一个任务炸掉而永久停住。
|
||||
kick_queue_dispatch(Path::new(&self.thread_id));
|
||||
}
|
||||
}
|
||||
|
||||
// 运行段经 task-local 携带回合身份,panic hook 据此把 panic 位置与负载写进应用日志。
|
||||
// task-local 而非 thread-local:多线程 runtime 下 future 会跨 worker 迁移。
|
||||
tokio::task_local! {
|
||||
static DIRECT_TURN_PANIC_CONTEXT: Option<(String, String)>;
|
||||
}
|
||||
|
||||
/// 兜底诊断:Direct 回合任务 panic 时,把位置与负载写进应用日志。
|
||||
///
|
||||
/// 没有它时,panic 只会让 `TurnReservation::drop` 把回合收成 `HostDropped`——那是"说不出原因"
|
||||
/// 的分类,离线只留一个 `phase=working` 的账本,`.agent/runtime/errors/` 与 `application.log`
|
||||
/// 全空,无法判断是哪一层退出的(真实案例:2026-10-02 连续两轮如此)。
|
||||
fn ensure_direct_turn_panic_hook() {
|
||||
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let previous = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
if let Ok(Some((thread_id, token))) = DIRECT_TURN_PANIC_CONTEXT.try_with(Clone::clone) {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|location| {
|
||||
format!(
|
||||
"{}:{}:{}",
|
||||
location.file(),
|
||||
location.line(),
|
||||
location.column()
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "未知位置".to_string());
|
||||
#[cfg(not(test))]
|
||||
let _ = crate::append_application_log(
|
||||
"error".to_string(),
|
||||
"direct-turn".to_string(),
|
||||
format!(
|
||||
"agent.direct_turn.panic threadId={} turnToken={} location={} payload={}",
|
||||
thread_id,
|
||||
token,
|
||||
location,
|
||||
info.payload_as_str().unwrap_or("未知 panic 负载")
|
||||
),
|
||||
);
|
||||
#[cfg(test)]
|
||||
let _ = (thread_id, token, location);
|
||||
}
|
||||
previous(info);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// 踢一脚:让队首那条待发消息有机会变成一轮真的回合。
|
||||
///
|
||||
/// 幂等,三个调用点:**入队之后**(队列空且没有回合在跑时,放行就是这一脚,不必再等一个调度周期)、
|
||||
@@ -109,10 +176,12 @@ pub(crate) fn kick_queue_dispatch(root: &Path) {
|
||||
crate::platform_session::current_platform_session_write_state().identity_generation;
|
||||
let reservation = TurnReservation::resume(&thread_id, &dispatched);
|
||||
let root = root.to_path_buf();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
ensure_direct_turn_panic_hook();
|
||||
let panic_context = Some((thread_id.clone(), dispatched.token.clone()));
|
||||
tauri::async_runtime::spawn(DIRECT_TURN_PANIC_CONTEXT.scope(panic_context, async move {
|
||||
run_dispatched_direct_turn(root, dispatched, reservation, release_identity_generation)
|
||||
.await;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。
|
||||
|
||||
Reference in New Issue
Block a user