feat: add bounded task priority and fair application dispatch

This commit is contained in:
2026-09-17 18:45:54 +08:00
parent 958b4a8cea
commit 309ed75666
42 changed files with 1277 additions and 61 deletions

View File

@@ -14,6 +14,7 @@
| `/v1/queues` 创建、查询及 `:configure` 控制 | 已实现队列并发、开始速率、积压背压与暂停/恢复 | [队列接口与 SDK 场景](queues.md) |
| `/v1/application/resources` 查询及 `:configure` 管理 | P3 已实现跨命名空间应用配额 | [应用配额与 SDK 场景](application-resources.md) |
| 任务 `concurrency_key` 与尝试 `concurrency_token` | P3 已实现跨队列资源键互斥 | [资源键接口与 SDK 场景](keyed-concurrency.md) |
| 任务 `priority` 与节点领取池 | P3 已实现有界优先级加速、应用/队列轮转 | [优先级与公平派发](priority-and-fairness.md) |
| 凭据校验与初始化 CLI | P1 已实现 | [鉴权](authentication.md) |
| `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) |
| 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) |

View File

@@ -158,4 +158,4 @@ unfinished=0 active=0 pending_starts=0
统一顺序为认证 → Worker 会话(需要时)→ 应用额度 → 队列 → 任务/尝试 → 许可。额度与身份分表,避免把认证共享锁升级为排他锁。恢复和调度对忙碌额度行使用 SKIP LOCKED,保留后续扫描;不采用全表计数参与每次领取。额度配置审计记录在发起凭据的命名空间,应用标识明确保存。
同一应用共享一条额度行,严格额度会使该应用的相关短事务串行化。当前范围是同一 MySQL 主库;未宣称跨库原子配额或百万租户性能,后续容量验证需测单个高流量应用的锁等待、租约续期和恢复延迟。业务键许可、公平派发、可靠订阅与完整生产容量/故障验收继续按开发计划推进。真实验证记录见 [应用配额回归](../reports/phase-3-application-resources.md)。
同一应用共享一条额度行,严格额度会使该应用的相关短事务串行化。当前范围是同一 MySQL 主库;未宣称跨库原子配额或百万租户性能,后续容量验证需测单个高流量应用的锁等待、租约续期和恢复延迟。[资源键许可](keyed-concurrency.md) 和 [优先级/节点公平领取](priority-and-fairness.md) 已接入;可靠订阅与完整生产容量/故障验收继续按开发计划推进。真实验证记录见 [应用配额回归](../reports/phase-3-application-resources.md)。

View File

@@ -114,4 +114,4 @@ resource_writes=2 stale_write_rejected=true worker_drained=true
000011 为历史任务填空键/空摘要、历史尝试填零序号、历史会话填 false,不改变原 business_key。新增四个模型字段均有中文注释和 SQL COMMENT,实体数仍为 33;数据库最终外键数为零。
当前基于一个 MySQL 主库及应用额度锁串行化事务,资源键记录随不同键数量持续增长。尚未提供可调的每键并发大于 1、资源管理/清理接口、优先级公平性或生产容量结论。验证证据见 [本阶段报告](../reports/phase-3-keyed-concurrency.md)。
当前基于一个 MySQL 主库及应用额度锁串行化事务,资源键记录随不同键数量持续增长。尚未提供可调的每键并发大于 1、资源管理/清理接口、生产容量结论;[优先级与节点公平派发](priority-and-fairness.md) 已补齐。验证证据见 [本阶段报告](../reports/phase-3-keyed-concurrency.md)。

View File

@@ -0,0 +1,117 @@
# 任务优先级与公平派发
状态:P3 已实现。优先级使用持久化任务字段,节点公平派发控制领取机会;任务执行权、应用配额与两类并发许可仍由 MySQL 事务决定。平台不增加缓存或消息队列服务。
## 1. 任务优先级
在 [POST /v1/tasks](tasks.md) 或 [计划版本的 definition.task](schedules.md) 中指定:
| 字段 | 类型 / 默认 | 含义 |
| --- | --- | --- |
| priority | 可选 JSON 整数,默认 0,范围 0–9 | 受理时固定的任务优先级;每级最多取得 30 秒排序优势 |
负数、超过 9、浮点表示、字符串、bool 和 null 均返回 HTTP 400 / INVALID_ARGUMENT。Go SDK 对应 `Priority int16`。零值省略以保留旧请求摘要;任务详情、摘要和 Worker 派发在非零时返回 priority。查询到零或省略都表示普通优先级。
权限、请求头、提交结果、受理幂等、取消和截止规则沿用任务接口,没有单独的优先级管理权限或运行中改级接口。相同受理键改变 priority 返回 409 IDEMPOTENCY_CONFLICT。安全重试保持冻结优先级,下一次 available_at 改变时重新计算排序时间;计划触发继承对应不可变版本里的 priority。
```json
{
"queue_id":"11111111111111111111111111111111",
"handler":"object.prepare",
"handler_version":"v1",
"payload_version":"v1",
"payload":{"object_ref":"objects/example","operation_ref":"prepare-001"},
"priority":9
}
```
成功响应仍是任务接口的完整信封,首次 201、重复受理 200;任务对象包含 `"priority":9`。提交确认不代表已执行。
## 2. 排序与等待补偿
同一队列、与当前 Worker 版本和能力匹配且满足门控/租约/许可条件的到期任务,按下面三个键升序排列:
1. `dispatch_at = available_at - priority × 30 秒`。
2. available_at。
3. task_id(只用于确定性打破同时间相等,不表示业务顺序)。
dispatch_at 为数据库生成的只读字段,并有相应派发索引;它不对外提供执行授权。真正的最早执行时间始终是 available_at,派发仍检查截止、次数、门控、能力和资源键占用。
| 两条已就绪任务 | 先选择的任务 |
| --- | --- |
| A:0 级、5 秒前到期;B:9 级、刚到期 | B,获得 270 秒排序优势 |
| A:0 级、271 秒前到期;B:9 级、刚到期 | A,旧任务的等待已超过最大优势 |
| A:0 级、已到期;B:9 级、1 小时后才到期 | A,B 不具备领取资格 |
| 同一优先级、不同 available_at | 更早到期者 |
由此限制新高优先级工作插队的时间优势,避免纯粹按优先级排序造成持续饥饿。**270 秒不是最大排队时长**:旧积压、队列/应用额度、兼容 Worker、暂停、共享资源和每个任务执行时长仍影响等待。已被预占或正在运行的任务不被抢占;没有全局 FIFO 或跨队列优先级比较。
## 3. 节点内应用与队列轮转
Worker 连接不再各自直接并发发起一批数据库领取。节点采用有界领取池:
| 约束 | 当前开发基线 |
| --- | --- |
| 同时执行的领取 SQL 操作 | 每节点最多 4 个 |
| 一个应用同时在途的领取 | 每节点最多 1 个,其他应用可以继续领取 |
| 待处理和在途领取请求合计 | 每节点最多 128 个 |
| 每个连接未完成领取请求 | 最多 1 个 |
| 选择顺序 | 活跃应用轮转 → 该应用的队列轮转 → 队列内连接请求顺序 |
| 单个连接派发节奏 | 每个扫描周期最多 4 次领取,当前周期 1 秒 |
应用身份来自服务端验证过的命名空间归属,客户端不能通过增加命名空间或伪造应用字段分散公平组。一个应用增加队列或 Worker 数量,也不会在所有应用都持续有待处理请求时得到额外轮次。某个应用没有请求或已有在途领取时,其他应用继续使用空闲池容量。
同命名空间、队列、精确处理器能力集合及资源键支持声明的空领取结果,在本节点合并一个扫描周期,缓存最多 128 项。不同能力不共用空结果。缓存过期重新查询;任务新到达或容量释放可能多等待一个周期。这只抑制重复探测,不能据此授权任务或更改持久化状态。
领取在后台有界执行,WS 控制循环继续接收心跳、Start、续租、Finish 和排空。关闭节点时取消待领取操作并等待在途收尾;已提交但尚未发送的预占仍由数据库租约恢复。任务处理器运行在业务 Worker,完全不占这个 SQL 领取池。
这是**节点内领取机会的公平性**。各节点分别轮转,没有宣称跨节点完全相等吞吐、全局权重或严格最大延迟。跨节点执行并发和开始额度仍由已实现的 [应用配额](application-resources.md) 与 [队列控制](queues.md) 原子保证。连接接入总上限仍为每节点 128;该机制不提供每应用连接预留,也不代替请求层限流或生产容量规划。
## 4. SDK 完整场景
源码:[priority/main.go](../../sdk/go/examples/priority/main.go)。创建专用队列,先按 0、9、4 的顺序提交三条任务,再启动容量为 1 的自动 Worker;三条任务在近同时到期的条件下按 9、4、0 执行,每条只有一次尝试。
从仓库根目录准备专用开发数据库,设置 ORBIT_MYSQL_DSN 和 `ORBIT_HTTP_ADDR=127.0.0.1:8080` 后:
```sh
go run ./cmd/orbit-migrate up
go run ./cmd/orbit-admin bootstrap --application priority-example --namespace default \
--environment test --queue initial --worker-access --output /tmp/orbit-priority.credentials.json
go run ./cmd/orbit
```
凭据需要全部队列范围的 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-priority.credentials.json go run ./examples/priority
```
预期输出:
```text
submitted_priorities=0,9,4 executed_priorities=9,4,0
completed=3 attempts_per_task=1 worker_drained=true
```
示例返回输入对象及精确大整数,不访问外部业务资源。总任务截止为 20 秒,整个场景 25 秒;每次创建独立队列/处理器与任务,正常结束排空,历史保留。异常非零退出并有界收尾。这个短场景演示优先级加速,等待补偿的 271 秒边界由真实数据库夹具验证,不让读者实际等待几分钟。
调用片段(上下文、客户端和队列已初始化):
```go
accepted, err := api.SubmitTask(ctx, "prepare-operation-001", orbit.SubmitTaskRequest{
QueueID: queueID, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1",
Payload: json.RawMessage(`{"object_ref":"objects/example"}`), Priority: 9,
})
```
在处理器中通过 `e.Task.Priority` 读取冻结值;调度顺序由平台确定,业务 Worker 不应绕过 Start 或自行重排已授权任务。
## 5. 升级与验收边界
停止旧写入节点,应用 000012,再启新版服务。历史任务优先级为 0,生成排序时间等于原 available_at;既有任务、索引和历史保留,新增索引单独建立。优先级限制附 CHECK,generated 字段与 Go 模型均有中文说明,数据库最终仍无外键。
新版 SDK 开始提交非零 priority 前,先升级服务端;旧服务严格拒绝未知输入字段。公开 Worker Schema 已扩展可选 priority,其他语言的严格验证器需同步升级;默认 0 的派发报文保持原形状。
单节点公平领取及真实 MySQL/WSS 故障隔离已纳入回归。多节点不均匀连接分布、混合负载下各应用延迟、SQL 锁等待、海量未来任务扫描及峰值容量在 P6 验收,当前参数不能作为百万租户容量承诺。完整记录见 [本阶段报告](../reports/phase-3-priority-fairness.md)。

