PROTOCOL.md 7.5 KB

界面 ↔ 引擎 运行期协议(v2)

代码里的唯一事实来源是 tiktok-app/src/protocol.js 与 tiktok-auto/src/protocol.js (两份实体文件,除挂载点一行外逐字节相同,由 tiktok-app/tools/protocol-check.js 强制)。 本文档是给人读的解释,任何数字与代码冲突时以 Protocol.SPEC 为准。

1. 为什么要协议

两条进程(界面主脚本 + engines.execScriptFile 起的子引擎)之间只有文件通道, 没有任何共享内存。在此之前,两边各自写死判断:

症状 原因
引擎在做 25 秒的 watch,界面却报"引擎已退出" 没有心跳,只能靠 engines.all() + 4 秒宽限期猜
点了开始、界面说"运行中",其实引擎连文件都还没读 启动间隙是模糊态
点了停止、界面说"正在收尾",其实引擎还没听见 停止只有一段超时,说不清是没听见还是没做完
状态枚举散在三处(engine.js / main.js / records.js) 漏改一处就变成零报错的悬挂

2. 文件分工

文件 写方 读方 频率
run-state.json 引擎主流程 bridge.report 界面 App.pollEngine ~1 次/条目 + 首尾
engine-beat.json 引擎心跳线程(唯一写者) 界面 每 2 秒
engine-config.json 界面 引擎 1 次/run
stop.flag 界面 引擎主流程 + 心跳线程 1 次/run
run-log.jsonl 引擎 logger sink 界面(执行日志页) 流式
run-config.json 界面 人(没有程序读者) 1 次/run

心跳单独成文件的理由:run-state 由主流程按条目写,心跳每 2 秒一次, 两个写者写同一个文件必然互相覆盖,而且频率差一个数量级。

3. 数据结构

3.1 run-state.json(v2 只增不改)

{
  "v": 2,
  "runId": "run-1783000000000-12345",
  "status": "running",
  "phase": "搜索 fashion",
  "done": 2,
  "total": 5,
  "seq": 17,
  "startedAt": 1783000001234,
  "updatedAt": 1783000045678,
  "engine": { "task": "video", "capabilities": { "heartbeat": true } },
  "message": "",
  "summary": null
}
  • seq 单调递增(每次 report +1),用来判断"同一毫秒内的两次写谁更新"
  • engine.capabilities.heartbeat 是正向证据:只有引擎真的把心跳线程起起来才会是 true
  • engine.task 由 bridge.setTask() 在 config.resolve 之后补,用它排障"跑的是哪条链路"

3.2 engine-beat.json

{ "v": 2, "runId": "run-…", "seq": 42, "beatAt": 1783000045000, "stopSeenAt": 0 }
  • stopSeenAt != 0 = 心跳线程确认"看见了本轮停止请求",是停止 ack 的快速通道
  • sticky:一旦写上就不再归零,直到进程结束

4. 状态迁移

from → starting running stopping finished/stopped failed
(无) ✅
starting 幂等 ✅ ✅ ✅(极短任务) ✅(配置无效)
running 幂等 ✅ ✅ ✅
stopping 幂等 ✅ ✅
终态 无出边

Protocol.legality(from, to) 返回:

返回值 含义 调用方应有的反应
ok 表里有的合法迁移 正常处理
idempotent 原地重复上报 忽略
regress 从终态往外走 拒绝
unknown 表里没有(多半是前向兼容来的新状态) 放宽,只在日志留一句,绝不据此结束运行

5. 阈值(Protocol.SPEC.thresholds)

常量 值 语义
beatIntervalMs 2000 心跳间隔
beatFreshMs 8000 新鲜。只用于"加速判死",不产生任何文案
beatDeadMs 15000 判死
startAckSoftMs 6000 只把文案降成「启动中(较慢)」,不改状态
startAckMaxMs 15000 判 failed,并补写 stop.flag(挡住迟到的引擎)
stopAckMs 3000 等"引擎承认收到停止"
stopFinishMs 10000 承认之后等"真的收完尾"
legacyChildGraceMs 4000 v1 回退:子引擎宽限期(改造前的值)
legacyStopTimeoutMs 8000 v1 回退:停止超时(改造前的值)
legacyNoBeatDeadMs 30000 引擎说有 v2 但心跳线程没起来时的判死阈值

5.1 判活公式(界面侧 App.pollEngine)

anchor = beatAt>0 ? beatAt : (ackedAt || startedAt)
limit  = capabilities.heartbeat ? beatDeadMs : legacyNoBeatDeadMs
accel  = capabilities.heartbeat ? beatFreshMs : legacyChildGraceMs

判死 = (now - anchor > limit)                       // 主判据:心跳
     || (!childAlive() && now - anchor > accel)      // 加速:列表里也没了

★ 反过来的方向不成立:心跳新鲜时,engines.all() 说"没有子引擎"绝不能判死。 引擎在做长等待时从列表里消失是常态,这正是要防的那类误判。

★ v1(run-state 里没有 v):完全走老口径 —— sawChild && startedAt > 4s && !childAlive()。

6. 前向兼容

  1. 未知 status → 按非终态处理,永不据此结束运行
  2. 未知字段 → 忽略(写方唯一,每次写全量)
  3. v 高于本机 → 只读降级,只认 runId/status/phase/done/total,不报错
  4. 协议档次只看两件事:
    • run-state.v >= 2,或
    • 已经收到过本轮的 engine-beat.json
    • ★ 反例:「现在还没有心跳文件」不是 v1 的证据 —— 新引擎刚起来那一两秒也没有
  5. 半截 JSON → readState/readBeat 返回 null,任何状态都不改变(尤其不得据此判死)
  6. 不用文件 mtime 判活(files 的 stat 语义在各运行时不一致)

7. 停止的三段与两段超时

界面                                    引擎
 │ 写 stop.flag (内容=runId)              │
 │ 状态条:正在通知引擎                    │
 │ ────────────────────────────────────▶ │ 心跳线程每 2s 读一次 flag
 │                                       │ 命中 → 写 beat.stopSeenAt(sticky)
 │ ◀──────────────────────────────────── │
 │ stopAckedAt = now → 正在收尾            │ 主流程 wait.tick → checkStop
 │                                       │ → report(stopping) → throw StopSignal
 │ ────────────────────────────────────▶ │ finally: finish(stopped) → 停心跳
 │ ◀──────────────────────────────────── │ run-state.status = stopped
 │ finishRun()                           │
  • ack 通道有两条:心跳的 stopSeenAt(快,不受主流程阻塞影响)和 run-state.status === 'stopping'(慢,要等主流程走到下一个 tick)
  • 超时:ack 3 秒;ack 之后 10 秒;两者都没等到就按 13 秒总时长收口,并如实说明是哪一段没等到
  • v1 引擎没有心跳,沿用旧的单段 8 秒

8. 改了协议要跑什么

cd tiktok-app && $NODE tools/protocol-check.js   # 两份协议一致 + 迁移表自洽 + 阈值关系
$NODE tools/build.js && $NODE tools/smoke.js     # 界面行为
cd ../tiktok-auto && $NODE tools/build.js && $NODE tools/pc-selftest-bridge.js

★ protocol-check 里那条"两侧除挂载点外逐字节相同"是硬约束: 要改就改一份再同步另一份(sed 替换挂载行即可),不要两边手改。