修复 Web 状态机在启动中停止时卡死

- 启动任务(含注册重试循环)此前是裸 tokio::spawn,stop 打不断:
  服务器不可达时点击停止,状态永远卡在 Starting 或显示 Running 的死网络。
  现在保存 JoinHandle,stop/restart 时先 abort_start_task 中断它,
  defer 守卫负责把状态迁移到 Stopped
- 清理任务此前 spawn 在自己所等待的 task_group 里,形成自引用等待,
  网络自行停止时永不返回;改为在任务组外等待
- 顺带修复 TaskGroup::wait_all_stopped 的丢失唤醒竞态:
  notified() 先 enable 注册再检查条件
- 补充 Starting 状态停止迁移、任务自然结束唤醒两个测试
This commit is contained in:
lbl
2026-08-20 22:21:54 +08:00
parent bd79844849
commit ca72f2b3d4
2 changed files with 91 additions and 5 deletions
+35 -1
View File
@@ -155,10 +155,14 @@ impl TaskGroup {
pub async fn wait_all_stopped(&self) {
loop {
// 先注册等待再检查条件,避免在检查与等待之间丢失唤醒
let notified = self.inner.all_stopped_notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.inner.all_tasks_stopped() {
return;
}
self.inner.all_stopped_notify.notified().await;
notified.await;
}
}
}
@@ -253,3 +257,33 @@ impl Drop for TaskGroupGuard {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 所有任务自然结束后 wait_all_stopped 必须返回。
/// 覆盖两个关键点:任务自然耗尽时 remove_task 置 stopped 并唤醒;
/// 等待方先注册再检查,不会因竞态错过唤醒而永久挂起。
#[tokio::test]
async fn test_wait_all_stopped_after_natural_completion() {
let manager = TaskGroupManager::new();
let (group, _guard) = manager.create_task().unwrap();
let waiter = {
let group = group.clone();
tokio::spawn(async move { group.wait_all_stopped().await })
};
// 让 waiter 先进入等待
tokio::task::yield_now().await;
let _sub = group.spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
});
tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
.await
.expect("wait_all_stopped should return after all tasks complete")
.unwrap();
}
}