View File

@@ -144,4 +144,4 @@ unfinished=0 rate_per_second=1 worker_drained=true
创建命令省略新字段时,幂等摘要保留旧字段集合。旧创建回执不含新字段,SDK 解码为零,只表示历史快照缺少信息,实时配置另行 GetQueue。配置接口是新能力;自动 SDK 已能处理 RATE_LIMITED,进行降额管理前应先升级 Worker。
本阶段验证见 [资源控制报告](../reports/phase-3-queue-resources.md)。[应用级跨命名空间配额](application-resources.md) 已实现并同时生效。仍需同业务键许可、可配置优先级及应用间公平性、可靠事件积压限制和容量验收;不能用单队列控制替代这些要求。
本阶段验证见 [资源控制报告](../reports/phase-3-queue-resources.md)。[应用级跨命名空间配额](application-resources.md) 已实现并同时生效。[资源键许可](keyed-concurrency.md) 与 [优先级/节点公平派发](priority-and-fairness.md) 已实现;可靠事件积压限制及跨节点公平负载和容量验收继续推进。

View File

@@ -25,6 +25,7 @@ P2 进展:服务端执行内核、WSS 与独立自动 Worker SDK 已可产生
| payload | JSON | 必填 | 非 null JSON,最大 256 KiB;平台不解释业务字段 |
| business_key | string | 可选 | 最多 191 个 Unicode 字符,无 NUL/换行;用于关联,不提供业务幂等 |
| concurrency_key | string | 可选,空为关闭 | 1–191 个 Unicode 字符,无控制字符和首尾空白;显式资源互斥键,同命名空间跨队列共享,见 [资源键契约](keyed-concurrency.md) |
| priority | 整数 | 可选,默认 0 | 0–9,冻结优先级,每级30秒排序优势;不提前延时任务,见 [优先级与公平派发](priority-and-fairness.md) |
| available_at | time / null | 受理时数据库时间 | 最早可执行时间;必须早于总截止时间,仅允许最多 1 秒的过去时间容差 |
| start_deadline_at | time / null | 无独立限制 | 必须晚于当前时间,不早于 available_at,不晚于总截止时间 |
| task_timeout_ms | uint64 | 0 或省略为 86400000 | 从受理时计算的总时限,最大 30 天(2592000000 毫秒) |

