feat: enforce durable queue resource limits and backpressure

This commit is contained in:
2026-09-17 17:25:56 +08:00
parent 3ce7d2c104
commit 5c8825e4bc
38 changed files with 1231 additions and 30 deletions

View File

@@ -11,7 +11,7 @@
| `/v1/tasks` 提交、查询、分页与待执行取消 | P1 已实现 | [任务接口](tasks.md) |
| `/v1/tasks/{task_id}/attempts` 与尝试详情 | P2 已实现 | [执行历史](execution-history.md) |
| `/v1/tasks/{task_id}/attempts/{attempt_id}/evidence` 与证据详情 | P2 已实现 | [执行历史](execution-history.md) |
| `/v1/queues` 创建与查询 | P1 已实现 | [队列接口](queues.md) |
| `/v1/queues` 创建、查询及 `:configure` 控制 | 已实现队列并发、开始速率、积压背压与暂停/恢复 | [队列接口与 SDK 场景](queues.md) |
| 凭据校验与初始化 CLI | P1 已实现 | [鉴权](authentication.md) |
| `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) |
| 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) |

View File

@@ -19,7 +19,7 @@ go run ./cmd/orbit-admin bootstrap \
ORBIT_MYSQL_DSN 通过环境注入,命令不读取 .env。所有名称必填,以字母/数字开头,后续允许字母、数字、`_ . : -`;environment 最长 32,其余最长 128。输出文件必须不存在,以 0600 创建;原始凭据不会打印到 stdout/stderr。`.local/` 和 `*.credentials.json` 已被 Git 忽略。
输出 JSON 包含 application_id / namespace_id / queue_id / credential_id / token / expires_at。初始凭据有效期为 30 天,权限包括当前命名空间内的五个 P1 动作和全部队列。初始队列 max_concurrency=4、rate_per_second=10;这是开发初始配置,执行限流随 P2–P3 落地。
输出 JSON 包含 application_id / namespace_id / queue_id / credential_id / token / expires_at。初始凭据有效期为 30 天,权限包括当前命名空间内的五个 P1 动作和全部队列。初始队列 max_concurrency=4、rate_per_second=10、max_unfinished_tasks=100000;并发、开始限速与积压背压已生效,语义及管理方式见 [队列接口](queues.md)。这些默认值是开发基线,不是容量承诺。
资源、凭据摘要和初始化审计在一个事务中提交。交付文件先写入并同步到磁盘,成功后再提交事务。如果文件写入失败则回滚并移除文件;如果已经交付后提交无法确认,保留文件并报错,应使用其中凭据核对结果,不能直接覆盖或重复创建。

View File

@@ -1,6 +1,6 @@
# 队列接口 v1
状态:P1 已实现创建与查询。更新、暂停、实际派发并发与速率控制随 P2–P3 实现;当前参数是后续执行所需的持久化配置。
状态:已实现创建、查询、配置替换、暂停/恢复、共享并发、开始速率与未终结任务背压。MySQL 是跨节点额度的权威存储;没有额外缓存或消息队列依赖。
## POST /v1/queues
@@ -11,6 +11,7 @@
| name | string | 1–128 字符,以 ASCII 字母/数字开头,其后允许字母、数字、`_ . : -`;命名空间内唯一 |
| max_concurrency | uint32 | 必填,1–100000 |
| rate_per_second | uint32 | 必填,1–1000000 |
| max_unfinished_tasks | uint32 | 可选,省略/零为 100000;有效范围 1–1000000,包含待核查任务 |
```sh
curl "$ORBIT_URL/v1/queues" \
@@ -30,7 +31,8 @@ curl "$ORBIT_URL/v1/queues" \
"queue":{
"id":"11111111111111111111111111111111",
"namespace_id":"33333333333333333333333333333333",
"name":"images","paused":false,"max_concurrency":8,"rate_per_second":100,"revision":"1"
"name":"images","paused":false,"max_concurrency":8,"rate_per_second":100,
"max_unfinished_tasks":100000,"unfinished_tasks":"0","revision":"1"
}
}
}
@@ -49,3 +51,97 @@ curl "$ORBIT_URL/v1/queues/$ORBIT_QUEUE_ID" -H "Authorization: Bearer $ORBIT_TOK
```
SDK:`Client.GetQueue(ctx, queueID)`。范围限制、重复创建和同名冲突已纳入 [P1 回归](../reports/phase-1.md)。这些配置上限不是生产容量承诺。
unfinished_tasks 为 uint64 的十进制字符串,表示当前 WAITING、LEASED、RUNNING、NEEDS_REVIEW 的总数。它是运行计数,变化不增加队列配置 revision。创建/配置回执保存操作时的快照,获取实时积压需要再次 GetQueue。
## POST /v1/queues/{queue_id}:configure
需要 queues:manage 及对应队列范围。完整替换控制参数,不改变名称、队列身份、已受理任务的冻结策略或历史结果。
| 字段 | 类型 | 要求 |
| --- | --- | --- |
| expected_revision | string | 必填,非零 uint64 的十进制字符串 |
| reason | string | 必填,非空白,最多 1024 个 Unicode 字符,写入审计 |
| paused | bool | 必须显式提供;省略/null 都拒绝 |
| max_concurrency | uint32 | 必填,1–100000 |
| rate_per_second | uint32 | 必填,1–1000000 |
| max_unfinished_tasks | uint32 | 必填,1–1000000;配置命令不使用零值默认 |
```sh
curl "$ORBIT_URL/v1/queues/$ORBIT_QUEUE_ID:configure" \
-H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: queue-control-001' \
--data '{"expected_revision":"1","reason":"limit background traffic","paused":false,"max_concurrency":2,"rate_per_second":1,"max_unfinished_tasks":100}'
```
成功 200 的 data 形状与创建一致,队列配置 revision 加一。配置、并发限制同步、审计、事件和幂等回执在一个事务提交。原键、原 expected_revision 和原内容重报返回原快照,deduplicated=true;同键不同内容为 409 IDEMPOTENCY_CONFLICT,新键使用旧版本为 409 STATE_CONFLICT。权限每次重新检查。
SDK:`Client.ConfigureQueue(ctx, queueID, key, orbit.ConfigureQueueRequest{...})` 返回 CreateQueueResult。先 GetQueue 读取版本,网络结果不明时保留原命令核对;只有确认版本冲突并重新评估后,才建立新的配置操作。
## 并发、限速与背压的区别
| 控制 | 实际行为 |
| --- | --- |
| paused=true | 阻止新的领取和 Start;继续受理任务、计划生成、续租、结果确认、取消及恢复 |
| max_concurrency | 限制队列有效执行许可,包括 LEASED 和 RUNNING;许可在结果/取消/恢复事务中释放 |
| rate_per_second | 每个数据库一秒计数窗口内最多提交的 Start 授权数;续租和原回执重报不计数 |
| max_unfinished_tasks | 限制新任务进入队列;等待、退避、预占、运行、待核查都占一个名额 |
速率窗口由第一次成功 Start 建立,保存到 MySQL;所有节点共享。窗口结束后的下一次 Start 建立新窗口。一次窗口内可突发至设定值,窗口交界附近可能出现接近两倍的短时突发;这不是“任意滑动一秒”的承诺,也不限制业务处理器内部发出请求的速率。开始授权成功不证明外部业务效果已经发生。
领取还预留尚未开始尝试所需的窗口空间,额度不足时保持 WAITING,不创建额外尝试。若领取后管理员降低速率,Start 可返回 WS RATE_LIMITED;不批准开始、不写回执、不增加尝试次数。自动 SDK 在同一租约内按原消息身份退避核对,底层客户端也必须遵守剩余租约,不能绕过开始授权执行。限流不延长总截止、开始截止或尝试预算。
降并发不撤销已有许可,超出新上限时等待在途工作收尾再领取;降速率保留当前窗口已用次数;降积压上限保留已有任务,禁止新受理直到低于新上限。调整参数、暂停/恢复、断开连接和节点重启都不能清零使用量。
任务第一次进入 SUCCEEDED、FAILED 或 CANCELLED 时,积压额度同事务减一;重试和待核查不释放、不重复增加。运行中取消只保存意图,仍占名额。迟到证据及重复确认不改变计数。人工核查解除入口仍待 P5,不能为了释放额度把未知结果改成成功/取消。
### HTTP 429 QUEUE_FULL
满额时新的 Submit 返回 429,`Retry-After: 1`,错误信封为:
```json
{"request_id":"44444444444444444444444444444444","error":{"code":"QUEUE_FULL","message":"queue unfinished task limit reached; retry later using the original identity"}}
```
该请求没有受理任务,新增占位幂等记录也回滚。已受理请求的同身份重报优先返回原回执,不被当前满额拒绝。释放空间后使用被拒请求的原键、原内容有限重试;建议至少等待 Retry-After 并加入抖动/总截止。若绝对 available_at/start_deadline_at 已过期,原请求会被参数规则拒绝,不能无限重试。
SDK 返回 `*orbit.APIError`,Code=QUEUE_FULL、StatusCode=429、RetryAfter=time.Second。RetryAfter 解析服务端整数秒建议,缺失/无效/超过一小时为零;SDK 不自动重报业务写请求。取消、查询、结果上报和管理配置不受受理满额限制。
计划触发也使用相同名额。部分补跑已生成的任务正常提交;满额的原触发点保留为下一游标,不创建假任务或跳过记录。下一批继续使用冻结的漏跑策略,因此等待期间超过补偿窗口的点仍可能按策略记录 SKIPPED。队列暂停仅控制执行,要停止计划生成应另外暂停计划。
## 完整 SDK 场景
源码:[resources/main.go](../../sdk/go/examples/resources/main.go)。先完成当前迁移并启动服务,初始化带 Worker 权限且 queues:manage 范围为全部队列的专用开发凭据:
```sh
mkdir -p .local
chmod 700 .local
go run ./cmd/orbit-admin bootstrap \
--application resources-demo --namespace default --environment development \
--queue bootstrap --worker-access --output .local/resources.credentials.json
cd sdk/go
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=../../.local/resources.credentials.json \
go run ./examples/resources
```
示例创建独立队列(积压 2、并发 2、每秒 1 次开始),暂停后提交两个任务;第三个被拒,重复第一个仍得到原回执;取消第一个释放空间,用第三个原身份重新提交,然后恢复并执行两个纯数据回显任务。
```text
backpressure=QUEUE_FULL http=429
duplicate=true rejected_request_retried=true
task=<第二个任务ID> state=SUCCEEDED attempts=1
task=<第三个任务ID> state=SUCCEEDED attempts=1
unfinished=0 rate_per_second=1 worker_drained=true
```
每次运行创建独立队列和任务,历史保留,不硬删除。无外部业务副作用;任务自带 20 秒总截止,失败非零退出并有界收尾。示例遇到未知网络结果会退出,不自动换键重建资源。
## 升级与未完成范围
000009 增加四个带中文说明的队列字段,回填所有既有未终结任务;旧配置版本及任务内容保持不变。先停止旧服务和写入,运行迁移,再启动新版。只读检查不能代替升级维护窗口;大表回填及混合版本升级待生产环境验证。
创建命令省略新字段时,幂等摘要保留旧字段集合。旧创建回执不含新字段,SDK 解码为零,只表示历史快照缺少信息,实时配置另行 GetQueue。配置接口是新能力;自动 SDK 已能处理 RATE_LIMITED,进行降额管理前应先升级 Worker。
本阶段验证见 [资源控制报告](../reports/phase-3-queue-resources.md)。仍需应用级跨命名空间配额、同业务键许可、可配置优先级及应用间公平性、可靠事件积压限制和容量验收;不能用单队列控制替代这些要求。

View File

@@ -56,6 +56,8 @@ Cron 段顺序为分钟、小时、月日、月、星期;支持 `*`、列表
所有任务策略在 task 内冻结,字段同 SubmitTaskRequest;payload 最大 256 KiB,数字文本保持精确。task 中禁止绝对 available_at / start_deadline_at,每次生成时立即可受理,task_timeout_ms 从本次实际创建时间计算。计划结束不取消已经生成的任务,也不缩短其任务时限。
生成任务受 [队列积压上限](queues.md) 约束。满额时保留未受理触发点,本批较早已生成部分仍提交;释放空间后按冻结的漏跑策略接续,超过补偿窗口的点仍按策略跳过。队列 paused 仅停止领取/开始,停止计划生成需使用计划暂停接口。
创建请求示例,ID 和未来时间替换为实际值:
```json

