outbox

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package outbox 提供事务发件箱:同事务登记,提交后主动发送,失败由项目 cron 补偿。 支持 MySQL/PostgreSQL;Store 借用主库,Worker 支持同步和异步发送,不启动后台调度。 构建标签 pixiu_bootstrap_no_mysql / pixiu_bootstrap_no_postgresql 同时排除对应后端的驱动错误适配, NewStore/NewDefaultStore 拒绝被禁用的后端;默认构建保留两种后端。

应用先通过 schemamigrate 显式登记 &outbox.Record{},再注册默认 Store 和 Worker:

_, err := outbox.NewDefaultStore(outbox.StoreConfig{
	Backend:  outbox.MySQL,
	MasterDB: mysqlClient.MasterDB,
	Location: env.TimeLocation(),
})
if err != nil {
	return err
}
_, err = outbox.NewDefaultWorker(map[outbox.HandlerKey]outbox.Handler{
	{
		Kind:    "project.notification",
		Version: 1,
	}: sendNotification,
})
if err != nil {
	return err
}
var taskID int64
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
	if err := tx.Create(&order).Error; err != nil {
		return err
	}
	var err error
	taskID, err = outbox.EnqueueTx(tx, outbox.Task{
		Kind:      "project.notification",
		Version:   1,
		Payload:   payload,
		ExpiresAt: time.Now().Add(24 * time.Hour),
	})
	return err
})
if err != nil {
	return err
}
outbox.SendAsync(taskID)
return nil

示例需导入 outbox、infra/env、gorm.io/gorm、time;ctx、db、mysqlClient、 order、payload 由应用提供。sendNotification 的签名为 func(context.Context, outbox.Delivery) error,同步发送成功才返回 nil。 db.Transaction 必须是最外层事务;提交后的发送失败不能回滚已提交业务。

每个进程只登记一个默认 Store 和 Worker,不提供 named 管理。NewDefaultWorker 自动绑定默认 Store, Handler 表在创建时复制,可包含多个 Kind/Version;完成全部初始化后再运行业务。 重复登记返回错误且不覆盖,初始化失败可重试;默认实例不支持替换、重置或运行期增改 Handler。 DefaultStore/DefaultWorker 返回已登记实例,未登记时 panic;包级 EnqueueTx/Get/Send/SendAsync/RetryPending 同样如此。 可通过 outbox.Get(ctx, taskID) 查询状态。仅登记任务的进程可以只初始化默认 Store。 NewStore/NewWorker 保留显式实例用法,不隐式设置默认实例;所有实例均不拥有数据库或 MQ 资源。

SendAsync(taskID) 在调用方检查默认 Worker,再直接启动 goroutine;显式实例可调用 worker.SendAsync(taskID)。 每次发送从 context.Background() 派生独立的 5 秒超时 context,不携带请求 context 的值, Handler 和发送日志从任务记录恢复 logId。异步入口不返回错误,失败额外记录 OutboxAsyncFailed; pending 任务由 cron 补偿,不使用 taskpool、不排队、不限制并发,也不在内部重试。 5 秒是 context 期限,Handler 须响应取消;结果回写有独立收尾过程,实际结束可能超过 5 秒。 如需同步等待结果,使用 outbox.Send(ctx, taskID);发送失败不能撤销已提交业务。

将补偿函数注册到已配置分布式锁的 cronx,频次由项目配置:

func retryOutbox(ctx context.Context) error {
	return outbox.RetryPending(ctx, 100)
}

