feat: add reusable MySQL outbox and HTTP lifecycle example

This commit is contained in:
2026-09-24 20:54:36 +08:00
parent 0140c78ea7
commit e29dec717f
52 changed files with 2488 additions and 357 deletions

View File

@@ -0,0 +1,76 @@
package outbox
import (
"context"
"errors"
"time"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
var ErrShutdownTimeout = errors.New("outbox sender shutdown deadline exceeded; reconcile persisted records")
// RunOptions 控制进程轮询与关闭,不改变每条记录持久化的 24 小时/100 次预算。
type RunOptions struct {
PollInterval time.Duration // PollInterval 空闲/退避轮询间隔,默认500毫秒,范围10毫秒至10秒。
ShutdownTimeout time.Duration // ShutdownTimeout 取消后等待当前发送及本地确认,默认20秒,最多5分钟。
}
// Run 持续发送;取消后停止领取,限时完成当前交换及确认,调用方必须等待返回。
// 存储/确认错误返回给宿主处理;HTTP暂时失败持久化退避,REVIEW留在数据库供核查。
// 可以多进程并行运行;数据库租约协调领取,同一记录的HTTP仍可能重复,始终复用原键。
func (s *Store) Run(parent context.Context, api *orbit.Client, options RunOptions) error {
if api == nil {
return ErrInvalid
}
if options.PollInterval == 0 {
options.PollInterval = 500 * time.Millisecond
}
if options.ShutdownTimeout == 0 {
options.ShutdownTimeout = 20 * time.Second
}
if options.PollInterval < 10*time.Millisecond || options.PollInterval > 10*time.Second || options.ShutdownTimeout < time.Millisecond || options.ShutdownTimeout > 5*time.Minute {
return ErrInvalid
}
work, cancel := context.WithCancel(context.WithoutCancel(parent))
done := make(chan struct{})
expired := make(chan struct{})
go func() {
defer close(done)
select {
case <-work.Done():
return
case <-parent.Done():
}
timer := time.NewTimer(options.ShutdownTimeout)
defer timer.Stop()
select {
case <-work.Done():
case <-timer.C:
close(expired)
cancel()
}
}()
defer func() { cancel(); <-done }()
for parent.Err() == nil {
_, err := s.SendOnce(work, api)
select {
case <-expired:
return ErrShutdownTimeout
default:
}
if err != nil && !errors.Is(err, ErrIdle) && !errors.Is(err, ErrLeaseLost) {
return err
}
if err == nil {
continue
}
timer := time.NewTimer(options.PollInterval)
select {
case <-parent.Done():
timer.Stop()
case <-timer.C:
}
}
return nil
}