Files
lionwebsite-backend/docs/TASK_PROTOCOL_V2.md

6.7 KiB
Raw Permalink Blame History

任务生命周期与协议 v2

范围:主站下载任务的持久化生命周期、与存储节点的可靠上报协议。对应评估报告第 2、6 节。

为什么重写

原实现里创建先POST上游、后落库,删除先删库、后等节点,节点上报异步写完后立刻清队列:上下游任一侧失败都会留下「主站与节点说法不一致」的状态,且没有任何东西能把它们对回来。

复现过的具体后果:模拟状态消息发送失败后,完成任务已从节点队列移除、消息却未发出,于是「归档已存在、主站仍显示未完成」。

不变量

  1. gid 只有一个有效生命周期意图。创建先落唯一 CREATE 意图(并写一条可见的占位任务行),靠 gallery(gid) 主键唯一把并发创建收敛成一个赢家;输家直接得到业务失败。

  2. 上游副作用只在意图持久化之后发生。顺序固定为:登记意图 → 只读解析(prepare,无 POST)→ 持久化 SUBMITTING → 真正 submit。

  3. SUBMITTING 之后绝不自动重发。上游不支持幂等,因此超时/断连/落库失败/重启中断一律落 UNKNOWN 等待人工对账处理,而不是再 POST 一次。

  4. 任务行与周用量在同一短事务里写入;重复落库不重复计量。数据库失败时任务行与意图保持一致(要么都在,要么明确为 UNKNOWN),不允许出现「有任务没计量」。

  5. 删除失败保留记录。先落 DELETE_PENDING 与稳定 deleteOperationId,只有节点确认(或文件已不存在)后才在同一事务里清收藏、删任务行并留 DELETED 墓碑。

  6. 上报按 revision 单调推进,按 operationId 归属。迟到的旧世代上报被丢弃且不改变当前任务;墓碑(DELETED)永久忽略所有上报,已删除任务只能由显式 CREATE 重新建立新世代。

  7. 回执只在提交之后发送。数据库失败不回 ACK,节点保留快照重投;重复上报也回 ACK 以停止重放,但不会重复通知。

  8. 通知与 WebSocket 推送在事务提交之后、由独立执行器发送;慢通知不阻塞状态提交。

  9. 只有确认已提交(SUBMITTED)或迁移前确实提交过的旧任务才允许下发给节点。UNKNOWN、RESERVED、SUBMITTING、DELETE_PENDING、DELETED 一律不下发:上游不支持幂等,把未确认/未计量的任务重投会在节点侧变成「下载完成却 0 额度」。

  10. 启动时先绑定上报处理器与判定解析器、执行恢复(SUBMITTING → UNKNOWN、释放 RESERVED),完成之后才允许连接与自动重投;恢复失败时只保持连接,禁止自动派发。

状态表

层 取值
task_operation.state RESERVED SUBMITTING SUBMITTED UNKNOWN DELETE_PENDING DELETED
任务展示状态 提交中 已提交 下载中 等待压缩 压缩中 下载完成 提交未知 待删除 删除失败
节点上报状态码 1 下载中 2 等待压缩 3 压缩中 4 下载完成(沿用旧码)

状态码到中文文案只在 TaskStatus 一处映射。

线协议

帧格式不变:[messageType(1B)][length(4B, 大端)][JSON]。旧类型 0..9 的字段一个都不动(旧节点 Jackson 会拒绝未知字段)。

类型 消息 关键字段
10 ProtocolHelloMessage protocolVersion=2, features=["reliable-task-v2"]
11 TaskCommandMessage operationId, action=DOWNLOAD|DELETE, gid, name
12 TaskReportMessage reportId, revision, galleryTasks[], operationIds{gid→op}
13 TaskReportAckMessage reportId, result

流程:主站先发旧 Identity,再发类型 10;节点回类型 10 声明 v2 后,才允许使用 11..13。未协商成功时自动退化为旧的 DownloadPostMessage/DeleteGalleryMessage(此时不承诺可靠 ACK,属已知兼容边界)。

部署与迁移

  1. 用 scripts/migrate_task_operation_v2.sql 建表。脚本只有独立语句、不含 BEGIN/COMMIT,由外层 wrapper 包事务并在执行前备份。
  2. 该脚本还会在 gallery(gid) 上建唯一索引。历史库若已有重复 gid,这条语句会失败:先人工确认重复来源再处理,不要直接删行。
  3. 主库连接需要 busy 等待(SqlConfiguration 已为主数据源设置 PRAGMA busy_timeout=5000)。单文件 SQLite 并发开短事务时,没有等待窗口会直接抛出 SQLITE_BUSY。生产库建议同时启用 WAL。
  4. 旧任务行没有操作记录,首次收到上报时会按显式策略补建一次「世代」(优先采纳上报里的 operationId),不会改动其已有状态。

验证

mvn -o test:549 项,0 失败/错误/跳过。

新增关键用例(TaskLifecycleIntegrationTest,真实 SQLite 文件 + 真实事务/触发器/唯一约束,落在 /tmp 临时目录):

  • 意图与占位行先于上游 POST;SUBMITTING 先于 submit。
  • 同 gid 6 线程并发创建:上游只被提交一次、任务行不超过一条。
  • 上游超时后落 UNKNOWN;再次点击创建也不会第二次 POST。
  • 重启恢复把中断的 SUBMITTING 判为 UNKNOWN(任务行同步显示「提交未知」),安全释放 RESERVED 占位,且不改写 operationId、不重发。
  • 额度写入失败时落库事务回滚,任务保持可对账的 提交未知 + UNKNOWN。
  • 重投/重试复用同一 operationId。
  • 判定为拒绝的任务(UNKNOWN 等)绝不下发,也不退化为无 operationId 的旧命令。
  • 删除失败保留任务行与意图,重试复用同一删除 ID;成功后清收藏、删行、留墓碑。删除意图 ID 不匹配时不得完成删除。
  • 正在提交(RESERVED/SUBMITTING)的任务拒绝删除;释放预占只清匹配世代的记录与占位行。
  • 落库严格匹配本次创建世代(expectedOperationId),被删除/换代后不得复活已删 gid。
  • 墓碑永久忽略所有上报(含同一 operationId),只允许显式 CREATE 用新 operationId 重建。
  • 旧节点不带 operationId 的连续上报保持可接受;新 v2 已知世代缺归属的上报仍被拒。
  • 过期 revision、缺 operationId、待删除任务的上报都被丢弃;未知 gid 忽略但回执。
  • 重复上报不重复通知;回执写入失败时 apply 返回不可回执结果。

TaskProtocolV2CodecTest 锁定类型 10..13 的帧头与字段名,并断言旧 DownloadPostMessage 没有混入新字段。

已知限制

未做双机真实故障演练。UNKNOWN 目前只提供状态与持久化记录:任务行会显示「提交未知」并拒绝自动重投/重试,对账与人工解决的接口由后续批次补齐。