batchSize 是查询页大小,每轮按 ID 游标扫描,每条任务最多尝试一次。 发送失败保留 pending 和 LastError;成功为 succeeded;发送前已过期则为 failed。 ExpiresAt 零值表示不过期;已开始的发送按结果收尾,不会撤回。 Send 与 RetryPending 均使用 FOR UPDATE NOWAIT;不同任务可并发发送。 同一任务被锁时,Send 返回 ErrBusy,RetryPending 跳过;cronx 分布式锁协调补偿轮次。 每条任务的事务从加锁持续到 Handler 返回并回写;Handler 应响应 context,发送期间占用数据库连接。 持锁读取后按应用服务器时间判断过期,次数和发送结果只更新一次。结果 UPDATE 使用独立的 3 秒 context; MySQL 提交/回滚还依赖连接读写超时,整个收尾过程不保证在 3 秒内返回。 数据库生成 int64 自增 ID,重复登记会生成新任务;重试沿用 ID,回写失败可能重发,消费者须幂等。 旧字符串主键须显式迁移并处理存量消息去重标识,不能直接 AutoMigrate 切换。

Location 省略时为 UTC,两种数据库均保留绝对时刻,读取按配置时区表示。 MySQL 时间列为 TIMESTAMP(6),驱动须开启 parseTime,且驱动 Location 与会话 time_zone 一致; 推荐连接使用 UTC,展示时区可独立传入 env.TimeLocation()。PostgreSQL 使用 TIMESTAMPTZ(6)。 Record 的时间字段使用 Timestamp,通过 record.CreatedAt.Time 获取原生 time.Time。 MySQL 非空 ExpiresAt 的 UTC 整秒范围为 1 至 2147483647,越界返回 ErrInvalidArgument。 已有 MySQL DATETIME 表须先按原存储时区迁移为 TIMESTAMP,不能在未知时区下直接 AutoMigrate。 退出时先停止业务/cron,并等待同步发送及补偿调用返回,再关闭 MQ/数据库,最后关闭日志。 SendAsync 不提供等待或排空,退出可能中断本次尝试,pending 任务由后续 cron 补偿; 必须在关闭底层资源前确认发送结束时,应使用 Send 并由应用等待同步调用完成。

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidArgument = errors.New("outbox: invalid argument")
	ErrNotTransaction  = errors.New("outbox: transaction required")
	ErrBackendMismatch = errors.New("outbox: backend mismatch")
	ErrNotFound        = errors.New("outbox: task not found")
	ErrBusy            = errors.New("outbox: sender busy")
	ErrExpired         = errors.New("outbox: task expired")
)

Functions

func EnqueueTx

func EnqueueTx(tx *gorm.DB, task Task) (int64, error)

EnqueueTx 使用默认 Store 在调用方事务内登记,返回自增 ID;未登记 Store 时 panic。 不发布、不提交、不自动唤醒;最外层事务提交成功后才能使用 ID 发送。

func RetryPending

func RetryPending(ctx context.Context, batchSize int) error

RetryPending 使用默认 Worker 执行一轮补偿;未登记 Worker 时 panic。 batchSize 为正的查询页大小,应由配置分布式锁的 cronx 周期调用。

func Send

func Send(ctx context.Context, id int64) error

Send 使用默认 Worker 主动发送一次;未登记 Worker 时 panic。 必须在最外层业务事务提交成功后调用,发送失败由下轮 cron 补偿。

func SendAsync

func SendAsync(id int64)

SendAsync 使用默认 Worker 在后台发送一次;未登记 Worker 时在调用方 panic。 必须在最外层业务事务提交成功后调用;独立 5 秒超时,失败记录日志并留给 cron 补偿。

Types

type Backend

type Backend string

Backend 指定业务事务和补偿所用的数据库方言。

const (
	MySQL      Backend = "mysql"
	PostgreSQL Backend = "postgresql"
)

type Delivery

type Delivery struct {
	Task
	ID      int64
	Attempt int64
}

Delivery 是一次发送的消息快照,Attempt 包含本次尝试。

type Handler

type Handler func(context.Context, Delivery) error

Handler 同步完成一次发送;返回 nil 才标记成功,失败留给下轮 cron。 调用方 context 控制执行期限,Handler 应及时响应取消。

type HandlerKey

type HandlerKey struct {
	Kind    string
	Version int
}

type Key

type Key string

Key 是任务记录中的标识字符串,为迁移提供跨数据库的二进制排序语义。

func (Key) GormDBDataType

