feat: enforce shared application resource quotas across namespaces
This commit is contained in:
161
docs/api/application-resources.md
Normal file
161
docs/api/application-resources.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 应用级共享配额
|
||||
|
||||
状态:P3 已实现。应用下所有命名空间和队列共享未终结任务额度、执行并发和开始速率;与 [队列控制](queues.md) 同时生效。研发接入仍使用独立 Go SDK,额度不依赖内存、外部队列或缓存。
|
||||
|
||||
## 1. 作用域与权限
|
||||
|
||||
| 操作 | HTTP | 权限 |
|
||||
| --- | --- | --- |
|
||||
| 当前应用配置及用量 | `GET /v1/application/resources` | `application:resources:read` |
|
||||
| 完整替换三个上限 | `POST /v1/application/resources:configure` | `application:resources:manage` |
|
||||
|
||||
应用由 Bearer 凭据所属命名空间推导,不能通过 URL 或请求体指定其他应用。应用级动作独立授权,允许查看或管理该应用跨命名空间的聚合额度,不受 `queues` 列表缩小范围;普通 `queues:manage`、`workers:consume` 或 `queues:["*"]` 都不会自动获得应用级权限。通常只给运维管理凭据授予应用动作,业务 Worker 不需要这些权限。
|
||||
|
||||
初始化 CLI 显式传 `--resource-access` 才授予这两项权限。已有凭据不会自动扩权。`--application-id` 可在指定现有且启用的应用中创建新命名空间,与 `--application` 二选一;它仍要求数据库管理权限,不是公共 HTTP 开放的跨命名空间创建入口。
|
||||
|
||||
## 2. 查询配置及用量
|
||||
|
||||
```sh
|
||||
curl "$ORBIT_URL/v1/application/resources" \
|
||||
-H "Authorization: Bearer $ORBIT_TOKEN"
|
||||
```
|
||||
|
||||
不接受查询参数。成功 HTTP 200:
|
||||
|
||||
```json
|
||||
{
|
||||
"request_id": "11111111111111111111111111111111",
|
||||
"data": {
|
||||
"application_id": "22222222222222222222222222222222",
|
||||
"revision": "1",
|
||||
"max_unfinished_tasks": 1000000,
|
||||
"unfinished_tasks": "5",
|
||||
"max_concurrency": 10000,
|
||||
"active_attempts": "2",
|
||||
"pending_starts": "1",
|
||||
"rate_per_second": 100000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `unfinished_tasks`:WAITING、LEASED、RUNNING、NEEDS_REVIEW 的任务总数。待核查仍占用额度;自动重试不重复占用。
|
||||
- `active_attempts`:已预占或运行的尝试总数;未 Start 的尝试也占并发。每次权威结果或到期恢复只释放一次。
|
||||
- `pending_starts`:已预占但未开始的尝试,是活跃尝试的子集。跨窗口仍保留预占,到期恢复或取消才释放未使用预占。
|
||||
- `revision`:仅配置变更增加。用量变化不使配置版本失效;查询返回单次数据库读取快照,之后可能继续变化。
|
||||
- 版本和累计用量用十进制字符串,SDK 解为 `uint64`;三个配置上限为 JSON 整数。
|
||||
|
||||
默认值是开发基线,不代表实测容量。
|
||||
|
||||
## 3. 配置及幂等
|
||||
|
||||
请求头必须有 `Authorization`、`Content-Type: application/json` 和 `Idempotency-Key`。键为 1–128 个非空白可打印 ASCII 字符;每次不同管理操作使用不同键,未确认结果重报保留原键和原内容。
|
||||
|
||||
| 字段 | 类型 | 要求 |
|
||||
| --- | --- | --- |
|
||||
| expected_revision | 十进制字符串 | 必填,非零,匹配当前配置版本 |
|
||||
| reason | 字符串 | 必填,非空白,最多 1024 个 Unicode 字符,写入审计 |
|
||||
| max_unfinished_tasks | uint32 | 必填,1–100000000;默认 1000000 |
|
||||
| max_concurrency | uint32 | 必填,1–1000000;默认 10000 |
|
||||
| rate_per_second | uint32 | 必填,1–10000000;默认 100000 |
|
||||
|
||||
这是完整替换,缺项或零值不能解释为沿用旧值。未知字段、重复 JSON 键、错误大小写及 null 被拒绝。
|
||||
|
||||
```sh
|
||||
curl -X POST "$ORBIT_URL/v1/application/resources:configure" \
|
||||
-H "Authorization: Bearer $ORBIT_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Idempotency-Key: application-budget-change-001' \
|
||||
--data '{"expected_revision":"1","reason":"调整应用资源预算","max_unfinished_tasks":2000000,"max_concurrency":2000,"rate_per_second":5000}'
|
||||
```
|
||||
|
||||
成功 HTTP 200:
|
||||
|
||||
```json
|
||||
{
|
||||
"request_id": "33333333333333333333333333333333",
|
||||
"data": {
|
||||
"resources": {
|
||||
"application_id": "22222222222222222222222222222222",
|
||||
"revision": "2",
|
||||
"max_unfinished_tasks": 2000000,
|
||||
"unfinished_tasks": "5",
|
||||
"max_concurrency": 2000,
|
||||
"active_attempts": "2",
|
||||
"pending_starts": "1",
|
||||
"rate_per_second": 5000
|
||||
},
|
||||
"deduplicated": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置、版本、管理审计、事件和幂等回执在一个事务中提交。相同键与内容返回原成功快照,`deduplicated=true`,其中用量也保留原值;实时用量应重新 GET。审计/事件失败则整体回滚。同一应用下两个命名空间的幂等键空间相互独立,共享配置版本;并发修改以 CAS 判定,不能借另一个命名空间重放管理身份。
|
||||
|
||||
降额不删除任务、不撤销已取得的并发、不重置速率窗口,也不延长租约。用量可暂时高于新上限。新受理和新领取会停止,等待已有任务释放;已取得执行并发的尝试仍可在有效租约内开始,但 Start 必须满足当前开始速率。
|
||||
|
||||
## 4. 执行与背压
|
||||
|
||||
- 直接提交和定时计划生成任务同时检查应用与队列积压上限。两者都允许才占用空间;任一失败不能留下另一侧的计数、任务或幂等记录。
|
||||
- 原受理回执重报先核对身份,满额也能返回原回执。满额失败未受理,调用方退避后可用原身份再次提交;SDK 不自动重试写请求。
|
||||
- 每次 Claim 同时受应用并发、队列并发、Worker 容量和两级开始窗口余量约束。没有额度时不派发、不增加尝试次数。
|
||||
- Start 使用数据库时间的一秒固定窗口:第一次成功授权建立窗口;之后有效开始计数增加,重复回执和续租不计数。应用与队列窗口分别维护,边界可能出现短时突发,不承诺任意滑动一秒内都不超限,也不控制处理器内部的业务请求速率。
|
||||
- 尚未 Start 的预占也计入窗口可用空间,避免其他队列把额度再次领走。配置降速后,既有预占可能超过新速率;Start 返回 `RATE_LIMITED`,自动 Worker SDK 仅在有效租约内使用原身份退避核对,过期按正常恢复处理。
|
||||
- 运行中取消只保存意图,收到权威结果或到期恢复才释放并发;效果未知进入 NEEDS_REVIEW,继续占用未终结任务名额。应用停用后拒绝新用户操作,后台恢复仍可回收已到期尝试。
|
||||
- 计划生成时满额,已成功处理的批次部分提交,未受理的原触发点保留,下一轮按冻结的计划策略处理,不把满额伪造为漏跑跳过。窗口超过策略期限后仍可能按原漏跑策略跳过。
|
||||
|
||||
## 5. 错误与重报
|
||||
|
||||
| 错误 | HTTP / WS | 处理 |
|
||||
| --- | --- | --- |
|
||||
| INVALID_ARGUMENT | HTTP 400 | 校正字段,不盲目重报 |
|
||||
| UNAUTHENTICATED | HTTP 401 | 核对秘密、过期/撤销、应用及命名空间启停 |
|
||||
| PERMISSION_DENIED | HTTP 403 | 申请明确应用动作;队列管理不能代替 |
|
||||
| STATE_CONFLICT | HTTP 409 | 查询新配置,确认意图后以新版本及新键发起操作 |
|
||||
| IDEMPOTENCY_CONFLICT | HTTP 409 | 保留原操作身份,查明请求内容为何改变 |
|
||||
| APPLICATION_FULL | HTTP 429,`Retry-After: 1` | 当前应用满额,退避后保留原受理身份核对 |
|
||||
| QUEUE_FULL | HTTP 429,`Retry-After: 1` | 当前队列满额,处理同上 |
|
||||
| RATE_LIMITED | Worker 错误帧 | 应用或队列 Start 限流,保留消息身份及租约边界 |
|
||||
| UNAVAILABLE / 网络结果未知 | HTTP 503 / 连接错误 | 保留原键、版本和内容核对,不能假定写入失败 |
|
||||
|
||||
`APPLICATION_FULL` 来自任务受理接口,额度管理本身不会因满额而被禁止。两级积压同时满时优先报告应用满额。`Retry-After` 不是空间保证。
|
||||
|
||||
## 6. SDK 与可运行场景
|
||||
|
||||
SDK 方法:`GetApplicationResources(ctx)` 和 `ConfigureApplicationResources(ctx, key, request)`。完整双命名空间场景源码:[application-resources](../../sdk/go/examples/application-resources/main.go)。
|
||||
|
||||
在仓库根目录,先用已迁移数据库启动服务,并初始化一个独立、空闲的示例应用:
|
||||
|
||||
```sh
|
||||
mkdir -p .local
|
||||
chmod 700 .local
|
||||
go run ./cmd/orbit-admin bootstrap \
|
||||
--application application-resources-demo --namespace first --environment development \
|
||||
--queue first --resource-access --output .local/app-first.credentials.json
|
||||
ORBIT_EXAMPLE_APP_ID=$(sed -n 's/.*"application_id":"\([^"]*\)".*/\1/p' .local/app-first.credentials.json)
|
||||
go run ./cmd/orbit-admin bootstrap \
|
||||
--application-id "$ORBIT_EXAMPLE_APP_ID" --namespace second --environment development \
|
||||
--queue second --resource-access --output .local/app-second.credentials.json
|
||||
cd sdk/go
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/app-first.credentials.json \
|
||||
ORBIT_SECOND_CREDENTIALS_FILE=../../.local/app-second.credentials.json \
|
||||
go run ./examples/application-resources
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
cross_namespace_backpressure=APPLICATION_FULL http=429
|
||||
same_identity_retry_accepted=true
|
||||
unfinished=0 active=0 pending_starts=0
|
||||
```
|
||||
|
||||
示例将应用上限设为 1:命名空间 first 受理任务 → second 被全局满额拒绝 → first 取消释放 → second 以原键成功受理 → 取消收尾。正常或报错退出都会尝试按原配置版本恢复上限,遇到其他管理者修改则报冲突,不覆盖新配置。进程被强杀不能保证运行清理;未收尾任务会按总截止时间恢复,结果未知时仍应保留原身份查询。无业务 Worker 和外部副作用,资源及历史保留,不提供硬删除。
|
||||
|
||||
## 7. 存储、升级与边界
|
||||
|
||||
000010 新增 `orbit_application_resources`,应用标识使用既有 `BINARY(16)`,全部模型及字段提供中文注释,无外键。回填所有应用的未终结、活跃及预占数,空应用为零;历史任务、尝试、队列配置不改写。停止全部旧写入节点 → 执行迁移 → 启动新版;旧版不维护应用计数,不能混跑。大表回填时间和在线迁移仍需部署环境验证。
|
||||
|
||||
统一顺序为认证 → Worker 会话(需要时)→ 应用额度 → 队列 → 任务/尝试 → 许可。额度与身份分表,避免把认证共享锁升级为排他锁。恢复和调度对忙碌额度行使用 SKIP LOCKED,保留后续扫描;不采用全表计数参与每次领取。额度配置审计记录在发起凭据的命名空间,应用标识明确保存。
|
||||
|
||||
同一应用共享一条额度行,严格额度会使该应用的相关短事务串行化。当前范围是同一 MySQL 主库;未宣称跨库原子配额或百万租户性能,后续容量验证需测单个高流量应用的锁等待、租约续期和恢复延迟。业务键许可、公平派发、可靠订阅与完整生产容量/故障验收继续按开发计划推进。真实验证记录见 [应用配额回归](../reports/phase-3-application-resources.md)。
|
||||
Reference in New Issue
Block a user