Files
orbit/docs/api/priority-and-fairness.md

7.7 KiB
Raw Permalink Blame History

任务优先级与公平派发

状态:P3 已实现。优先级使用持久化任务字段,节点公平派发控制领取机会;任务执行权、应用配额与两类并发许可仍由 MySQL 事务决定。平台不增加缓存或消息队列服务。

1. 任务优先级

在 POST /v1/tasks 或 计划版本的 definition.task 中指定:

字段 类型 / 默认 含义
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。

{
  "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 领取池。

这是节点内领取机会的公平性。各节点分别轮转,没有宣称跨节点完全相等吞吐、全局权重或严格最大延迟。跨节点执行并发和开始额度仍由已实现的 应用配额 与 队列控制 原子保证。连接接入总上限仍为每节点 128;该机制不提供每应用连接预留,也不代替请求层限流或生产容量规划。

4. SDK 完整场景

源码:priority/main.go。创建专用队列,先按 0、9、4 的顺序提交三条任务,再启动容量为 1 的自动 Worker;三条任务在近同时到期的条件下按 9、4、0 执行,每条只有一次尝试。

从仓库根目录准备专用开发数据库,设置 ORBIT_MYSQL_DSN 和 ORBIT_HTTP_ADDR=127.0.0.1:8080 后:

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。另一个终端执行:

cd examples
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=/tmp/orbit-priority.credentials.json go run ./priority

预期输出:

submitted_priorities=0,9,4 executed_priorities=9,4,0
completed=3 attempts_per_task=1 worker_drained=true

示例返回输入对象及精确大整数,不访问外部业务资源。总任务截止为 20 秒,整个场景 25 秒;每次创建独立队列/处理器与任务,正常结束排空,历史保留。异常非零退出并有界收尾。这个短场景演示优先级加速,等待补偿的 271 秒边界由真实数据库夹具验证,不让读者实际等待几分钟。

调用片段(上下文、客户端和队列已初始化):

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 验收,当前参数不能作为百万租户容量承诺。完整记录见 本阶段报告。