feat: add reusable MySQL outbox and HTTP lifecycle example
This commit is contained in:
99
examples/http-service/cmd/server/lifecycle.go
Normal file
99
examples/http-service/cmd/server/lifecycle.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user