Files
orbit/docs/api/keyed-concurrency.md

8.8 KiB
Raw Permalink Blame History

资源键互斥与单调许可序号

状态:P3 已实现。适合多个队列的任务更新同一份对象、文件或其他共享资源。数据库使用 MySQL 事务,不引入外键、Redis 或额外锁服务。

1. 四种身份各自的作用

字段 作用与范围
Idempotency-Key 一次平台受理命令的身份;原键原内容重报取回原回执
business_key 业务追踪关联,保持原有语义,不限制并发、不自动去重
concurrency_key 显式资源键;同一命名空间内跨队列共享一个有效尝试许可
concurrency_token 此资源键的持久化单调序号;供资源端原子拒绝过时写入,不能代替授权或业务幂等

资源键必须由所有相关提交方按同一约定生成。省略键、使用不同键或处于不同命名空间的任务不会参与同一个互斥组。不要把凭据或敏感业务正文放入键中;任务详情与摘要会返回它。

平台控制有效许可,无法撤回已经发往外部的请求。租约到期后旧请求可能仍在运行;资源端需要结合序号与操作幂等保护实际写入。

2. HTTP 提交与查询

沿用 任务接口 的鉴权、动作、幂等、状态和错误信封,不增加专门的管理接口。

位置 / 字段 类型 / 默认 约束
POST /v1/tasks 的 concurrency_key 可选 string,省略或空字符串表示关闭 非空时 1–191 个 Unicode 字符;有效 UTF-8,不含控制字符,首尾不能有 Unicode 空白;null 拒绝
任务详情、列表摘要、task.assign.task 的 concurrency_key string,可省略 返回受理时冻结值,不能事后换键
task.assign 的 concurrency_token 非零 uint64 十进制字符串,有资源键时必填 无资源键时省略;SDK 为 uint64,不能转换为浮点数
尝试列表、详情的 concurrency_token 同上,有资源键时返回 历史查询用于核对,不授予执行权限
计划版本 definition.task 的 concurrency_key 与提交相同 版本固定值,生成任务时继承;无模板插值或动态表达式

键按 UTF-8 字节精确匹配,不做大小写折叠或 Unicode 规范化。例如 object/A、object/a、组合及分解形式的重音字符属于不同键。若业务认为它们相同,应在提交前统一生成规则。

curl "$ORBIT_URL/v1/tasks" \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: update-object-42-operation-001' \
  --data '{"queue_id":"11111111111111111111111111111111","handler":"object.update","handler_version":"v1","payload_version":"v1","payload":{"operation_ref":"update-001","object_ref":"objects/42"},"concurrency_key":"object/42"}'

响应是任务接口的完整信封,首次 201、重报 200;任务对象增加 "concurrency_key":"object/42"。原幂等键更换资源键返回 409 IDEMPOTENCY_CONFLICT。非法键返回 400 INVALID_ARGUMENT;权限或队列范围沿用任务接口。资源忙时受理仍可成功,任务保持 WAITING,不消耗尝试次数,但占用未完成任务配额并继续计算总截止时间。

3. 许可、释放与恢复

领取在同一事务中预占应用额度、队列许可和资源键许可,创建尝试并分配序号。有键任务同时持有两条许可记录,应用活跃尝试只计一次。许可覆盖 LEASED 和 RUNNING;业务代码必须等到 Start 确认当前执行权后才运行。

情况 资源键处理
Worker 重连、Resume、续租 保留同一尝试和序号;续租同步延长许可
结果重复确认、旧回执重放 不重复释放,不影响后续尝试的许可
未开始取消、明确结束、到期恢复 同事务释放;后续尝试取得更大的序号
安全重试退避 释放本次执行许可,等待下次领取;不在退避期间独占资源
UNKNOWN / NEEDS_REVIEW 释放执行许可,保留未知效果和历史;待核查任务仍占积压配额
暂停队列、停止派发、断开连接 不直接释放在途许可,仍按原租约和结果收尾

因此未知效果不会自动把整个资源键封锁到人工核查结束。若业务要求核查期间禁止其他操作,还需独立的业务冻结状态。当前不提供人工解锁或重置序号接口。