View File

@@ -180,6 +180,7 @@ SDK:`Client.CancelTask(ctx, taskID, key, request)`。此操作不承诺回滚
| 409 STATE_CONFLICT | 当前状态或版本不允许操作,先查询 |
| 413 BODY_TOO_LARGE | 减小请求或改为传递业务对象引用 |
| 415 UNSUPPORTED_MEDIA_TYPE | 使用 application/json |
| 429 QUEUE_FULL | 本次未受理,按 Retry-After 有限退避并保留原身份;已受理的原回执不受当前满额限制 |
| 503 UNAVAILABLE | 结果可能未知;保留原幂等身份,有限退避后核对 |
```json

View File

@@ -129,7 +129,9 @@ receipt 是原操作的不可变确认;lease 是本次重新核验的执行权
平台在同一事务中分配队列并发许可、创建尝试、递增任务执行代次和次数、保存 LEASED 与派发事件。领取本身消耗一次 max_attempts 预算,包括最终未获开始授权的尝试,预算不会因重连或过期恢复重置。
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。当前已实施队列 max_concurrency;rate_per_second、其他配额和调度策略在 P3 完成。
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。已实施队列 max_concurrency、rate_per_second 及未终结任务上限;参数含义、窗口突发、暂停及降额语义见 [队列资源控制](queues.md)。应用级配额、业务键许可与公平性仍在 P3 推进。
领取前核对速率窗口及已有 LEASED 预留,满额时不新建尝试。Start 在同一队列事务内消费开始预算,原 ACK 重报不重复计数。领取后若配置降额,Start 可能返回 RATE_LIMITED;保持原消息身份在剩余租约内退避核对。续租、结果确认、取消及恢复不消费新开始额度。
每个节点每 2 秒发起一批恢复扫描,每批最多 100 项: