feat: serialize keyed tasks across queues with durable fencing tokens
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
| `/v1/tasks/{task_id}/attempts/{attempt_id}/evidence` 与证据详情 | P2 已实现 | [执行历史](execution-history.md) |
|
||||
| `/v1/queues` 创建、查询及 `:configure` 控制 | 已实现队列并发、开始速率、积压背压与暂停/恢复 | [队列接口与 SDK 场景](queues.md) |
|
||||
| `/v1/application/resources` 查询及 `:configure` 管理 | P3 已实现跨命名空间应用配额 | [应用配额与 SDK 场景](application-resources.md) |
|
||||
| 任务 `concurrency_key` 与尝试 `concurrency_token` | P3 已实现跨队列资源键互斥 | [资源键接口与 SDK 场景](keyed-concurrency.md) |
|
||||
| 凭据校验与初始化 CLI | P1 已实现 | [鉴权](authentication.md) |
|
||||
| `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) |
|
||||
| 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) |
|
||||
|
||||
@@ -54,6 +54,7 @@ curl "$ORBIT_URL/v1/tasks/$TASK_ID/attempts/$ATTEMPT_ID/evidence/$EVIDENCE_ID" \
|
||||
| --- | --- | --- |
|
||||
| id、task_id | string / 是 | 尝试与父任务标识 |
|
||||
| epoch | 十进制字符串 / 是 | 单任务执行代次;SDK uint64 |
|
||||
| concurrency_token | 十进制字符串 / 否 | 有显式资源键时返回跨任务单调许可序号;SDK uint64,无键时为零并省略;历史查询不能授权执行,见 [资源键契约](keyed-concurrency.md) |
|
||||
| state | string / 是 | LEASED、RUNNING、SUCCEEDED、FAILED、CANCELLED、EXPIRED、UNKNOWN;枚举不表示全部状态转换已实现 |
|
||||
| revision | 十进制字符串 / 是 | 记录版本,续租及归属变化可递增;SDK uint64 |
|
||||
| lease_until | UTC RFC 3339 / 是 | 数据库记录的租约期限,**查询不提供执行授权** |
|
||||
|
||||
117
docs/api/keyed-concurrency.md
Normal file
117
docs/api/keyed-concurrency.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 资源键互斥与单调许可序号
|
||||
|
||||
状态: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、资源管理/清理接口、优先级公平性或生产容量结论。验证证据见 [本阶段报告](../reports/phase-3-keyed-concurrency.md)。
|
||||
@@ -24,6 +24,7 @@ P2 进展:服务端执行内核、WSS 与独立自动 Worker SDK 已可产生
|
||||
| payload_version | string | 必填 | 输入契约版本,1–32 字符 |
|
||||
| payload | JSON | 必填 | 非 null JSON,最大 256 KiB;平台不解释业务字段 |
|
||||
| business_key | string | 可选 | 最多 191 个 Unicode 字符,无 NUL/换行;用于关联,不提供业务幂等 |
|
||||
| concurrency_key | string | 可选,空为关闭 | 1–191 个 Unicode 字符,无控制字符和首尾空白;显式资源互斥键,同命名空间跨队列共享,见 [资源键契约](keyed-concurrency.md) |
|
||||
| available_at | time / null | 受理时数据库时间 | 最早可执行时间;必须早于总截止时间,仅允许最多 1 秒的过去时间容差 |
|
||||
| start_deadline_at | time / null | 无独立限制 | 必须晚于当前时间,不早于 available_at,不晚于总截止时间 |
|
||||
| task_timeout_ms | uint64 | 0 或省略为 86400000 | 从受理时计算的总时限,最大 30 天(2592000000 毫秒) |
|
||||
@@ -90,7 +91,7 @@ curl -i "$ORBIT_URL/v1/tasks" \
|
||||
}
|
||||
```
|
||||
|
||||
可选响应字段:start_deadline_at、result、cancel_requested_at、completed_at、business_key,未设置时省略。effect_uncertain=true 表示存在尚未核实的授权执行效果,false 时省略;详情、摘要和 Worker 派发均可返回。等待重试时 result/completed_at 省略,历史结果在尝试中保留。终态有 completed_at。状态集合为 WAITING / LEASED / RUNNING / NEEDS_REVIEW / SUCCEEDED / FAILED / CANCELLED;P1 仅主动产生 WAITING 和 CANCELLED。
|
||||
可选响应字段:start_deadline_at、result、cancel_requested_at、completed_at、business_key、concurrency_key,未设置时省略。effect_uncertain=true 表示存在尚未核实的授权执行效果,false 时省略;详情、摘要和 Worker 派发均可返回。等待重试时 result/completed_at 省略,历史结果在尝试中保留。终态有 completed_at。状态集合为 WAITING / LEASED / RUNNING / NEEDS_REVIEW / SUCCEEDED / FAILED / CANCELLED;P1 仅主动产生 WAITING 和 CANCELLED。
|
||||
|
||||
### 幂等与失败处理
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ ACK 的 body 根据原请求选择:worker.capacity → welcome,worker.resume
|
||||
- instance_secret/lease_secret 必须是 32 字节秘密的严格无填充 base64url,长度 43;知道资源 ID 无法替代秘密。
|
||||
- 结果为非 null JSON;FAILED/UNKNOWN/CANCELLED 必须携带稳定 error_code;SUCCEEDED 省略或使用空 error_code。
|
||||
- hello.cancellation_notifications 可选且默认 false;Lease 可返回 cancel_requested_at。CANCELLED 需要事务核验已保存意图;通知、取消后执行边界与升级顺序见 [取消契约](cancellation.md)。
|
||||
- hello.concurrency_keys 可选且默认 false,未声明时不派发带资源键任务;task.assign.task.concurrency_key 与非零字符串 concurrency_token 必须同时出现。Schema 检查字段配对,SDK 另核验支持声明和键格式,事务维护许可;见 [资源键契约](keyed-concurrency.md)。
|
||||
- finish.retryable 为可选布尔值,默认 false;true 仅允许 FAILED,Schema 与服务端都校验。派发任务可带 effect_uncertain=true,表示此前授权执行的效果尚未核实。业务重试条件见 [安全重试](retries.md)。
|
||||
- 返回时间使用 UTC RFC 3339,最多微秒精度。验证器应开启 format 断言以校验实际日历日期。
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ go run ./cmd/orbit-admin bootstrap \
|
||||
| capabilities | 1–32 个 handler / handler_version / payload_version 精确版本组合,不可重复 |
|
||||
| capacity | 总执行槽数量,1–256,不是可用槽的累加值 |
|
||||
| cancellation_notifications | 可选 bool,默认 false;true 接收 task.cancel 通知,自动 Worker 默认开启 |
|
||||
| concurrency_keys | 可选 bool,默认 false;true 接收资源键和单调序号,自动 Worker 默认开启;同进程重连不可更改 |
|
||||
|
||||
重连必须保持凭据、队列、处理器集合和进程秘密,处理器集合的排列顺序不影响身份。新连接递增会话代次并使旧会话失效。知道 instance_id 或 task_id 不能接管执行。
|
||||
|
||||
@@ -77,7 +78,7 @@ worker.welcome 返回 session_id、epoch、server_time、expires_at、lease_dura
|
||||
| --- | --- | --- |
|
||||
| worker.resume | AttemptRef,字段见上方 task.start 示例 | 当前 LeaseStatus;成功迁移有效尝试到新会话,不创建尝试或延长期限 |
|
||||
| worker.capacity | sequence 字符串、capacity 0–256、drain bool | 更新后的 welcome 字段 |
|
||||
| task.assign | 服务端主动发送 | task 完整任务、attempt_id、epoch、lease_secret、lease_until、server_time |
|
||||
| task.assign | 服务端主动发送 | task 完整任务、attempt_id、epoch、lease_secret、lease_until、server_time;有资源键时带 concurrency_token 字符串 |
|
||||
|
||||
capacity 使用单调递增序号和覆盖值。同序号同内容可重复,同序号不同内容或旧序号冲突;capacity=0 不释放已有任务的并发许可。drain=true 是进程级持久化排空,重连保留;后续必须保持 drain=true,尝试清除返回 STATE_CONFLICT。新进程使用新实例身份。
|
||||
|
||||
@@ -129,7 +130,9 @@ receipt 是原操作的不可变确认;lease 是本次重新核验的执行权
|
||||
|
||||
平台在同一事务中预占应用并发与开始额度、分配队列并发许可、创建尝试、递增任务执行代次和次数、保存 LEASED 与派发事件。领取本身消耗一次 max_attempts 预算,包括最终未获开始授权的尝试,预算不会因重连或过期恢复重置。
|
||||
|
||||
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。已实施队列 max_concurrency、rate_per_second 及未终结任务上限;参数含义、窗口突发、暂停及降额语义见 [队列资源控制](queues.md)。[应用级配额](application-resources.md) 已同时生效;业务键许可与公平性继续推进。
|
||||
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。已实施队列 max_concurrency、rate_per_second 及未终结任务上限;参数含义、窗口突发、暂停及降额语义见 [队列资源控制](queues.md)。[应用级配额](application-resources.md) 与 [显式资源键许可](keyed-concurrency.md) 同时生效;公平性继续推进。
|
||||
|
||||
同命名空间的 concurrency_key 跨队列互斥,领取事务同时保存键许可和 concurrency_token;原 business_key 仅追踪。没有声明 concurrency_keys 的 Worker 不领取带键任务。资源端需用单调序号和操作幂等保护实际写入,平台无法撤回外部已发出的请求;未知结果释放执行许可并保留核查状态,详细边界和升级顺序见资源键契约。
|
||||
|
||||
领取前核对速率窗口及已有 LEASED 预留,满额时不新建尝试。Start 在同一事务内消费应用与队列的开始预算,原 ACK 重报不重复计数。领取后若配置降额,Start 可能返回 RATE_LIMITED;保持原消息身份在剩余租约内退避核对。续租、结果确认、取消及恢复不消费新开始额度。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user