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,99 @@
package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"sync/atomic"
"time"
)
// component 不接管进程信号;宿主负责为每一类生命周期提供独立取消和等待。
type component struct {
name string
run func(context.Context) error
}
type completion struct {
name string
err error
}
// serve 在HTTP监听已成功后启动后台组件;返回前完成HTTP→发送/执行→结果消费的关闭顺序。
func serve(parent context.Context, listener net.Listener, server *http.Server, ready *atomic.Bool, workers []component, consumer component, grace time.Duration) error {
workCtx, stopWork := context.WithCancel(context.Background())
defer stopWork()
eventCtx, stopEvents := context.WithCancel(context.Background())
defer stopEvents()
workDone := make(chan completion, len(workers))
eventDone := make(chan completion, 1)
for _, c := range workers {
go func() { workDone <- completion{c.name, c.run(workCtx)} }()
}
go func() { eventDone <- completion{consumer.name, consumer.run(eventCtx)} }()
httpDone := make(chan error, 1)
ready.Store(true)
go func() { httpDone <- server.Serve(listener) }()
remaining := len(workers)
eventsPending := true
var result error
add := func(c completion) {
if c.err != nil {
result = errors.Join(result, fmt.Errorf("%s: %w", c.name, c.err))
}
}
select {
case <-parent.Done():
case err := <-httpDone:
if !errors.Is(err, http.ErrServerClosed) {
result = errors.Join(result, err)
}
case c := <-workDone:
remaining--
if c.err == nil {
c.err = errors.New("unexpected component exit")
}
add(c)
case c := <-eventDone:
eventsPending = false
if c.err == nil {
c.err = errors.New("unexpected component exit")
}
add(c)
}
ready.Store(false)
// 用新的关闭上下文;进程信号的context此时可能已取消,不能用它调用Shutdown。
shut, cancel := context.WithTimeout(context.Background(), grace)
if err := server.Shutdown(shut); err != nil {
result = errors.Join(result, fmt.Errorf("http shutdown: %w", err))
_ = server.Close()
}
cancel()
stopWork()
deadline := time.NewTimer(grace)
defer deadline.Stop()
for remaining > 0 {
select {
case c := <-workDone:
remaining--
add(c)
case <-deadline.C:
result = errors.Join(result, errors.New("background shutdown deadline exceeded"))
remaining = 0
}
}
// 结果消费器最后停止。仍未投递的结果会在重启后由持久订阅恢复,不以本地空闲判断全局完成。
stopEvents()
if eventsPending {
timer := time.NewTimer(grace)
defer timer.Stop()
select {
case c := <-eventDone:
add(c)
case <-timer.C:
result = errors.Join(result, errors.New("consumer shutdown deadline exceeded"))
}
}
return result
}