Documentation
¶
Overview ¶
contention.go —— 认领的并发正确性**判据**(判据,不是测试风格)。
为什么判据要长在包里而不是只在测试里 ¶
「并发认领同一任务只能有一个赢家」是 @@Board@@ 的**契约条款**,不是某个 实现的性质。写在测试里 → 每个宿主各写一份、各判一套;写成导出的判据 → 宿主的存储实现只要被这个函数跑一遍就算验过契约(@sidetest@ 的同一范式)。
判据怎么判 ¶
让 N 个 agent 在同一个起跑线上同时 @@Claim@@ 同一个任务:
- 成功者必须**恰好 1 个**(0 个说明谁都认领不了;>1 个说明存储没做到原子);
- 失败者的错误必须是 @@ErrTaken@@(不能是 ErrBlocked / ErrTerminal 之类—— 那说明这些 agent 压根没走到争用那一步,这次判定没测到想测的东西);
- 任何**其它**错误都是基础设施故障(要显式报出来,不能混进「输家」里)。
第三条容易被忘掉:把「数据库连不上」也算成「输家」,判据就会在存储坏掉时 **变绿**——它测到的是「没有人赢」而不是「只有一个赢家」,而两者都符合 「winners <= 1」。
Package taskboard 多 agent 协调面的**存储中立契约 + 一致性判据**。
它补的是哪一段 ¶
@dispatch@ 只回答「能不能派」。派出去之后的三件事——谁负责哪个子任务、 它依赖谁、怎么把认领还回去——此前**没有任何机制面**:各宿主自己拿一张 表或一个 map 兜着,于是「同一个子任务被两个 agent 同时认领」这种错在 报告里读起来完全正常(两条分支都干完了活,各自都以为自己是负责人)。
本仓给什么、不给什么 ¶
**给**:① 存储中立的接口(@@Board@@);② 快照的**不变量判据**(@@Check@@, 纯函数,宿主可以随时拿当前快照跑一遍);③ 并发正确性的**可跑断言** (@@ConcurrentClaim@@,判据本身而不是测试风格)。
**不给**:存储实现。表结构、事务边界、持久化形态是宿主资产——本仓给一份 参考实现,宿主就会照着改它,最后两份真相并存。这与 @checkpoint.StoreCAS@ 的分工完全同构:契约与判据归内核,落库归宿主。
为什么「认领」必须是原子的 ¶
认领是**读-判-写**:读当前状态、判依赖是否完成、写 owner。三段分开做, 两个 agent 会读到同一个「open」,各自判定自己可以认领,然后先后写入—— 两个赢家。这与 @@commitgate@@ 的「两条并行分支都赢了」是同一种失效, 而这里的代价更直接:同一份工作被做两遍,或者两份互相覆盖的结果被当成一份。
故契约要求 @@Board.Claim@@ **由存储实现保证原子性**,本包只判结果 (并发跑 N 次,成功者必须恰好 1 个)。
释放认领凭令牌,不凭内容 ¶
与 @@commitgate@@ 的教训同源:用「任务 id + agent 名」认人判的是 「内容相同」,不是「同一次占用」。同名 agent(并行分支常同名)或 「失败 → 释放 → 重试」这条推荐路径都会撞上,于是输家放掉赢家的闸、 上一轮的迟到释放放掉新一轮的闸。故 @Lease@ 里的 token 由存储**铸造**, @@Release@@ / @@Complete@@ 只认 token。
Index ¶
Constants ¶
const ( CodeEmptyID = "empty_id" CodeDuplicateID = "duplicate_id" CodeUnknownState = "unknown_state" CodeUnknownDep = "unknown_dep" CodeSelfDep = "self_dep" CodeCycle = "dep_cycle" CodeClaimedNoLease = "claimed_without_lease" CodeLeaseNoClaim = "lease_without_claim" CodeBlockedClaim = "blocked_claim" CodeTerminalClaimed = "terminal_claimed" )
不变量类别。
Variables ¶
var ( // ErrNotFound 任务不存在。 ErrNotFound = errors.New("taskboard: 任务不存在") // ErrTaken 任务被他人活着持有。 ErrTaken = errors.New("taskboard: 任务已被他人认领") // ErrBlocked 依赖未完成。 ErrBlocked = errors.New("taskboard: 依赖未完成") // ErrTerminal 任务已是终态。 ErrTerminal = errors.New("taskboard: 任务已是终态") // ErrLeaseLost 令牌不匹配(租约已易主或被释放过)。 ErrLeaseLost = errors.New("taskboard: 租约已易主") )
错误哨兵(宿主实现必须复用,不要各造一套:判定方按 As/Is 认人)。
Functions ¶
Types ¶
type Board ¶
type Board interface {
// List 返回当前全部任务的快照(顺序不要求,判据会排序)。
List(ctx context.Context) ([]Task, error)
// Claim 认领一个任务。
//
// 以下情形必须报错而不是「尽力而为」:不存在(ErrNotFound)/
// 被别人活着持有(ErrTaken)/ 依赖未完成(ErrBlocked)/ 已是终态(ErrTerminal)。
Claim(ctx context.Context, id TaskID, agent AgentID, lease Lease) (Task, error)
// Release 归还认领(凭 token)。
Release(ctx context.Context, id TaskID, token string) error
// Complete 标记完成(凭 token)。
Complete(ctx context.Context, id TaskID, token string) error
}
Board 协调面的存储中立契约。
实现方必须保证:
- @@Claim@@ **原子**:读-判-写不可分割(并发认领同一任务只能有一个赢家);
- @@Release@@ / @@Complete@@ **只认 token**,且 token 不匹配时报 @@ErrLeaseLost@@(不许静默成功——静默成功会让输家以为自己的释放生效了);
- @@List@@ 返回**一致快照**(不是边改边读的流)。
type Lease ¶
type Lease struct {
// Token 由存储**铸造**的唯一令牌(释放与完成的唯一凭据)。
Token string
// Until 租约到期时刻(零值 = 不限期,由宿主自担)。
//
// 到期后其他 agent 可以接管:进程崩掉时没有它会留下一把永久占用。
Until time.Time
}
Lease 一次认领的凭证。
type Problem ¶
type Problem struct {
// Task 相关任务 id。
Task TaskID
// Code 机器可判的类别(宿主按它分流与留痕)。
Code string
// Text 人读的说明。
Text string
}
Problem 一条快照不变量违反。
type Task ¶
type Task struct {
// ID 任务标识(全局唯一)。
ID TaskID
// Title 展示用标题。
Title string
// Deps 依赖的任务 id(全部完成才可认领)。
Deps []TaskID
// State 当前状态。
State State
// Owner 当前认领者(仅 StateClaimed 时有值)。
Owner AgentID
// Lease 当前租约(仅 StateClaimed 时有值)。
Lease Lease
// Seq 单调递增序号(宿主用它做 CAS 与留痕;本包只要求它随变更增大)。
Seq uint64
}
Task 一个子任务(值类型,可安全复制与比较)。