scheduling

package
v0.35.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

backpressure.go —— 背压的裁决判据:满了就明说,不悄悄排队到失控。

背压判据的本体:**裁决必须带机器可判的原因**。「拒绝」如果只有一个 布尔,调用方分不清「队列满」(稍后重试有意义)与「在途满」(加容量 才有意义)——两种拒绝的处置完全不同,压平它们就是把信号埋掉。

fair.go —— 加权公平份额的确定性选择器。

公平判据的本体:**按「每权重份已得服务」最欠的那个先被服务**—— 每个参与者按自己的权重应得 served_i ∝ weight_i 的份额; 比值 served_i/weight_i 最小者就是当前最欠份额的人。平局按名字字典序, 全程不依赖 map 遍历顺序与时钟——两次重建同一状态得到同一选择。

priority.go —— 优先级排序的确定性判据。

优先级本身只是一维数字;判据的本体是**排序的确定性**: 同优先级内必须有名字层平局裁决,且不依赖输入的排列顺序。 输入顺序通常来自配置文件书写次序或事件到达次序——一次无关的重排 就换掉执行次序,宿主无从解释「为什么它先跑」。

quota.go —— 资源配额的账本判据。

配额判据的本体是**账平**:占用与释放必须守恒,任何一侧超界都要显式拒绝。 账不平的形态是静默的——重复释放让余额虚增,后续占用全部「看起来有余量」, 而真实资源早就被占满了。

Package scheduling Agent 调度的**判据面**:优先级、公平份额、资源配额、背压。

生命周期: Experimental(v0.35.0 定级)——本包仍会改、删、拆,不承诺任何 兼容,升级风险自负;定级理由见 api-lifecycle.yaml,分级规则见 docs/api-compat.md。

归属结论(ADR-0008,结论写证据不写感觉)

本仓的 @dispatch@ 是上游 @oagentkit/agent/dispatch@ 的**门面**,它只判 三件事:allow 矩阵可达性、派发深度上限、自派发拒绝(证据:上游 dispatch.go 的全部判定面就是 Guard.Assert 的三条分支与 DenyError.Reason 的三个常量,见 docs/adr/ADR-0008-调度判据归属.md 的「证据」节)。 上游**没有**优先级、公平性、配额、背压——这四样是上游确实不提供的部分, 按问题域落在本包:本包只给**判据**(纯函数面),不实现队列、不做派发执行。

与 dispatch 的分工(本仓不出现与上游语义重复的实现)

  • dispatch(门面):这次派发**能不能**发生——可达性 + 深度 + 自派发;
  • scheduling(本包):派发请求**按什么次序、谁多谁少、满了怎么办**—— 优先级排序、加权公平份额、配额账本、背压裁决。

本包不 import dispatch,也不重复实现可达性/深度判定:两组语义正交, 宿主把它们**串起来**用(先过 dispatch 的可达性,再进 scheduling 的排序 与配额)。哪天上游把四样里的某样收编了,对应的判据应当转门面而不是 并存第二份(AGENTS.md 的同一条纪律)。

为什么全是纯函数 / 确定性状态机

调度判据一旦不确定,编排就不可复现:同一批请求两次调度给出不同次序, 宿主没法解释「为什么它先跑」。故本包两条硬口径:

  1. **确定性**:平局必须有名字层裁决(不依赖 map 顺序、不依赖时钟);
  2. **裁决显式**:拒绝必须带机器可判的原因(Reason),不许「尽力而为」。

不做什么

不做队列本体、不做 goroutine 池、不做执行(那是宿主与上游的活); 不判可达性(dispatch 的活);不做动态权重调整(宿主按需重建 Picker ——权重变更走「换整份」而不是改其中一条,与 guardset.Assembly 同一条纪律)。

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidConfig = errors.New("scheduling: 配置非法")

ErrInvalidConfig 配置非法(权重非正、配额上限为负、背压阈值为负等)。

装配期就要红:调度参数写错(权重 0、负上限)会让判据在运行期给出 无意义的结论(全部拒绝或全部放行),静默运行比拒绝启动更贵。

