Documentation
¶
Overview ¶
audit.go —— 介入留痕契约与进程内参考实现。
留痕是介入的硬前提,不是事后增强:没有留痕的介入无法回答「谁在什么时候 依据什么改了运行状态」,出问题时与「存储被绕过直接改」无法区分。 故契约层面 Auditor 缺位 ⇒ 介入一律拒绝(ErrNoAuditor),实现层面 MemAuditor 对不完整的留痕拒收——两层各挡一个方向。
Package controlplane 控制平面的**最小契约 + 进程内参考实现 + HTTP 适配面**。
生命周期: Experimental(v0.35.0 定级)——本包仍会改、删、拆,不承诺任何 兼容,升级风险自负;定级理由见 api-lifecycle.yaml,分级规则见 docs/api-compat.md。
它补的是哪一段(issue #115) ¶
本仓此前最接近控制面的是 taskboard.Check——判一份快照的不变量,宿主随时 可跑。但它不回答「现在跑到哪了」(运行状态查询),更不提供任何**人工介入** 动作:强制释放、裁决未知结果、终止运行。缺这一面的代价是:线上出问题时 宿主只能自己拼命令去查、自己去改存储——这些动作**绕过了运行时的判据**, 改完可能与运行中的持有者打架。
契约给什么 ¶
- **查询**:任务 / 运行的状态视图 + 不变量视图(taskboard.Check 的 投影,TaskView / RunView);
- **介入**:强制释放(ForceRelease)、裁决未知结果(Adjudicate)、 终止运行(Terminate);
- **权限边界**:介入动作必须走**与运行时同一套判据**,不绕闸—— 强制释放是「经闸换持有者」(旧令牌当场作废,迟到提交凭 taskboard.ErrLeaseLost 被拒;下一次认领铸造的代次严格大于被释放的 那一代,下游栅栏据此拒收被释放者的迟到写入);裁决走 sideeffect 的 对账语义(凭下游事实不凭持有权,先到者为准);终止凭**运行代次** (ExpectedGen 不匹配即 ErrStaleRun——「改完与运行中的持有者打架」 正是这条要拦的形态)。租户上下文经 tenant.Require fail-closed: 介入是写路径,必须可归责到租户与主体(issue #114 判据面的落点);
- **留痕**:每次介入必须产生一条 AuditEntry(谁、什么时候、依据什么 结论),留痕面缺位 ⇒ 介入一律拒绝(ErrNoAuditor)——没有留痕的 介入无法回答「谁在什么时候依据什么改了运行状态」。
边界 ¶
控制平面是**接口面**,不是 UI;具体界面归宿主。本包给三样:
- Plane 契约(宿主包着自己的 taskboard.Board / sideeffect Runner / 运行状态存储实现它);
- 进程内参考实现(MemPlane + MemAuditor——判据怎么落的演示体, 也是测试与演练的最小运行时面);
- net/http JSON 适配面(httpapi.go——鉴权钩子由宿主注入,内核不鉴权)。
httpapi.go —— 控制面的 net/http JSON 适配面。
鉴权边界:内核不鉴权 ¶
scope 词表、租户体系、会话形态全是宿主资产。Handler 持有 Authorizer 钩子 (宿主实现):它从请求解析主体与租户、判定该动作是否可执行,返回的 tenant.Context 被注入 request context 传给 Plane——Plane 侧的 tenant.Require(介入写路径的 fail-closed 判据)据此有据可判。 这是「HTTP 面 ↔ 机制面」的唯一接缝:宿主在钩子里做完自己的判定, 机制面拿到的已经是判定过的上下文。
钩子缺位 ⇒ 全部请求 401(fail-closed):控制面不开无鉴权的口子。
路由(Go 1.22+ 模式路由) ¶
GET /tasks/{id} → TaskView
GET /runs/{id} → RunView
POST /tasks/{id}/force-release body: ForceRelease
POST /effects/{key}/adjudicate body: Adjudication
POST /runs/{id}/terminate body: Termination
路径参数是介入目标的**唯一来源**:请求体里同名字段被忽略——同一个目标 不允许两条来源(一个真相)。
memplane.go —— 控制面的进程内参考实现。
MemPlane 同时是两面 ¶
- **控制面**:实现 Plane 契约(查询 + 三类介入),演示介入判据怎么落在 「与运行时同一份状态」上——留痕与状态变更在同一个临界区里完成, 留痕失败即整体失败(没有「先改后补痕迹」的窗口);
- **最小运行时面**:Claim / Complete / Release / Seed* 驱动场景—— 控制面的介入判据必须对着运行时的租约与代次演示(强释之后旧令牌 提交被拒、下一次认领代次严格递增),没有运行时面就没有可演示的 「同一套判据」。
参考实现用单调计数器铸令牌(可读、可断言),**不承诺随机性**:宿主 实现存储时按 taskboard 的契约自行铸造(不可预测性是宿主的安全需求, 参考实现只演示判据)。进程内存储重启即失效——与 ticket 同一条理由: 控制面参考实现的价值在判据演示,不在持久化。
runtime.go —— MemPlane 的**最小运行时面**(建场景与认领/提交/归还)。
控制面的介入判据必须对着运行时的租约与代次演示(强释之后旧令牌提交被拒、 下一次认领代次严格递增),没有运行时面就没有可演示的「同一套判据」。 这些方法不是 Plane 契约的一部分——它们是「运行时那一侧」的参考实现: 宿主的真实运行时是自己的 taskboard.Board / sideeffect Runner / 状态存储。
Seed* 先整单校验再落账:部分落账的 run 是半成品状态,视图会把 「建了一半」当成「建成了」。
Index ¶
- Variables
- type Action
- type Adjudication
- type AuditEntry
- type Auditor
- type Authorizer
- type ForceRelease
- type Handler
- type MemAuditor
- type MemPlane
- func (m *MemPlane) Adjudicate(ctx context.Context, req Adjudication) error
- func (m *MemPlane) Claim(_ context.Context, id taskboard.TaskID, agent taskboard.AgentID, ...) (taskboard.Task, error)
- func (m *MemPlane) Complete(_ context.Context, id taskboard.TaskID, token string) error
- func (m *MemPlane) ForceRelease(ctx context.Context, req ForceRelease) error
- func (m *MemPlane) Release(_ context.Context, id taskboard.TaskID, token string) error
- func (m *MemPlane) RunView(_ context.Context, id RunID) (RunView, error)
- func (m *MemPlane) SeedEffect(key string, unsafe bool) error
- func (m *MemPlane) SeedRun(id RunID, status string, tasks []taskboard.Task) error
- func (m *MemPlane) TaskView(_ context.Context, id taskboard.TaskID) (TaskView, error)
- func (m *MemPlane) Terminate(ctx context.Context, req Termination) error
- type Plane
- type RunID
- type RunView
- type TaskView
- type Termination
- type Verdict
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotFound 查询目标不存在(任务 / 运行)。 ErrNotFound = errors.New("controlplane: 目标不存在") // ErrInvalidRequest 介入请求不完整(Operator / Basis / 结论词表不合格)。 // 无归责不介入。 ErrInvalidRequest = errors.New("controlplane: 介入请求不完整——无归责不介入") // ErrNoAuditor 留痕面未装配。留痕是介入的硬前提:没有留痕的介入 // 无法回答「谁在什么时候依据什么改了运行状态」,一律拒绝(fail-closed)。 ErrNoAuditor = errors.New("controlplane: 留痕面未装配,拒绝介入") // ErrStaleRun 运行代次已前进:介入基于过期视图(Terminate 的乐观并发闸)。 ErrStaleRun = errors.New("controlplane: 运行代次已前进,介入基于过期视图") // ErrTerminalRun 运行已是终态:无需也无法终止(介入重试撞上它说明 // 第一次介入已生效)。 ErrTerminalRun = errors.New("controlplane: 运行已是终态,无需终止") )
错误哨兵(宿主判定走 errors.Is,不要各造一套)。
var ErrAlreadyExists = errors.New("controlplane: 目标已存在")
ErrAlreadyExists 目标已存在(Seed* 的重复登记;介入类动作不会报它—— 介入对已存在目标的语义是执行,不是登记)。
var ErrInvalidAudit = errors.New("controlplane: 留痕不完整——谁/动作/目标/依据缺一不可")
ErrInvalidAudit 留痕不完整(谁 / 动作 / 目标 / 依据缺一不可)。 MemAuditor 对不完整的留痕拒收;宿主实现应复用同一判据。
Functions ¶
This section is empty.
Types ¶
type Adjudication ¶
type Adjudication struct {
// EffectKey 副作用幂等键(sideeffect.Action.Key;宿主映射)。
EffectKey string
// Verdict 裁决结论(词表见 Verdict)。
Verdict Verdict
// ForceRetryable 与 sideeffect.Resolution.ForceRetryable 同口径:
// 不可重试(Unsafe)的动作凭「未生效」的裁决不自动开门——重新打开
// 施加的门必须是显式决策,不能是「对账流程跑完了」的自然结果。
ForceRetryable bool
// Operator 谁在介入(留痕必填)。
Operator string
// Basis 依据什么结论(留痕必填:通常是「查了下游的什么、看到什么」)。
Basis string
}
Adjudication 裁决未知结果(sideeffect.Reconcile 同口径)。
契约:裁决凭下游事实,不凭持有权 ¶
裁决**不校验栅栏 / 令牌**:它的依据是宿主查到的下游事实——原持有者可能 已经死亡、租约早已过期,而对账恰恰要在这种情况下进行(sideeffect 同一条契约)。同一 Key 的多次裁决**先到者为准**:结论已定后再来裁决 → sideeffect.ErrNotUncertain(不覆盖先到的结论)。
type AuditEntry ¶
type AuditEntry struct {
// At 留痕时刻。由**留痕实现侧**补(实现收到零值时取自己的当前时刻)——
// 介入方的时钟不可信,留痕时刻以实现侧为准。
At time.Time
// Operator 谁在介入(必填)。
Operator string
// Action 动作种类(必填,词表见 Action* 常量)。
Action Action
// Target 目标(任务 id / 效果键 / run id,必填)。
Target string
// Basis 依据什么结论(必填)。
Basis string
// Detail 结果摘要(可空:成功 / 被拒原因)。
Detail string
}
AuditEntry 一条人工介入留痕:谁、什么时候、做了什么、对谁、依据什么、结果如何。
type Auditor ¶
type Auditor interface {
// Append 追加一条介入留痕。
Append(ctx context.Context, e AuditEntry) error
// Entries 按目标取留痕(查询面)。target 为空取全部;按追加序返回。
Entries(ctx context.Context, target string) ([]AuditEntry, error)
}
Auditor 留痕契约:**只增不改**(append-only)。
实现方必须保证:
- Append 成功后条目永不修改、永不删除(留痕可被追加,不可被改写—— 可改写的留痕不是留痕);
- 收到零值 At 时补实现侧当前时刻;
- 对缺 Operator / Action / Target / Basis 的条目拒绝(ErrInvalidAudit), 不静默补空。
持久化形态归宿主(表 / 文件 / 只读副本皆可);MemAuditor 是进程内参考实现。
type Authorizer ¶
type Authorizer interface {
Authorize(r *http.Request, act Action, target string) (tenant.Context, error)
}
Authorizer 鉴权钩子(宿主实现):解析请求 → 判定动作是否可执行 → 返回注入 request context 的租户/权限上下文。返回错误 ⇒ 403,不触达 Plane。
type ForceRelease ¶
type ForceRelease struct {
// TaskID 要释放的任务。
TaskID taskboard.TaskID
// Operator 谁在介入(留痕必填)。
Operator string
// Basis 依据什么结论(留痕必填:证据 / 判据 / 单号,宿主词表)。
Basis string
}
ForceRelease 强制释放任务认领。
契约:不是绕过闸,是经闸换持有者 ¶
实现必须按「接管」处理:被释放任务的旧令牌**当场作废**,任务回到可认领 态。于是旧持有者(崩溃的进程、失联的 worker)迟到的 Complete / Renew 一切动作都凭 taskboard.ErrLeaseLost 被拒;下一次认领铸造的接管代次 (taskboard.Fence)严格大于被释放的那一代——宿主的下游资源凭代次比较 拒收被释放者的迟到写入(与 taskboard / sideeffect 的栅栏同一纪律)。
危险实现(契约禁止):只改状态不清租约——旧令牌仍然匹配,幽灵持有者的 迟到提交会照常落账。
对不在认领态的任务强释 → 包装 taskboard.ErrLeaseLost 的错误(该任务 的租约已经不在了;介入重试撞上它说明第一次介入已生效)。
func (ForceRelease) Validate ¶
func (r ForceRelease) Validate() error
Validate 介入请求的留痕完整性判定(Operator 与 Basis 必填——无归责不介入)。
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler 控制面的 JSON 适配面。经 NewHandler 构造,直接挂到任意 http.Server。
func NewHandler ¶
func NewHandler(p Plane, a Authorizer) *Handler
NewHandler 构造适配面。auth 可为 nil——此时全部请求 401(见包内 httpapi 头注:控制面不开无鉴权的口子)。
type MemAuditor ¶
type MemAuditor struct {
// contains filtered or unexported fields
}
MemAuditor 进程内参考留痕面。零值可用,并发安全。
func (*MemAuditor) Append ¶
func (a *MemAuditor) Append(_ context.Context, e AuditEntry) error
Append 追加留痕(先判完整性与动作词表,再补时刻,再落账)。
func (*MemAuditor) Entries ¶
func (a *MemAuditor) Entries(_ context.Context, target string) ([]AuditEntry, error)
Entries 按目标取留痕(target 空 = 全部;按追加序)。 返回的是拷贝:调用方改写条目不影响留痕面(只增不改的可观测面同样只读)。
type MemPlane ¶
type MemPlane struct {
// contains filtered or unexported fields
}
MemPlane 进程内参考实现。经 NewMemPlane 构造;并发安全。
func NewMemPlane ¶
NewMemPlane 构造参考实现。aud 可为 nil——留痕面缺位时介入一律拒绝 (ErrNoAuditor),这正是契约要演示的 fail-closed 形态。
func (*MemPlane) Adjudicate ¶
func (m *MemPlane) Adjudicate(ctx context.Context, req Adjudication) error
Adjudicate 实现 Plane:裁决未知结果(契约见 Adjudication;sideeffect 同口径)。
func (*MemPlane) Claim ¶
func (m *MemPlane) Claim(_ context.Context, id taskboard.TaskID, agent taskboard.AgentID, ttl time.Duration) (taskboard.Task, error)
Claim 运行时侧:认领(铸造令牌与接管代次,语义对齐 taskboard.Board.Claim ——被占用 ErrTaken / 终态 ErrTerminal / 依赖未完成 ErrBlocked)。
func (*MemPlane) ForceRelease ¶
func (m *MemPlane) ForceRelease(ctx context.Context, req ForceRelease) error
ForceRelease 实现 Plane:经闸换持有者(契约见 ForceRelease)。
留痕与状态变更在同一临界区:留痕失败 ⇒ 状态不变 ⇒ 整体失败—— 不存在「改完了但没留痕」的窗口。
func (*MemPlane) SeedEffect ¶
SeedEffect 运行时侧:登记一个处于「结果未知」态的副作用键。 unsafe 对应 sideeffect 的 RetrySafetyUnsafe(未生效裁决不自动开门)。
func (*MemPlane) SeedRun ¶
SeedRun 运行时侧:建一个运行及其任务(taskID 全局唯一)。 status 为空取 contract.StatusCreated。重复 run / 重复任务 → ErrAlreadyExists。 先整单校验再落账:部分落账的 run 是半成品状态,视图会把「建了一半」 当成「建成了」。
type Plane ¶
type Plane interface {
// TaskView 任务的运行状态视图(快照 + 该任务身上的不变量违反)。
// 不存在 → ErrNotFound。
TaskView(ctx context.Context, id taskboard.TaskID) (TaskView, error)
// RunView 运行的状态视图(状态、代次、全部任务、全 run 不变量视图)。
// 不存在 → ErrNotFound。
RunView(ctx context.Context, id RunID) (RunView, error)
// ForceRelease 强制释放任务认领(契约见 ForceRelease)。
ForceRelease(ctx context.Context, req ForceRelease) error
// Adjudicate 裁决未知结果(契约见 Adjudication)。
Adjudicate(ctx context.Context, req Adjudication) error
// Terminate 终止运行(契约见 Termination)。
Terminate(ctx context.Context, req Termination) error
}
Plane 控制面契约:查询 + 介入。存储中立——实现方是宿主。
实现方必须保证(三条硬契约,缺一即破):
- 介入**走运行时的同一套判据**(令牌 / 代次 / 栅栏),不绕闸;
- 介入**留痕**:Auditor 缺位时一律拒绝(ErrNoAuditor),不允许 「先介入、留痕以后补」——补的留痕无法证明发生在动作之前;
- 介入请求**可归责**:Operator 与 Basis 必填(各请求的 Validate 判)。
查询面无此约束:查询不改变状态,未注入租户上下文时由宿主自行裁剪 (httpapi 的 Authorizer 是宿主裁剪的落点)。
type RunView ¶
type RunView struct {
// RunID 运行标识。
RunID RunID
// Status 运行状态。取值建议用 contract 的状态常量(内核不解释词表;
// 终态词表与 contract 保持一致,宿主自定义状态自行登记)。
Status string
// Gen 运行代次:运行级介入的乐观并发判据。Terminate 携带的
// ExpectedGen 必须等于它,不等即 ErrStaleRun——介入方先查视图再介入,
// 两次介入之间状态若已前进,旧视图上的动作必须重查。
Gen uint64
// Tasks 该 run 的全部任务快照(按任务 id 排序,确定性输出)。
Tasks []taskboard.Task
// Problems 全 run 的不变量视图(taskboard.Check)。
Problems []taskboard.Problem
}
RunView 运行的状态视图。
type TaskView ¶
type TaskView struct {
// Task 任务快照(状态 / owner / 租约 / 代次——与运行时读到的同一份)。
Task taskboard.Task
// Problems 该任务身上的不变量违反(run 级 taskboard.Check 的投影;
// 空 = 干净)。控制面不止报状态,还报「这份状态是否自相矛盾」——
// 人工介入的结论(Basis)往往正是一条 Problem。
Problems []taskboard.Problem
}
TaskView 任务的运行状态视图。
type Termination ¶
type Termination struct {
// RunID 要终止的运行。
RunID RunID
// ExpectedGen 介入方视图里的运行代次(来自 RunView.Gen)。
ExpectedGen uint64
// Operator 谁在介入(留痕必填)。
Operator string
// Basis 依据什么结论(留痕必填)。
Basis string
}
Termination 终止运行。
契约:终止凭运行代次(乐观并发) ¶
ExpectedGen 必须等于当前 RunView.Gen:不等即 ErrStaleRun。终止本身就是 一次代次前进——两个介入者拿着同一份旧视图并发终止,先到者生效,后到者 凭代次被拒,不会出现「两次终止都以为自己终了这次运行」。终止是运行级 动作:任务租约的清理走 ForceRelease,两级判据不混。