相同 (namespace_id, concurrency_key) 的新许可序号严格递增,允许间隔;内部领取和释放都推进资源行版本。不得删除或重置这条资源行,否则会破坏单调性。序号空间不足时拒绝领取,不能溢出回绕。不同资源之间不比较序号,单任务的 epoch 也不能替代跨任务资源序号。

领取查询在候选 LIMIT 前排除已占用键,避免一批阻塞任务挡住其他键。这不承诺 FIFO、跨队列公平性或最大等待时间。

4. Worker 与 SDK

新版 NewWorker 自动发送 hello.concurrency_keys=true,处理器读取 e.Task.ConcurrencyKey 和 e.ConcurrencyToken。底层接入者须显式设置 registration.ConcurrencyKeys=true,实现资源端校验后再启用。这是整个进程身份的固定注册属性;同进程重连改变它返回 STATE_CONFLICT,升级进程应创建新身份。

未声明支持的旧 Worker 继续消费无键任务,不领取带键任务。SDK 拒绝缺少序号、无键却携带序号、未声明支持却收到有键任务等协议错误。重复派发不能静默替换原键或原序号。

提交片段(api、ctx、queueID 已初始化):

accepted, err := api.SubmitTask(ctx, "update-object-42-operation-001", orbit.SubmitTaskRequest{
    QueueID: queueID, Handler: "object.update", HandlerVersion: "v1", PayloadVersion: "v1",
    Payload: json.RawMessage(`{"operation_ref":"update-001","object_ref":"objects/42"}`),
    ConcurrencyKey: "object/42",
})

资源端需要在一次原子操作里完成:核验命名空间、资源键和调用权限;拒绝 token < highest_token;提升最高序号;按稳定操作身份查重并写入实际结果。同序号的重复请求仍需要操作幂等。仅先查询最高序号、再分开写入会产生竞争;仅在 Worker 内用 mutex 无法保护多进程和外部系统。

最高序号校验只能拒绝资源端已经见过更高序号后的旧写入;更高序号尚未到达时,不能单靠本地最高值证明旧租约仍有效。需要更强授权语义时,资源端还需相应的授权校验或业务版本条件。平台序号也不能替代资源端的身份认证。

5. 完整可运行场景

源码:keyed-concurrency/main.go。示例创建两个专用队列,每条队列提交一个相同资源键的任务;两个自动 Worker 各自容量为 4。处理器调用一个受控内存资源模型,查询尝试确认序号增长,最后模拟旧请求验证它被拒绝。

从仓库根目录,在专用开发数据库完成迁移并启动服务:

go run ./cmd/orbit-migrate up
go run ./cmd/orbit-admin bootstrap --application keyed-example --namespace default \
  --environment test --queue initial --worker-access --output /tmp/orbit-keyed.credentials.json
go run ./cmd/orbit

上述命令使用预先设置的 ORBIT_MYSQL_DSN,服务使用 ORBIT_HTTP_ADDR=127.0.0.1:8080。示例凭据需具有全部队列范围的 queues:manage、tasks:submit/read 和 workers:connect/consume。另一个终端执行:

cd examples
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=/tmp/orbit-keyed.credentials.json go run ./keyed-concurrency

正常输出:

queues=2 completed=2 tokens_increasing=true
resource_writes=2 stale_write_rejected=true worker_drained=true

示例不访问真实外部资源;内存 mutex 和 map 仅说明原子校验位置,不能复制为生产分布式资源存储。正常结束排空 Worker;每次运行创建独立队列、任务和资源键并保留历史。任务总时限 20 秒,失败非零退出并有界收尾,进程被强杀仍需按历史核对。

6. 升级与当前边界

停止旧服务写入,应用全部迁移到 000011,再启动新服务、升级 Worker SDK,最后提交带资源键的任务。旧节点不维护资源键许可,不能与新节点混跑;新 SDK 的 hello 扩展也不能发给尚未升级的严格旧服务。

000011 为历史任务填空键/空摘要、历史尝试填零序号、历史会话填 false,不改变原 business_key。新增四个模型字段均有中文注释和 SQL COMMENT,实体数仍为 33;数据库最终外键数为零。

当前基于一个 MySQL 主库及应用额度锁串行化事务,资源键记录随不同键数量持续增长。尚未提供可调的每键并发大于 1、资源管理/清理接口、生产容量结论;优先级与节点公平派发 已补齐。验证证据见 本阶段报告。