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

118 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 资源键互斥与单调许可序号
状态:P3 已实现。适合多个队列的任务更新同一份对象、文件或其他共享资源。数据库使用 MySQL 事务,不引入外键、Redis 或额外锁服务。
## 1. 四种身份各自的作用
| 字段 | 作用与范围 |
| --- | --- |
| Idempotency-Key | 一次平台受理命令的身份;原键原内容重报取回原回执 |
| business_key | 业务追踪关联,保持原有语义,不限制并发、不自动去重 |
| concurrency_key | 显式资源键;同一命名空间内跨队列共享一个有效尝试许可 |
| concurrency_token | 此资源键的持久化单调序号;供资源端原子拒绝过时写入,不能代替授权或业务幂等 |
资源键必须由所有相关提交方按同一约定生成。省略键、使用不同键或处于不同命名空间的任务不会参与同一个互斥组。不要把凭据或敏感业务正文放入键中;任务详情与摘要会返回它。
平台控制有效许可,无法撤回已经发往外部的请求。租约到期后旧请求可能仍在运行;资源端需要结合序号与操作幂等保护实际写入。
## 2. HTTP 提交与查询
沿用 [任务接口](tasks.md) 的鉴权、动作、幂等、状态和错误信封,不增加专门的管理接口。
| 位置 / 字段 | 类型 / 默认 | 约束 |
| --- | --- | --- |
| 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`、组合及分解形式的重音字符属于不同键。若业务认为它们相同,应在提交前统一生成规则。
```sh
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` 已初始化):
```go
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](../../sdk/go/examples/keyed-concurrency/main.go)。示例创建两个专用队列,每条队列提交一个相同资源键的任务;两个自动 Worker 各自容量为 4。处理器调用一个受控内存资源模型,查询尝试确认序号增长,最后模拟旧请求验证它被拒绝。
从仓库根目录,在专用开发数据库完成迁移并启动服务:
```sh
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。另一个终端执行:
```sh
cd sdk/go
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=/tmp/orbit-keyed.credentials.json go run ./examples/keyed-concurrency
```
正常输出:
```text
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、资源管理/清理接口、生产容量结论;[优先级与节点公平派发](priority-and-fairness.md) 已补齐。验证证据见 [本阶段报告](../reports/phase-3-keyed-concurrency.md)。