222 lines
6.2 KiB
Go
222 lines
6.2 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/common/config"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/startup"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type bootstrapContextKey struct{}
|
|
|
|
type bootstrapRuntime struct {
|
|
handler *handlerSwitch
|
|
server *http.Server
|
|
listeners *serverListeners
|
|
ready atomic.Bool
|
|
readinessMu sync.Mutex
|
|
readinessPublishing bool
|
|
startupClosing bool
|
|
}
|
|
|
|
type startupSignalContextKey struct{}
|
|
|
|
type processContextKey struct{}
|
|
|
|
type workerCancelContextKey struct{}
|
|
|
|
type startupSignalController struct {
|
|
first chan os.Signal
|
|
stop chan struct{}
|
|
done chan struct{}
|
|
signals chan os.Signal
|
|
once sync.Once
|
|
}
|
|
|
|
func newStartupSignalController(ctx context.Context, cancel context.CancelFunc, log *logger.Logger) *startupSignalController {
|
|
controller := &startupSignalController{
|
|
first: make(chan os.Signal, 1),
|
|
stop: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
signals: make(chan os.Signal, 2),
|
|
}
|
|
signal.Notify(controller.signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
if log != nil {
|
|
log.Debug("shutdown signal handler armed",
|
|
zap.Int("pid", os.Getpid()),
|
|
zap.Int("ppid", os.Getppid()))
|
|
}
|
|
go func() {
|
|
defer close(controller.done)
|
|
select {
|
|
case sig := <-controller.signals:
|
|
controller.first <- sig
|
|
cancel()
|
|
select {
|
|
case second := <-controller.signals:
|
|
if log != nil {
|
|
log.Warn("Received second shutdown signal, forcing exit", zap.String("signal", second.String()))
|
|
_ = log.Close()
|
|
}
|
|
os.Exit(1)
|
|
case <-controller.stop:
|
|
}
|
|
case <-ctx.Done():
|
|
case <-controller.stop:
|
|
}
|
|
}()
|
|
return controller
|
|
}
|
|
|
|
func withStartupSignalController(ctx context.Context, controller *startupSignalController) context.Context {
|
|
return context.WithValue(ctx, startupSignalContextKey{}, controller)
|
|
}
|
|
|
|
func withProcessContext(ctx, process context.Context) context.Context {
|
|
return context.WithValue(ctx, processContextKey{}, process)
|
|
}
|
|
|
|
func processContextFromContext(ctx context.Context) context.Context {
|
|
process, _ := ctx.Value(processContextKey{}).(context.Context)
|
|
return process
|
|
}
|
|
|
|
// processRuntimeContext returns the process lifetime carried by startup
|
|
// workers. Restore cancels workers while the process remains alive to finish
|
|
// the restore and deliver its terminal job update.
|
|
func processRuntimeContext(ctx context.Context) context.Context {
|
|
if process := processContextFromContext(ctx); process != nil {
|
|
return process
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
// beginReadinessPublication serializes the readiness handoff with the startup
|
|
// cancellation watcher. Once startup cancellation claims listener closure,
|
|
// initialization cannot publish a router into a closing server.
|
|
func (runtime *bootstrapRuntime) beginReadinessPublication(ctx context.Context) bool {
|
|
runtime.readinessMu.Lock()
|
|
defer runtime.readinessMu.Unlock()
|
|
if runtime.startupClosing || ctx.Err() != nil {
|
|
return false
|
|
}
|
|
runtime.readinessPublishing = true
|
|
return true
|
|
}
|
|
|
|
func (runtime *bootstrapRuntime) claimStartupClose() bool {
|
|
runtime.readinessMu.Lock()
|
|
defer runtime.readinessMu.Unlock()
|
|
if runtime.ready.Load() || runtime.readinessPublishing {
|
|
return false
|
|
}
|
|
runtime.startupClosing = true
|
|
return true
|
|
}
|
|
|
|
func withWorkerCancel(ctx context.Context, cancel context.CancelFunc) context.Context {
|
|
return context.WithValue(ctx, workerCancelContextKey{}, cancel)
|
|
}
|
|
|
|
func workerCancelFromContext(ctx context.Context) context.CancelFunc {
|
|
cancel, _ := ctx.Value(workerCancelContextKey{}).(context.CancelFunc)
|
|
return cancel
|
|
}
|
|
|
|
func startupSignalControllerFromContext(ctx context.Context) *startupSignalController {
|
|
controller, _ := ctx.Value(startupSignalContextKey{}).(*startupSignalController)
|
|
return controller
|
|
}
|
|
|
|
func (controller *startupSignalController) wait(ctx context.Context) os.Signal {
|
|
select {
|
|
case sig := <-controller.first:
|
|
return sig
|
|
default:
|
|
}
|
|
select {
|
|
case sig := <-controller.first:
|
|
return sig
|
|
case <-ctx.Done():
|
|
select {
|
|
case sig := <-controller.first:
|
|
return sig
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (controller *startupSignalController) stopAndWait() {
|
|
if controller == nil {
|
|
return
|
|
}
|
|
controller.once.Do(func() {
|
|
signal.Stop(controller.signals)
|
|
close(controller.stop)
|
|
<-controller.done
|
|
})
|
|
}
|
|
|
|
// runWithBootstrap owns the listener for both initialization and application use.
|
|
// Constructors run synchronously so resource cleanup cannot race initialization.
|
|
// The process context remains live while worker contexts are quiesced, which
|
|
// lets restore finish and deliver its job result before explicit shutdown.
|
|
func runWithBootstrap(ctx context.Context, cfg *config.Config, log *logger.Logger, initialize func(context.Context) bool) bool {
|
|
startupCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
controller := newStartupSignalController(startupCtx, cancel, log)
|
|
defer controller.stopAndWait()
|
|
if err := startupCtx.Err(); err != nil {
|
|
return false
|
|
}
|
|
|
|
progress := startup.New(log)
|
|
startupCtx = startup.WithReporter(startupCtx, progress)
|
|
startupCtx = withStartupSignalController(startupCtx, controller)
|
|
workerCtx, cancelWorkers := context.WithCancel(startupCtx)
|
|
defer cancelWorkers()
|
|
workerCtx = withProcessContext(workerCtx, startupCtx)
|
|
workerCtx = withWorkerCancel(workerCtx, cancelWorkers)
|
|
ready.Store(false)
|
|
handler, server, listeners, err := bindBootstrapListeners(cfg, log, Version, progress)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer closeBoundListeners(server, listeners, log)
|
|
runtime := &bootstrapRuntime{handler: handler, server: server, listeners: listeners}
|
|
done := make(chan struct{})
|
|
stopped := make(chan struct{})
|
|
go func() {
|
|
defer close(stopped)
|
|
ticker := time.NewTicker(15 * time.Second)
|
|
defer ticker.Stop()
|
|
progress.Report()
|
|
for {
|
|
select {
|
|
case <-startupCtx.Done():
|
|
if runtime.claimStartupClose() {
|
|
closeBoundListeners(server, listeners, log)
|
|
}
|
|
return
|
|
case <-done:
|
|
return
|
|
case <-ticker.C:
|
|
progress.Report()
|
|
}
|
|
}
|
|
}()
|
|
defer func() { close(done); <-stopped }()
|
|
ok := initialize(context.WithValue(workerCtx, bootstrapContextKey{}, runtime))
|
|
return ok && (startupCtx.Err() == nil || runtime.ready.Load())
|
|
}
|