taskboard

package
v0.31.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
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

View Source
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

func ConcurrentClaim

func ConcurrentClaim(ctx context.Context, b Board, id TaskID, agents []AgentID, lease func(AgentID) Lease) (int, error)

ConcurrentClaim 让多个 agent 同时认领同一个任务,返回成功者数量。

lease 为每个 agent 铸造租约(测试里通常是固定 token;真实存储应当自己铸)。 返回的 error 只表示**基础设施故障或契约违反**,不表示「有人输」—— 输家是正常结果。

Types

type AgentID

type AgentID string

AgentID 执行者标识。

**允许重复**(并行分支常同名)——这正是「凭 token 不凭内容」的理由。

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 一条快照不变量违反。

func Check

func Check(tasks []Task) []Problem

Check 判一份快照的**不变量**,按 (Task, Code) 排序返回(确定性)。

这是宿主随时能跑的「体检」:不必等出问题,配置加载、每个调度周期都可以跑; 它判的是**状态自身是否自相矛盾**,不判「策略对不对」。

type State

type State string

State 任务状态。

const (
	// StateOpen 待认领。
	StateOpen State = "open"
	// StateClaimed 已被认领(owner + lease 必须有值)。
	StateClaimed State = "claimed"
	// StateDone 已完成(终态)。
	StateDone State = "done"
	// StateFailed 已失败(终态;但允许重新认领——失败的工作通常要重做)。
	StateFailed State = "failed"
)

func (State) Terminal

func (s State) Terminal() bool

Terminal 报告该状态是否**不可再被认领**。

只有 done 是终态:failed 的任务必须能重做,否则一次失败就把子任务永久钉死, 而宿主的补救路径(重新派一个人)会静默失效。

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 一个子任务(值类型,可安全复制与比较)。

type TaskID

type TaskID string

TaskID 子任务标识。

Directories

Path Synopsis
Package boardtest @@Board@@ 的**共享契约测试套件**。
Package boardtest @@Board@@ 的**共享契约测试套件**。

Jump to

Keyboard shortcuts

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