check / check (push) Canceled after 0s
The start hook registered its own handler for SIGINT and SIGTERM and asked fx to shut down from it, although fx already stops the daemon on both signals. A stop signal was handled twice, and the handler stayed registered after the stop. fx now handles both signals alone, as GO_HTTP_SERVER_CONVENTIONS.md "Signal Handling and Graceful Shutdown" requires. Model: opus-5-5
113 lines
2.9 KiB
Go
113 lines
2.9 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
const (
|
|
// shutdownTimeout is how long Shutdown has after a stop signal. upaas,
|
|
// like docker stop by default, kills the container 10 seconds after the
|
|
// stop signal (README "Running under upaas").
|
|
shutdownTimeout = 8 * time.Second
|
|
// stopTimeout is fx's limit on the stop hook, past which fx exits with
|
|
// status 1: shutdownTimeout and time for Shutdown to close the HTTP
|
|
// server after its deadline.
|
|
stopTimeout = 9 * time.Second
|
|
// debugInterval is how often to log debug stats
|
|
debugInterval = 60 * time.Second
|
|
// bytesPerMB is bytes per megabyte
|
|
bytesPerMB = 1024 * 1024
|
|
// failureExitCode is the exit status when RouteWatch stops on an error
|
|
failureExitCode = 1
|
|
)
|
|
|
|
// logDebugStats logs goroutine count and memory usage
|
|
func logDebugStats(logger *logger.Logger) {
|
|
// Only run if DEBUG env var contains "routewatch"
|
|
debugEnv := os.Getenv("DEBUG")
|
|
if !strings.Contains(debugEnv, "routewatch") {
|
|
return
|
|
}
|
|
|
|
ticker := time.NewTicker(debugInterval)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
|
|
logger.Debug("System stats",
|
|
"goroutines", runtime.NumGoroutine(),
|
|
"alloc_mb", m.Alloc/bytesPerMB,
|
|
"total_alloc_mb", m.TotalAlloc/bytesPerMB,
|
|
"sys_mb", m.Sys/bytesPerMB,
|
|
"num_gc", m.NumGC,
|
|
"heap_alloc_mb", m.HeapAlloc/bytesPerMB,
|
|
"heap_sys_mb", m.HeapSys/bytesPerMB,
|
|
"heap_idle_mb", m.HeapIdle/bytesPerMB,
|
|
"heap_inuse_mb", m.HeapInuse/bytesPerMB,
|
|
"heap_released_mb", m.HeapReleased/bytesPerMB,
|
|
"stack_inuse_mb", m.StackInuse/bytesPerMB,
|
|
)
|
|
}
|
|
}
|
|
|
|
// CLIEntry is the main entry point for the routewatch command-line interface.
|
|
// It initializes the application using the fx dependency injection framework
|
|
// and starts the RouteWatch service. It blocks until fx receives SIGINT or
|
|
// SIGTERM or RouteWatch stops on an error; fx then runs the stop hook and sets
|
|
// the exit code.
|
|
func CLIEntry() {
|
|
app := fx.New(
|
|
fx.Options(getModule()...),
|
|
fx.StopTimeout(stopTimeout),
|
|
fx.Invoke(func(
|
|
lc fx.Lifecycle,
|
|
rw *RouteWatch,
|
|
logger *logger.Logger,
|
|
shutdowner fx.Shutdowner,
|
|
) {
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
// Start debug stats logging
|
|
go logDebugStats(logger)
|
|
|
|
go func() {
|
|
err := rw.Run(ctx)
|
|
if err != nil {
|
|
logger.Error("RouteWatch error", "error", err)
|
|
|
|
err = shutdowner.Shutdown(fx.ExitCode(failureExitCode))
|
|
if err != nil {
|
|
logger.Error("Failed to shutdown gracefully",
|
|
"error", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
logger.Info("Shutting down RouteWatch")
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, shutdownTimeout)
|
|
defer cancel()
|
|
|
|
rw.Shutdown(ctx)
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}),
|
|
)
|
|
|
|
app.Run()
|
|
}
|