View Source
var ErrOverRelease = errors.New("scheduling: 释放超过已占用")

ErrOverRelease 释放量超过已占用量:账不平。

释放超过占用说明调用方重复释放或释放了别人的额度——放行会把配额账本 变成负数,后续占用全部「看起来有余额」。拒绝并让调用方修调用点。

View Source
var ErrQuotaExhausted = errors.New("scheduling: 配额已用尽")

ErrQuotaExhausted 配额已用尽:本次占用被拒(可等释放后再试)。

这是**可重试**的拒绝——别人 Release 之后同一调用可能成功。把它当 永久失败处理会让动作永远丢失。

Functions

func Order

func Order[T any](items []T, keyFunc func(T) string, prioFunc func(T) Priority) []T

Order 把 items 按 (优先级降序, key 升序) 排出确定性次序,返回新切片 (不改输入——排序是判据,不是对调用方数据的原地改写)。

keyFunc 必须给出稳定的标识(同优先级内的平局裁决依据)。两个条目 key 相同则次序由稳定排序保留输入次序——但那是宿主的错误(同一 key 两次出现,标识已经不唯一),本函数不替它纠偏。

prioFunc 为 nil 视为全部 Priority(0)——只有一组时次序由 key 决定。

func TopK

func TopK[T any](items []T, keyFunc func(T) string, prioFunc func(T) Priority, n int) []T

TopK 从 items 里按 Order 的同一口径取前 n 个(确定性)。

n<=0 返回空——「要 0 个」是合法请求,不是错误;负值同样按 0 处理 (调用方算错了配额,不该由排序判据替它 panic)。

Types

type Decision

type Decision struct {
	Admit  bool
	Reason Reason
}

Decision 背压裁决:准入与否 + 拒绝原因(准入时 Reason 为空)。

type FairPicker

type FairPicker struct {
	// contains filtered or unexported fields
}

FairPicker 加权公平选择器:Pick 返回当前最「欠份额」的参与者。

零值不可用(构造期拒绝空集与非正权重);构造后**只读权重、只增计数**, 并发使用方自担同步(判据面不做内部锁——它通常被宿主的调度循环单线程消费; 要并发就把它包进自己的锁里,判据不隐藏同步成本)。

权重变更不走「改其中一条」:用新权重表重建一份 Picker(换整份纪律), 旧的 served 计数想保留就经 Served() 带进 NewFairPickerWithServed。

func NewFairPicker

func NewFairPicker(weights map[string]int) (*FairPicker, error)

NewFairPicker 按权重表构造选择器。权重必须全为正;空集拒绝。

权重为 0 或负的参与者会让「应得份额」失去意义(0 权重者永远不该被选, 负权重无从解释)——装配期就拒,不静默钳位。

func NewFairPickerWithServed

func NewFairPickerWithServed(weights map[string]int, served map[string]uint64) (*FairPicker, error)

NewFairPickerWithServed 在 NewFairPicker 的基础上带入既有服务计数 (权重变更换整份时保留历史公平性)。

served 里的未知键忽略(参与者已撤出),已登记但缺计数的按 0 起。

func (*FairPicker) MaxDebt

func (p *FairPicker) MaxDebt() float64

MaxDebt 公平性的观察量(诊断面,不参与判定):应得比例的最大落差 max_i(served_i/w_i) − min_i(served_i/w_i)。0 = 完全成比例; 越大说明有人按权重份算落后得越多。命名直说它是债的幅度: 宿主拿它做告警阈值时,刻度是「每权重份的服务次数差」。

func (*FairPicker) Pick

func (p *FairPicker) Pick() string

Pick 返回当前最欠份额的参与者(不推进计数——是否算一次服务由调用方 Record 决定,选中后不消费是合法的:调用方可能对它探活后再放弃)。

口径:served_i / weight_i 最小者(每权重份欠得最多);平局按名字字典序。

func (*FairPicker) Record

func (p *FairPicker) Record(key string)

Record 记一次对 key 的服务(推进它的已服务计数)。

未登记的 key 忽略——选择器的参与者集在构造期定死,运行期不收新成员 (要扩员就重建一份,与「换整份」同一条纪律)。