func (Key) GormDBDataType(db *gorm.DB, field *schema.Field) string

func (Key) GormDataType

func (Key) GormDataType() string

type Record

type Record struct {
	ID         int64      `` /* 167-byte string literal not displayed */
	Kind       Key        `gorm:"size:64;not null;comment:任务类型,与版本共同匹配处理器"`
	Version    int32      `gorm:"not null;comment:任务协议版本,正整数"`
	Payload    []byte     `gorm:"not null;comment:不可变任务内容,最大1MiB"`
	Status     Status     `` /* 191-byte string literal not displayed */
	Attempts   int64      `gorm:"not null;comment:发送尝试次数,包含首次"`
	ExpiresAt  *Timestamp `gorm:"precision:6;comment:发送截止时间,空表示不过期"`
	LastError  string     `gorm:"type:text;not null;comment:最近一次失败原因,最大4KiB,成功后清空"`
	LogID      string     `gorm:"type:text;not null;comment:登记时的上下文logId"`
	CreatedAt  Timestamp  `gorm:"precision:6;not null;autoCreateTime:false;comment:数据库生成的创建时间,按配置时区读取"`
	UpdatedAt  Timestamp  `gorm:"precision:6;not null;autoUpdateTime:false;comment:数据库生成的更新时间,按配置时区读取"`
	FinishedAt *Timestamp `gorm:"precision:6;index:pixiu_outbox_finished_idx,priority:2;comment:成功或过期的完成时间"`
}

Record 对应事务发件箱表 pixiu_outbox,显式交给 schemamigrate 迁移,同时作为 Get 返回的独立快照。 表注释在登记模型时通过 schemamigrate.Table.Comment 指定为“事务发件箱”。 状态修改须经 Store/Worker,发送互斥由数据库行锁保证。

func Get

func Get(ctx context.Context, id int64) (Record, error)

Get 使用默认 Store 查询任务;未登记 Store 时 panic。

func (*Record) AfterFind

func (r *Record) AfterFind(tx *gorm.DB) error

AfterFind 将数据库返回的绝对时刻转换为 Store 配置的时区表示。 直接使用 GORM 查询且没有 Store 上下文时沿用 UTC,不改变共享连接。

func (*Record) TableName

func (*Record) TableName() string

type Status

type Status string

Status 是持久化任务状态。

const (
	Pending   Status = "pending"
	Succeeded Status = "succeeded"
	Failed    Status = "failed"
)

func (Status) GormDBDataType

func (Status) GormDBDataType(db *gorm.DB, field *schema.Field) string

func (Status) GormDataType

func (Status) GormDataType() string

type Store

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

Store 借用配置中的主库,不创建连接、迁移表或启动后台服务。

func DefaultStore

func DefaultStore() *Store

DefaultStore 返回显式登记的默认 Store;未登记时 panic。 NewStore 不设置默认实例,登记后不能替换或重置。

func NewDefaultStore

func NewDefaultStore(cfg StoreConfig) (*Store, error)

NewDefaultStore 创建并登记唯一的默认 Store;失败不占用默认位置,重复登记返回错误。 应在启动阶段登记,完成 Store 和 Worker 初始化后再运行业务;数据库仍由调用方管理。

func NewStore

func NewStore(cfg StoreConfig) (*Store, error)

func (*Store) EnqueueTx

func (s *Store) EnqueueTx(tx *gorm.DB, task Task) (int64, error)

EnqueueTx 在调用方事务内登记并返回自增 ID,不发布、不提交、不自动唤醒。 ID 只在最外层事务提交成功后可用于发送;回滚可能留下序列间隙,重复登记会生成新任务。

func (*Store) Get

func (s *Store) Get(ctx context.Context, id int64) (Record, error)

type StoreConfig

type StoreConfig struct {
	Backend  Backend
	MasterDB func(context.Context) *gorm.DB
	// Location 为 nil 时使用 UTC,控制查询结果与 Delivery 的时间表示。
	// MySQL 驱动解析时区须与数据库会话时区一致,本配置不修改连接。
	Location *time.Location
}

StoreConfig 借用已有主库,不管理连接生命周期。

type Task

type Task struct {
	Kind      string
	Version   int
	Payload   []byte
	ExpiresAt time.Time
}

Task 保存不可变发送意图,Payload 不超过 1 MiB;登记时由数据库生成 ID。 ExpiresAt 为绝对截止时间,零值表示不过期。 MySQL 非空 ExpiresAt 的 UTC 整秒必须在 1 至 2147483647 范围内。

type Timestamp

type Timestamp struct {
	time.Time
}

Timestamp 嵌入标准时间,为迁移提供 MySQL TIMESTAMP 与 PostgreSQL TIMESTAMPTZ 映射。 时间方法和 JSON 表示沿用 time.Time,原生值通过 Time 字段取得。 数据库参数传入 *Timestamp 或原生 Time;空指针表示 SQL NULL。

func (*Timestamp) GormDBDataType

func (*Timestamp) GormDBDataType(db *gorm.DB, field *schema.Field) string

func (*Timestamp) GormDataType

func (*Timestamp) GormDataType() string

func (*Timestamp) Scan

func (t *Timestamp) Scan(value any) error

Scan 只接收驱动已解析的时间,避免猜测无时区字符串的实际含义。

func (*Timestamp) Value

func (t *Timestamp) Value() (driver.Value, error)

Value 将原生时间交给驱动,保留实际时刻及连接的正常时区转换。

type Worker

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

Worker 执行主动发送与一轮补偿;支持同步 Send 和独立 5 秒超时的 SendAsync。 每条任务使用独立事务行锁;多实例补偿轮次由应用的 cron 分布式锁协调。

func DefaultWorker

func DefaultWorker() *Worker

DefaultWorker 返回显式登记的默认 Worker;未登记时 panic。 NewWorker 不设置默认实例,登记后不能替换或重置。

func NewDefaultWorker

func NewDefaultWorker(handlers map[HandlerKey]Handler) (*Worker, error)

NewDefaultWorker 创建并登记唯一的默认 Worker,绑定已登记的默认 Store。 未登记 Store、Handler 无效或重复登记均返回错误;失败可重试,不替换已有实例。 Handler 表在创建时复制;不启动后台服务,不管理数据库或消息客户端的生命周期。

func NewWorker

func NewWorker(store *Store, handlers map[HandlerKey]Handler) (*Worker, error)

func (*Worker) RetryPending

func (w *Worker) RetryPending(ctx context.Context, batchSize int) error

RetryPending 按 ID 分页补偿本表的待发送任务,每条任务本轮最多尝试一次。 batchSize 为查询页大小;跳过已锁任务,失败继续下一条,扫描/回写错误或取消则结束本轮。 应挂载到配置分布式锁的 cronx,避免多实例补偿轮次重叠。

func (*Worker) Send

func (w *Worker) Send(ctx context.Context, id int64) error

Send 在业务事务提交后尝试发送一次;失败保存原因,由下轮 cron 补偿。 已成功任务直接返回 nil;过期返回 ErrExpired;同一任务被锁定时返回 ErrBusy。

func (*Worker) SendAsync

func (w *Worker) SendAsync(id int64)

SendAsync 在最外层业务事务提交后启动 goroutine,用独立的 5 秒超时 context 尝试发送一次。 不接收请求 context;Handler 和发送日志通过任务记录中的 logId 关联业务。 错误记录日志,pending 任务由 cron 补偿;不提供等待或排空,退出可能中断本次尝试。 Handler 须响应取消;结果回写仍有独立收尾过程,5 秒不是 goroutine 的强制结束期限。

Directories

Path Synopsis
Package natsoutbox 将 JetStream 发布接入 outbox,发送成功以有效 PubAck 为准。
Package natsoutbox 将 JetStream 发布接入 outbox,发送成功以有效 PubAck 为准。

Jump to

Keyboard shortcuts

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