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 的同一条纪律)。
为什么全是纯函数 / 确定性状态机 ¶
调度判据一旦不确定,编排就不可复现:同一批请求两次调度给出不同次序, 宿主没法解释「为什么它先跑」。故本包两条硬口径:
- **确定性**:平局必须有名字层裁决(不依赖 map 顺序、不依赖时钟);
- **裁决显式**:拒绝必须带机器可判的原因(Reason),不许「尽力而为」。
不做什么 ¶
不做队列本体、不做 goroutine 池、不做执行(那是宿主与上游的活); 不判可达性(dispatch 的活);不做动态权重调整(宿主按需重建 Picker ——权重变更走「换整份」而不是改其中一条,与 guardset.Assembly 同一条纪律)。
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidConfig = errors.New("scheduling: 配置非法")
ErrInvalidConfig 配置非法(权重非正、配额上限为负、背压阈值为负等)。
装配期就要红:调度参数写错(权重 0、负上限)会让判据在运行期给出 无意义的结论(全部拒绝或全部放行),静默运行比拒绝启动更贵。
var ErrOverRelease = errors.New("scheduling: 释放超过已占用")
ErrOverRelease 释放量超过已占用量:账不平。
释放超过占用说明调用方重复释放或释放了别人的额度——放行会把配额账本 变成负数,后续占用全部「看起来有余额」。拒绝并让调用方修调用点。
var ErrQuotaExhausted = errors.New("scheduling: 配额已用尽")
ErrQuotaExhausted 配额已用尽:本次占用被拒(可等释放后再试)。
这是**可重试**的拒绝——别人 Release 之后同一调用可能成功。把它当 永久失败处理会让动作永远丢失。
Functions ¶
Types ¶
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 ¶
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 拦下,不会被静默钳位。
type Priority ¶
type Priority int
Priority 优先级:数值越大越优先。
只定义相对次序,不定义刻度含义(「多少算高」是宿主的策略)。 缺省零值即最低档——未声明优先级的请求排在所有声明过的后面, 方向取「漏声明的拿到最弱的那档」,与 sandbox.Level 同一条口径。
type Quota ¶
type Quota struct {
// contains filtered or unexported fields
}
Quota 配额账本:同一资源的并发占用上限(判据面不做内部锁, 并发使用方自担同步——与 FairPicker 同一条边界)。
上限在构造期定死:运行期调额走「换整份」(重建一份并把 Used 带过去), 不提供「改上限」入口——运行中悄悄调大上限等于绕过已经做出的占用决策。
func (*Quota) Acquire ¶
Acquire 占用 n 个额度:余额不足整单拒绝(**不做部分占用**)。
部分占用会让调用方拿到「占了一半还失败了」的半截状态,回滚路径 立刻多一种写法;整单原子拒绝把失败收缩到「什么都没占」。 n <= 0 视为调用方错误(占用零个或负数个额度无从解释),按 ErrInvalidConfig 拒绝。
func (*Quota) Release ¶
Release 归还 n 个额度。释放超过已占用 ⇒ ErrOverRelease(账不平, 修调用点而不是钳位——钳位把重复释放这个 bug 静默吞掉)。