共享队列架构¶
类型:wiki(知识沉淀) 描述:v0.10.0 后的核心调度模型 — 多任务共用一个调度器、Tab/worker 是全局资源 最后更新:2026-05-26(v0.10.14) 相关源码:
src/entrypoints/background/batch-controller.ts、src/entrypoints/background/task-manager.ts、src/utils/engine-manager.ts相关 rules:docs/rules/settings-change-pre-check.md
是什么¶
v0.10.0 起,所有用户任务共用一个全局调度器:
- 任务 = 用户在「任务页」创建的(关键词 × 城市 × 国家组合),生成 URL 列表
- 调度器 = 后台 service worker 里的 pumpScheduler / manageQueue
- 资源 = 浏览器 Tab、HTTP fetch worker
- 队列 = 全局,所有任务的 URL 进同一池子
[Task A: 100 URL]──┐
[Task B: 1 URL]──┼─→ 共享队列 ─→ Worker 池(N 槽)─→ Tab / Fetch
[Task C: 50 URL]──┘ ↑
N = maxConcurrentTasks
/ deepScrapeConcurrency
/ pagerConcurrency
为什么这么设计¶
v0.9.x(老版本)的死法¶
每个 task 自带独立 BatchState,N 个调度器在共享窗口里互相抢焦点。
真实事故(v0.9.40 dentist 任务):
- 两个任务同时跑,第二个开起来后 0 进度持续 53 分钟
- 根因:每个 batch 的 openTabInShared 都用 active: true 开 tab
- 新 tab 抢前台 → 上一个 tab 被推到后台 → Chromium 后台节流(timer ≥1s, RAF 暂停)
- content script 永远初始化不完 → 永远不发 'batch-tab-ready' → tab 永远不会 promote
- setTimeout(60s 超时) 也可能死于 SW 重启
用户洞察(v0.10.0 启动原因):
"多个任务不应该是共用一个序列吗?"
完全正确。调度对象(窗口、tab)是共享的,调度者却被切成 N 个独立实例,必然撞车。
调度算法¶
地图抓取(batch-controller.ts)¶
pumpScheduler() 在 activeTabs.size < maxConcurrentTasks 时循环:
1. round-robin 挑一个 status='running' 且还有 URL 的 task
2. 从该 task 取 nextIndex URL
3. 开 tab(active: false,不抢焦点)
4. 注册 activeTabs[tabId] = {taskId, urlIndex, state}
5. 60s 超时计时器
tab 完成(onTabDone / 超时 / 异常):
- 移出 activeTabs
- 对应 task 的 finishedCount++
- 重新 pumpScheduler 吸下一个 URL(可能是别的 task 的)
网站抓取(engine-manager.ts)¶
manageQueue() 在 activeTasks < deepScrapeConcurrency 时循环:
1. 从 MapTaskData(全局表)查 scrape_status=0 的网址
2. 过滤:域名黑名单 / 单域名并发上限
3. 开始 processWebsiteScrape(id, websiteUrl)
4. activeTasks++
5. 完成后 activeTasks--,重新触发
→ N 个 worker 同时跑,1 任务 / 100 任务行为相同
关键代码位置¶
| 文件 | 位置 | 作用 |
|---|---|---|
batch-controller.ts |
1-55 行注释 | 架构总述(必读) |
batch-controller.ts |
pumpScheduler |
地图 Tab 调度核心 |
batch-controller.ts |
pumpPager |
翻页 Fetch 串行处理 |
task-manager.ts |
pumpTasks |
任务级调度(决定哪些 task 是 running) |
engine-manager.ts |
manageQueue |
网站抓取调度核心 |
engine-manager.ts |
domainActive Map |
单域名并发计数(防对方站打挂) |
字段速查(与字段语义对照)¶
| 字段 | 全局/单任务 | 控制什么 | 上限 |
|---|---|---|---|
maxConcurrentTasks |
全局 | activeTabs.size 上限 | 1-10 |
pagerConcurrency |
全局 | 翻页 Fetch 并发 | 1-5 |
deepScrapeConcurrency |
全局 | 网站抓取 worker 数 | 1-10 |
deepScrapeDomainConcurrency |
per-域名 | 单 hostname 同时几个 worker | 1-10 |
requestConcurrency |
已废弃(v0.10.0+) | — | — |
详见 settings-field-semantics.md。
易踩坑¶
⚠️ 坑 1:望文生义命名 → 误导用户(反复犯)¶
- v0.10.12:
maxConcurrentTaskslabel 写成「= 同时进行的任务数」 → 错 - v0.10.14:
deepScrapeConcurrencylabel 写成「同时挖几个网站」 → 错
正确心智:池子里有 N 个 worker,从全局队列抢任务。与任务数完全无关。
⚠️ 坑 2:以为 1 个任务用不满 N 个 Tab¶
错。1 个任务的 URL 列表(关键词 × 城市 = 笛卡尔积)可能 100+ 个 URL,能用满任何 N。
⚠️ 坑 3:以为 deepScrapeConcurrency 等于「网站个数」¶
错。它是 URL 抓取并发数。单网站可能有 10 个 URL(multi-keyword 重复商家),如果 deepScrapeDomainConcurrency=2,则该网站最多 2 个 worker,剩 8 个排队。
修改这块时要 / 不要做什么¶
- ✅ 加新调度字段 → 必须在本文件加一行 + 字段对照表
- ✅ 改 round-robin 算法 → 必须在「调度算法」章节同步
- ❌ 不要在 settings UI 出现「per-task」字眼(除非真是 per-domain)
- ❌ 不要直接读
requestConcurrency(已废弃)
版本里程碑¶
| 版本 | 事件 |
|---|---|
| v0.9.x | per-task 调度器,多任务撞车 |
| v0.10.0 | 共享队列重写(本架构起点) |
| v0.10.12 | 修正 maxConcurrentTasks label 误导 |
| v0.10.14 | 修正 deepScrapeConcurrency label 误导 + 加跨根域跟随 |