func (*FairPicker) Served

func (p *FairPicker) Served() map[string]uint64

Served 返回一份服务计数的快照(权重变更换整份时带走历史)。

type Policy

type Policy struct {
	// MaxQueue 等待队列深度上限(<=0 = 不设限)。
	MaxQueue int
	// MaxInFlight 在途执行数上限(<=0 = 不设限)。
	MaxInFlight int
}

Policy 背压阈值。零值语义刻意取「不设限」——背压是宿主**显式选择** 的保护机制,缺省关着;要保护就写明数字,与 netguard.Allow 的 「零值即严」方向相反但同一立场:缺省侧取「不改变既有行为」的那一侧, 写错的数字在装配期被 Validate 拦下,不会被静默钳位。

func (Policy) Decide

func (p Policy) Decide(queueDepth, inFlight int) Decision

Decide 背压裁决(纯函数):queueDepth 是**新请求入队前**的等待数, inFlight 是当前在途执行数。

判定次序:**先判在途、再判队列**。在途满是容量问题(重试无用), 队列满是拥塞问题(退避重试有用)——先报容量这一条,宿主的处置 才指向正确的修法(与 gatecheck「需人审优先于档位不足」同一思路: 先报哪条决定宿主去找谁)。

两个都满时报 in_flight_full:容量约束是最硬的那条,队列只是它的结果。

func (Policy) Validate

func (p Policy) Validate() error

Validate 装配期自检:负值即红(阈值语义只有「不设限(<=0)」与「上限(>0)」 两档,负数是笔误不是语义)。

type Priority

type Priority int

Priority 优先级:数值越大越优先。

只定义相对次序,不定义刻度含义(「多少算高」是宿主的策略)。 缺省零值即最低档——未声明优先级的请求排在所有声明过的后面, 方向取「漏声明的拿到最弱的那档」,与 sandbox.Level 同一条口径。

type Quota

type Quota struct {
	// contains filtered or unexported fields
}

Quota 配额账本:同一资源的并发占用上限(判据面不做内部锁, 并发使用方自担同步——与 FairPicker 同一条边界)。

上限在构造期定死:运行期调额走「换整份」(重建一份并把 Used 带过去), 不提供「改上限」入口——运行中悄悄调大上限等于绕过已经做出的占用决策。

func NewQuota

func NewQuota(limit int64) (*Quota, error)

NewQuota 构造配额账本。limit 为负拒绝(「负上限」无从解释; limit = 0 合法:完全不许占用——显式写 0 与漏配是两回事)。

func (*Quota) Acquire

func (q *Quota) Acquire(n int64) error

Acquire 占用 n 个额度:余额不足整单拒绝(**不做部分占用**)。

部分占用会让调用方拿到「占了一半还失败了」的半截状态,回滚路径 立刻多一种写法;整单原子拒绝把失败收缩到「什么都没占」。 n <= 0 视为调用方错误(占用零个或负数个额度无从解释),按 ErrInvalidConfig 拒绝。

func (*Quota) Limit

func (q *Quota) Limit() int64

Limit 上限(换整份重建时核对用)。

func (*Quota) Release

func (q *Quota) Release(n int64) error

Release 归还 n 个额度。释放超过已占用 ⇒ ErrOverRelease(账不平, 修调用点而不是钳位——钳位把重复释放这个 bug 静默吞掉)。

func (*Quota) Remaining

func (q *Quota) Remaining() int64

Remaining 当前余额(limit − used;limit=0 时恒为 0)。

func (*Quota) Used

func (q *Quota) Used() int64

Used 当前已占用。

type Reason

type Reason string

Reason 背压拒绝的原因(词表封闭:宿主按常量比对,不按字符串猜)。

const (
	// ReasonQueueFull 等待队列已满:暂不接受新请求(稍后重试有意义)。
	ReasonQueueFull Reason = "queue_full"
	// ReasonInFlightFull 在途执行已达上限:暂不接受新请求
	// (重试同样会被拒——要消化积压得先让在途收敛或扩容量)。
	ReasonInFlightFull Reason = "in_flight_full"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL