100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
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
|
||
}
|