6.7 KiB
任务生命周期与协议 v2
范围:主站下载任务的持久化生命周期、与存储节点的可靠上报协议。对应评估报告第 2、6 节。
为什么重写
原实现里创建先POST上游、后落库,删除先删库、后等节点,节点上报异步写完后立刻清队列:上下游任一侧失败都会留下「主站与节点说法不一致」的状态,且没有任何东西能把它们对回来。
复现过的具体后果:模拟状态消息发送失败后,完成任务已从节点队列移除、消息却未发出,于是「归档已存在、主站仍显示未完成」。
不变量
-
gid只有一个有效生命周期意图。创建先落唯一CREATE意图(并写一条可见的占位任务行),靠gallery(gid)主键唯一把并发创建收敛成一个赢家;输家直接得到业务失败。 -
上游副作用只在意图持久化之后发生。顺序固定为:登记意图 → 只读解析(
prepare,无 POST)→ 持久化SUBMITTING→ 真正submit。 -
SUBMITTING之后绝不自动重发。上游不支持幂等,因此超时/断连/落库失败/重启中断一律落UNKNOWN等待人工对账处理,而不是再POST一次。 -
任务行与周用量在同一短事务里写入;重复落库不重复计量。数据库失败时任务行与意图保持一致(要么都在,要么明确为
UNKNOWN),不允许出现「有任务没计量」。 -
删除失败保留记录。先落
DELETE_PENDING与稳定deleteOperationId,只有节点确认(或文件已不存在)后才在同一事务里清收藏、删任务行并留DELETED墓碑。 -
上报按
revision单调推进,按operationId归属。迟到的旧世代上报被丢弃且不改变当前任务;墓碑(DELETED)永久忽略所有上报,已删除任务只能由显式CREATE重新建立新世代。 -
回执只在提交之后发送。数据库失败不回 ACK,节点保留快照重投;重复上报也回 ACK 以停止重放,但不会重复通知。
-
通知与 WebSocket 推送在事务提交之后、由独立执行器发送;慢通知不阻塞状态提交。
-
只有确认已提交(
SUBMITTED)或迁移前确实提交过的旧任务才允许下发给节点。UNKNOWN、RESERVED、SUBMITTING、DELETE_PENDING、DELETED一律不下发:上游不支持幂等,把未确认/未计量的任务重投会在节点侧变成「下载完成却 0 额度」。 -
启动时先绑定上报处理器与判定解析器、执行恢复(
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,属已知兼容边界)。
部署与迁移
- 用
scripts/migrate_task_operation_v2.sql建表。脚本只有独立语句、不含BEGIN/COMMIT,由外层 wrapper 包事务并在执行前备份。 - 该脚本还会在
gallery(gid)上建唯一索引。历史库若已有重复 gid,这条语句会失败:先人工确认重复来源再处理,不要直接删行。 - 主库连接需要 busy 等待(
SqlConfiguration已为主数据源设置PRAGMA busy_timeout=5000)。单文件 SQLite 并发开短事务时,没有等待窗口会直接抛出SQLITE_BUSY。生产库建议同时启用 WAL。 - 旧任务行没有操作记录,首次收到上报时会按显式策略补建一次「世代」(优先采纳上报里的
operationId),不会改动其已有状态。
验证
mvn -o test:549 项,0 失败/错误/跳过。
新增关键用例(TaskLifecycleIntegrationTest,真实 SQLite 文件 + 真实事务/触发器/唯一约束,落在 /tmp 临时目录):
- 意图与占位行先于上游
POST;SUBMITTING先于submit。 - 同
gid6 线程并发创建:上游只被提交一次、任务行不超过一条。 - 上游超时后落
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 目前只提供状态与持久化记录:任务行会显示「提交未知」并拒绝自动重投/重试,对账与人工解决的接口由后续批次补齐。