sqlbroker

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package sqlbroker 是 PostgreSQL / MySQL 两个服务器型后端的共享核心:基于标准库 database/sql,把两库"真正不同"的点收进 Dialect(见 dialect.go),其余标准 SQL 一份。 实现蓝本是 sqlitebroker,但有两处 sqlite 没有、SQL 后端必须新写的机制:

  1. withTx 死锁重试环:sqlite 单连接串行天生不死锁,PG/MySQL 是真并发行锁, 多行事务(尤其连锁传播按树形状加锁)必然有死锁窗口,靠 Dialect.Retryable 判定后自动重跑。
  2. 占位符只用不复用:一律位置 ?,同值在 args 里重复传(MySQL 不支持 ?N 复用)。

认领互斥与 ReapExpired 靠 FOR UPDATE SKIP LOCKED(要求 MySQL 8.0+ / PG 9.5+), 终态更新 + 子任务唤醒同事务(宪法 III),语义由 brokertest 18 条契约统一验收(env 门控)。

本包是 internal:只给同模块的 pgbroker/mysqlbroker 薄壳 import,不对外公开。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetTestQuotaNow

func SetTestQuotaNow(fn func() int64)

SetTestQuotaNow 仅测试用(与 TotalRetries 同类的测试观测口):设置介质时间覆盖, 传 nil 恢复"用数据库服务端钟"。非并发安全,只应在跑用例前的单线程阶段设置。

func TotalRetries

func TotalRetries() int64

TotalRetries 返回 withTx 重试环累计触发次数,供压测断言死锁重试真的发生过。

Types

type Broker

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

Broker PG/MySQL 共享后端。实现 taskgate.Broker。

func New

func New(dialect Dialect, db *sql.DB, cfg Config) *Broker

New 用打开好的 *sql.DB 和一个方言装配核心。db 由薄壳包 sql.Open 得到; 返回的 Broker 用之前必须先 Init(建表也在 Init 里,那时才有独占连接跑 DDL)。

func (*Broker) Ack

func (b *Broker) Ack(ctx context.Context, id, leaseToken string, result []byte) error

Ack 成功完结:completed + Result + FinishedAt,并在同一事务内唤醒子任务。

func (*Broker) Cancel

func (b *Broker) Cancel(ctx context.Context, id string) error

Cancel 取消:排队类状态(blocked/pending/retrying)直接 canceled 并同事务传播; running 只打 cancel_requested 标记(终态由 FinishCanceled 落库); 终态报 ErrAlreadyFinal,不存在报 ErrTaskNotFound。

func (*Broker) Close

func (b *Broker) Close() error

Close 关库:标记 closed 并广播,让阻塞中的 Dequeue 尽快退出,再关连接池。

func (*Broker) Counts

func (b *Broker) Counts(ctx context.Context) (map[string]map[taskgate.Status]int64, error)

Counts 出现过的 Type×Status 稀疏矩阵,和逐个 Get 汇总必须一致(brokertest 验证)。

func (*Broker) Dequeue

func (b *Broker) Dequeue(ctx context.Context, queues []string) (*taskgate.Task, error)

Dequeue 阻塞认领:直到某队列出现"status∈{pending,retrying} 且 run_at≤now"的任务,或 ctx 取消。 循环结构与 sqlitebroker 一致,三个唤醒源:同进程写入的内部信号、注入 clock 的到点信号、ctx。 跨进程写入靠 PollInterval 兜底轮询发现(默认 200ms,一次网络往返,比 sqlite 的 100ms 放宽)。

func (*Broker) Enqueue

func (b *Broker) Enqueue(ctx context.Context, t *taskgate.Task) error

Enqueue 入队。同一个事务内完成:ID 查重、父任务存在性校验、初始状态判定(DecideOnSubmit)、 tasks 与 task_deps 落库;生成的 ID 与判定结果回填到 *t。撞主键翻译成 ErrTaskExists。

func (*Broker) Fail

func (b *Broker) Fail(ctx context.Context, id, leaseToken, errMsg string, kind taskgate.FailKind, retryAt time.Time) error

Fail 失败路径:按 FailKind 动对应计数,封顶或耗尽进 failed(同事务连锁传播), 否则进 retrying、RunAt=retryAt 到点重跑。

func (*Broker) FinishCanceled

func (b *Broker) FinishCanceled(ctx context.Context, id, leaseToken string) error

FinishCanceled worker 响应取消后收尾:running → canceled 落库并同事务传播。

func (*Broker) Get

func (b *Broker) Get(ctx context.Context, id string) (*taskgate.Task, error)

Get 取单个任务(只读,不加锁)。scanRec 扫出来就是全新副本,调用方改了不影响存储。

func (*Broker) Heartbeat

func (b *Broker) Heartbeat(ctx context.Context, id, leaseToken string) error

Heartbeat 续租:lease_until = now + TTL。发现取消标记时续租照做(先提交), 再返回 ErrTaskCanceled 提醒 scheduler 去 cancel handler 的 ctx。

func (*Broker) Init

func (b *Broker) Init(opts taskgate.BrokerOptions) error

Init 装配运行参数(零值补默认),并建表。首次调用时在独占连接上加库级互斥锁跑 DDL, 兼容多进程冷启动并发建表(避免 PG 的 tuple concurrently updated 等脏错)。

func (*Broker) List

func (b *Broker) List(ctx context.Context, f taskgate.Filter) ([]*taskgate.Task, error)

List 按 Filter 过滤,零值字段不过滤;先过滤 → ORDER BY (created_at, id) 升序 → LIMIT/OFFSET 分页(排序分页合同见 broker-contract.md,Offset 越界返回空)。只读走连接池无锁。

func (*Broker) QueueLen

func (b *Broker) QueueLen(ctx context.Context, queue string) (int, error)

QueueLen 队列积压:status∈{pending,retrying} 的数量(不看 RunAt 到没到点)。

func (*Broker) QueueQuota

func (b *Broker) QueueQuota(queue string, qc taskgate.QueueConfig) (taskgate.QuotaGate, error)

QueueQuota 构造该队列的配额闸(taskgate.QuotaProvider 能力实现)。

func (*Broker) ReapExpired

func (b *Broker) ReapExpired(ctx context.Context) (int, error)

ReapExpired 回收过期租约(lease_until < now 严格小于,压线不算过期)。逐行处理(不用 UPDATE...RETURNING): 先 SELECT id ... FOR UPDATE SKIP LOCKED 锁住待收割的行(多进程各跑 reaper 不互踩、不重复计), 再逐行按 sqlite 同一套语义改状态、连锁传播;封顶失败的 last_error 文案在 Go 里拼(免 || / CONCAT 方言差异)。

  • 第零步:带 cancel_requested 的过期任务直接落 canceled(不占 LeaseLost),触发传播;
  • 第一步:其余过期任务 LeaseLost+1,封顶进 failed(触发传播)否则回 pending;
  • 第二步:防御修复——blocked 却发现父全终态的,按提交时同一套决策函数补齐。

返回值只算租约回收条数(第零步 + 第一步)。

func (*Broker) Replay

func (b *Broker) Replay(ctx context.Context, req taskgate.ReplayRequest) (*taskgate.Task, error)

Replay 重放一次终态执行(spec 005):定位目标(按 ID 或按键取链尾)、校验前置条件 (终态/未被重放/completed 需显式允许)、创建新执行,全部在同一个事务内完成。 并发同目标重放:目标行 SELECT ... FOR UPDATE 串行化,uq_replay_of 唯一索引兜底, 恰好一个成功。目标行零改写;新执行沿用目标的 Type/Queue/MaxRetry/OnParentFailure 与 BusinessKey,三计数清零、无依赖、pending 落库。

func (*Broker) Requeue

func (b *Broker) Requeue(ctx context.Context, id, leaseToken string) error

Requeue 优雅停机时归还任务:running → pending,三计数与 RunAt 全不动,清租约和取消标记。 这不算失败,一个计数都不许占(合同要求)。

type Config

type Config struct {
	TablePrefix  string        // 表名/索引名前缀,默认 "taskgate_";多应用共享服务器库时隔离
	MaxOpenConns int           // 连接池上限,默认 10;防一堆 worker 的阻塞轮询打爆 max_connections
	PollInterval time.Duration // Dequeue 无果时的兜底轮询间隔,默认 200ms;跨进程写入靠它发现
	MaxTxRetry   int           // withTx 死锁重试上限,默认 5
}

Config 共享核心的运行参数,由薄壳包从各自 Options 映射而来(零值补默认)。

type Dialect

type Dialect interface {
	// Name 方言名 "postgres" / "mysql",日志与错误前缀用。
	Name() string

	// Rebind 只改 SQL 文本、不碰 args 切片:PG 把第 n 个 ? 换成 $n;MySQL 原样返回。
	// 核心里所有 SQL 一律只用不复用的位置 ?(同值在 args 里重复传),Rebind 才能保持最简形态。
	Rebind(query string) string

	// SchemaSQL 返回建表 + 建索引的 DDL(按 prefix 拼表名/索引名),按顺序执行。
	// 各库类型映射不同(TEXT/VARCHAR、BLOB/BYTEA/LONGBLOB、INTEGER/BIGINT);
	// MySQL 必须内置 utf8mb4_bin 排序规则(否则自定义 ID "abc"/"ABC" 被判重复、排序契约漂移)。
	SchemaSQL(prefix string) []string

	// IsDuplicateKey 判定错误是否为唯一约束冲突(主键或唯一索引)。
	// PG SQLSTATE 23505 / MySQL errno 1062。实现必须 errors.As 到驱动错误类型再看码,禁止字符串匹配。
	IsDuplicateKey(err error) bool

	// DuplicateKeyConstraint 唯一冲突撞的是哪个约束/索引(spec 005:据此区分主键、
	// uq_chain_head、uq_replay_of 三种冲突并翻译成不同的合同错误);不是唯一冲突或拿不到名字
	// 返回 ""。PG 从 pgconn.PgError.ConstraintName 取(结构化字段);MySQL 驱动没有结构化字段,
	// 只能从 1062 的 message 提取索引名——这是本项目唯一允许的错误字符串匹配,原因如上。
	DuplicateKeyConstraint(err error) string

	// IsIdempotentDDLErr 判定建表期 DDL 错误是否为"重复执行导致的良性错误"(列已存在/索引已存在)。
	// PG 的 DDL 全部用 IF NOT EXISTS 表达,恒返回 false;MySQL 的 ALTER 没有 IF NOT EXISTS,
	// 存量表升级(spec 005)靠 errno 1060(列已存在)/1061(索引名已存在)幂等跳过。
	IsIdempotentDDLErr(err error) bool

	// Retryable 判定事务错误的重试档位(见 RetryClass)。同样 errors.As 禁字符串匹配。
	Retryable(err error) RetryClass

	// QuotaSQL 周期配额(spec 006)两库"真正不同"的三条语句;表名按 prefix 拼好、
	// 占位符用 ?(核心过 Rebind)。Release/清理语句两库同形,不在本方法(见 quota.go)。
	QuotaSQL(prefix string) QuotaSQL

	// Lock 建表期库级互斥:多进程首启并发跑 DDL,PG 会报 tuple concurrently updated 等脏错。
	// 锁必须钉在传入的独占连接 conn 上(MySQL GET_LOCK 是会话级,连接池会错位)。
	Lock(ctx context.Context, conn *sql.Conn, key string) error
	// Unlock 释放建表锁(同一 conn)。PG advisory 会话结束也会自动释放,MySQL 必须显式 RELEASE_LOCK。
	Unlock(ctx context.Context, conn *sql.Conn, key string) error
}

Dialect 收口 PostgreSQL 与 MySQL 两库"真正不同"的点,其余标准 SQL 全在共享核心一份。 实现放在各自薄壳包(pgbroker/mysqlbroker),因为要断言驱动私有错误类型 (*pgconn.PgError / *mysql.MySQLError),核心包 internal/sqlbroker 因此零驱动依赖。

认领(claim)与收割(reap)不在本接口:它们统一用 "SELECT ... FOR UPDATE SKIP LOCKED 拿行 → 逐行 UPDATE" 完成,这个语法两库完全一致, 不需要 UPDATE...RETURNING(PG 有 MySQL 无),核心一份代码即可。

type QuotaSQL

type QuotaSQL struct {
	Reserve string
	Now     string
	Upsert  string
}

QuotaSQL 周期配额的方言语句包(spec 006):

  • Reserve 非空 = 单语句路径(PG):一条原子语句完成"取服务端时间 → 算窗口 → 检查余额 → 扣减 → RETURNING win";参数 (qkey, 时间覆盖, period, period, limit), 零行 = 耗尽;
  • Reserve 为空 = 两步路径(MySQL 无 RETURNING):先 Now 取服务端时间(参数:时间覆盖, NULL = 服务端钟),Go 侧算窗口,再 Upsert 原子扣减(参数 qkey, win, limit), 按 affected rows 判定(1=插入 2=真更新 → 成功;0=没变 → 耗尽)。 取时与扣减分两步不破坏硬配额:原子性在 Upsert 上,时间只决定窗口归属, 边界竞态最多把预留记进上一窗(仍是合法窗口,不会多放)。

type RetryClass

type RetryClass int

RetryClass 事务错误的重试判定,三态。sqlite 单连接串行不会死锁,所以蓝本里没有重试; PG/MySQL 是真并发行锁,多行事务(尤其连锁传播按树形状加锁)必然有死锁窗口, withTx 靠 Dialect.Retryable 判定后自动重跑整个事务(见 broker.go 的重试环)。

const (
	// NotRetryable 不可重试:业务错误、约束冲突等,原样返回给调用方。
	NotRetryable RetryClass = iota
	// RetryImmediate 立即可重试:数据库单方面 kill 的死锁/序列化失败(PG 40P01/40001、MySQL 1213),
	// 事务几乎瞬间返回,重试便宜,按指数退避重试到上限。
	RetryImmediate
	// RetryLimited 有限重试:MySQL 1205 锁等待超时——默认要等 innodb_lock_wait_timeout 才报,
	// 不能和死锁同等指数重试(单次调用最坏挂死几百秒),只重试一次、短退避。
	// mysqlbroker 同时会把会话级 innodb_lock_wait_timeout 调低配合。
	RetryLimited
)

Jump to

Keyboard shortcuts

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