feat: polish & windviz & deploy

This commit is contained in:
Anatoly Antonov 2026-05-30 06:29:39 +09:00
parent 81b8e763bd
commit 465ad00f7b
78 changed files with 20622 additions and 2154 deletions

View file

@ -19,7 +19,6 @@ import (
"go.uber.org/zap/zapcore"
"predictor-refactored/internal/api"
"predictor-refactored/internal/api/async"
"predictor-refactored/internal/config"
"predictor-refactored/internal/datasets"
"predictor-refactored/internal/datasets/gefs"
@ -27,15 +26,65 @@ import (
"predictor-refactored/internal/elevation"
"predictor-refactored/internal/metrics"
wgfs "predictor-refactored/internal/weather/gfs"
"predictor-refactored/internal/windviz"
)
// Build metadata, injected via -ldflags at build time (see Dockerfile).
var (
version = "dev"
revision = "unknown"
)
func main() {
// `predictor -healthcheck` probes the local /health endpoint and exits
// 0/1. The container HEALTHCHECK uses it so the (distroless) image needs
// no shell or curl.
for _, a := range os.Args[1:] {
if a == "-healthcheck" || a == "--healthcheck" {
os.Exit(healthcheck())
}
}
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "fatal:", err)
os.Exit(1)
}
}
// healthcheck performs a liveness probe against the local server. It resolves
// the port through the same config loader as the server, so the probe always
// matches the bind port regardless of how it was set (flag, env, or file).
func healthcheck() int {
port := 8080
if cfg, err := config.Load(withoutHealthcheckFlag(os.Args[1:])); err == nil {
port = cfg.HTTP.Port
}
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/health", port))
if err != nil {
fmt.Fprintln(os.Stderr, "healthcheck:", err)
return 1
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Fprintln(os.Stderr, "healthcheck: status", resp.StatusCode)
return 1
}
return 0
}
// withoutHealthcheckFlag drops the -healthcheck flag so the remaining args
// parse cleanly through config.Load (which does not define it).
func withoutHealthcheckFlag(args []string) []string {
out := make([]string, 0, len(args))
for _, a := range args {
if a == "-healthcheck" || a == "--healthcheck" {
continue
}
out = append(out, a)
}
return out
}
func run(args []string) error {
cfg, err := config.Load(args)
if err != nil {
@ -48,6 +97,10 @@ func run(args []string) error {
}
defer log.Sync()
log.Info("starting stratoflights-predictor",
zap.String("version", version),
zap.String("revision", revision))
log.Info("configuration loaded",
zap.Int("port", cfg.HTTP.Port),
zap.String("data_dir", cfg.Data.Dir),
@ -141,12 +194,10 @@ func run(args []string) error {
scheduler.StartAsync()
defer scheduler.Stop()
asyncMgr := async.New(async.Config{
Workers: cfg.HTTP.AsyncWorkers,
QueueSize: cfg.HTTP.AsyncQueueSize,
ResultTTL: cfg.HTTP.AsyncResultTTL,
}, mgr, elev, sink, log)
defer asyncMgr.Close()
var windCache *windviz.Cache
if cfg.Wind.Enabled {
windCache = windviz.NewCache(cfg.Wind.CacheSize, cfg.Wind.CacheTTL)
}
server, err := api.New(cfg.HTTP.Port, api.Deps{
Manager: mgr,
@ -154,12 +205,17 @@ func run(args []string) error {
Metrics: sink,
MetricsHandler: metricsHandler,
MetricsPath: cfg.Metrics.Path,
AsyncManager: asyncMgr,
EnableWind: cfg.Wind.Enabled,
WindCache: windCache,
AsyncWorkers: cfg.HTTP.AsyncWorkers,
AsyncQueueSize: cfg.HTTP.AsyncQueueSize,
AsyncResultTTL: cfg.HTTP.AsyncResultTTL,
Log: log,
})
if err != nil {
return fmt.Errorf("init server: %w", err)
}
defer server.Close()
// Graceful shutdown
ctx, cancel := signalContext()