View File

@@ -35,6 +35,7 @@ ACK 的 body 根据原请求选择:worker.capacity → welcome,worker.resume
- 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 断言以校验实际日历日期。
- task.assign.task.priority 为可选 0–9 整数,零值省略;平台在实际到期后结合等待时间排序,详见 [优先级与公平派发](priority-and-fairness.md)。
P2 收紧了开发期此前允许的省略控制字段、非规范计数字符串和数组内字段别名。当前 SDK 一直发送完整规范字段,不需迁移;其他自研客户端应按本文件更新。SDK 尚未发布首个版本。将来扩展消息类型或改变字段语义必须同时更新 Schema、服务端、SDK 与兼容回归。

View File

@@ -130,7 +130,7 @@ receipt 是原操作的不可变确认;lease 是本次重新核验的执行权
平台在同一事务中预占应用并发与开始额度、分配队列并发许可、创建尝试、递增任务执行代次和次数、保存 LEASED 与派发事件。领取本身消耗一次 max_attempts 预算,包括最终未获开始授权的尝试,预算不会因重连或过期恢复重置。
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。已实施队列 max_concurrency、rate_per_second 及未终结任务上限;参数含义、窗口突发、暂停及降额语义见 [队列资源控制](queues.md)。[应用级配额](application-resources.md) 与 [显式资源键许可](keyed-concurrency.md) 同时生效;公平性继续推进。
运行中的进程总容量跨其所有旧/新会话统计。声明容量减少、断开连接或重复结果,都不能重复释放额度。已实施队列 max_concurrency、rate_per_second 及未终结任务上限;参数含义、窗口突发、暂停及降额语义见 [队列资源控制](queues.md)。[应用级配额](application-resources.md) 与 [显式资源键许可](keyed-concurrency.md) 同时生效;[任务优先级和节点公平领取](priority-and-fairness.md) 已接入。
同命名空间的 concurrency_key 跨队列互斥,领取事务同时保存键许可和 concurrency_token;原 business_key 仅追踪。没有声明 concurrency_keys 的 Worker 不领取带键任务。资源端需用单调序号和操作幂等保护实际写入,平台无法撤回外部已发出的请求;未知结果释放执行许可并保留核查状态,详细边界和升级顺序见资源键契约。
@@ -147,8 +147,8 @@ receipt 是原操作的不可变确认;lease 是本次重新核验的执行权
## 6. 连接保护与当前边界
当前节点上限 128 条 Worker 连接;每连接入站队列 4 帧、出站队列 8 帧,派发保留 ACK 缓冲空间,每秒最多发起 4 次新派发尝试。读空闲期限 60 秒、写期限 5 秒、单个数据库控制操作期限 8 秒。慢消费者导致连接关闭时,已提交状态仍由稳定身份恢复。
当前节点上限 128 条 Worker 连接;每连接入站队列 4 帧、出站队列 8 帧,派发保留 ACK 缓冲空间,每个扫描周期最多发起 4 次新派发尝试;节点总领取池并发 4、待处理与在途总数 128、每应用在途领取最多 1,按应用及队列轮转,同能力空探测合并一个周期。读空闲期限 60 秒、写期限 5 秒、单个数据库控制操作期限 8 秒。慢消费者导致连接关闭时,已提交状态仍由稳定身份恢复。
这些是有界的开发基线,不代表经过容量压测的生产参数。当前同队列操作使用较粗的行锁协调,吞吐与公平性需要 P6 按负载验证。服务退出会显式关闭被 HTTP Upgrade 接管的连接;断开不会直接撤销外部已发出的请求。
这些是有界的开发基线,不代表经过容量压测的生产参数。当前同队列操作使用较粗的行锁协调,节点内公平领取已实现;吞吐、多节点连接分布和应用延迟隔离需要 P6 按负载验证。服务退出会显式关闭被 HTTP Upgrade 接管的连接;断开不会直接撤销外部已发出的请求。
协议 Schema 和真实进程强杀恢复已通过回归,见 [P2 阶段验收](../reports/phase-2.md)。[运行中取消](cancellation.md) 已实现并通过回归;凭据轮换、数据库故障切换、跨主机演练与容量验收属于后续生产验证。自动运行器对非协作处理器提供有界退出与状态报告,不能强制停止其外部副作用。