跳转至

title: [ISSUE-0015] Watchdog 鲁棒性三连:并发触发 / nuke 不真关 tab / 计数残留 description: "watchdog 鲁棒性三连:并发 / nuke 不真关 tab / 计数残留" tags: [issue, scraping] created: 2026-05-26 updated: 2026-05-26 type: issue status: fixed severity: medium


[ISSUE-0015] Watchdog 鲁棒性三连:并发触发 / nuke 不真关 tab / 计数残留

相关源码src/utils/scrape-watchdog.tssrc/entrypoints/background/batch-controller.tssrc/utils/engine-manager.ts 发现途径:v0.10.19 代码审查

用户感知的现象

三个相关的小问题(agent 报的 #5 / #7 / #14),都不严重但累积起来体感不好:

  1. 并发触发:用户连续操作(点开始 → 改设置 → 再开始),watchdog 可能被多次唤起,看到「累积异常」通知反复弹
  2. nuke 不真关 tab:连续异常 3 次触发核弹重启时,浏览器里的孤儿 tab 没被关 → 用户还能看到一堆 Google Maps tab
  3. handleScrapeFailure 退场时:连续失败 15 次 → 关 alarm 但 local:watchdogConsecutive 留旧值 → 下次启动从老计数继续 → 提前触发 nuke

根因分析

#5 并发触发

runWatchdog() 函数没有 mutex。alarm 5 分钟一次本来不会撞,但: - 用户点「开始」会 syncWatchdogAlarm() 重建 alarm - 重建瞬间 alarm 可能立刻 fire 一次 - 同时正常周期的 fire 也在跑 - 两次 runWatchdog 同时读 CONSECUTIVE_KEY → 各自 +1 → 各自重启

#7 nuke 不真关 tab

export function nukeAllSchedulerState(): void {
  for (const ab of activeTabs.values()) {
    if (ab.timer) clearTimeout(ab.timer);
  }
  activeTabs.clear();
  ...
}

注释「调用前应该已经 forceCloseSharedWindow」 — 但若 forceClose 失败(窗口已不存在 / API 异常),nuke 后浏览器里 tab 还在,内存 Map 已 clear → 下次 watchdog 又判定为孤儿。

#14 consecutive 残留

engine-manager.tshandleScrapeFailure

if (continuousFailures >= FAILURE_THRESHOLD) {
  await storage.setItem('local:is_engine_running', false);
  await browser.alarms.clear('engine_heartbeat');
  await browser.alarms.clear('scrape_watchdog');  // ← 清 alarm
  // 但没清 local:watchdogConsecutive
}

修复方案

#5 加 mutex

let isRunning = false;
export async function runWatchdog() {
  if (isRunning) return { ... };  // 上一次还没跑完
  isRunning = true;
  try {
    // ...原逻辑
  } finally {
    isRunning = false;
  }
}

#7 nuke 时真关 tab

export async function nukeAllSchedulerState(): Promise<void> {
  // 改成 async,关 tab 再清状态
  const tabIds = Array.from(activeTabs.keys());
  for (const tabId of tabIds) {
    const ab = activeTabs.get(tabId);
    if (ab?.timer) clearTimeout(ab.timer);
    try {
      await browser.tabs.remove(tabId);
    } catch (e) { /* tab 可能已关 */ }
  }
  activeTabs.clear();
  ...
}

#14 清 consecutive

if (continuousFailures >= FAILURE_THRESHOLD) {
  // ...
  await browser.alarms.clear('scrape_watchdog');
  await storage.setItem('local:watchdogConsecutive', 0); // v0.10.20 加
}

改动文件

文件 改了什么
src/utils/scrape-watchdog.ts runWatchdog 加 isRunning mutex
src/entrypoints/background/batch-controller.ts nukeAllSchedulerState 改 async + browser.tabs.remove
src/utils/engine-manager.ts handleScrapeFailure 加清 watchdogConsecutive

验证方式

  1. 多次点击「开始」「停止」「设置变更」 → watchdog 日志应单次执行,不重复
  2. 制造异常累积场景(断网 + 跑任务)→ 核弹触发时浏览器里所有 scrape tab 应被一并关闭
  3. 触发 15 次连续失败 → 重启引擎 → consecutive 应从 0 开始

如何避免再犯

  • 任何"周期性 + 事件触发"的 alarm/handler 都需要 mutex:alarm 触发时机不可控
  • nukeAllSchedulerState 类的"核弹"函数必须自包含,不能假设调用方先做了什么
  • 清 alarm 时同时清相关 storage:alarm 是行为,storage 是状态,两者必须同步

相关问题