Seed from go-template-repo, renamed to simplexcalc

The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 21:38:57 +00:00
parent e336f46e34
commit f8ce8cef83
79 changed files with 7131 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
// Package server owns the HTTP server lifecycle and the route table.
package server
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"go.uber.org/fx"
"sneak.berlin/go/simplexcalc/internal/config"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/internal/handlers"
"sneak.berlin/go/simplexcalc/internal/logger"
"sneak.berlin/go/simplexcalc/internal/middleware"
"sneak.berlin/go/simplexcalc/internal/telemetry"
)
// Params defines dependencies for Server.
type Params struct {
fx.In
Logger *logger.Logger
Globals *globals.Globals
Config *config.Config
Middleware *middleware.Middleware
Handlers *handlers.Handlers
Metrics *telemetry.Metrics
}
// Server is the HTTP server and its lifecycle state.
type Server struct {
params Params
log *slog.Logger
mw *middleware.Middleware
h *handlers.Handlers
metrics *telemetry.Metrics
httpServer *http.Server
listener net.Listener
router *chi.Mux
serveErr chan error
}
// New creates the Server and hooks it into the fx lifecycle.
//
// The listener is opened during OnStart, not in a goroutine after it:
// binding is the part that fails (port in use, permission denied), and
// a failure there has to be a failure to start. A server that logs
// "listen failed" from a goroutine while fx reports a successful
// startup is a process that is up and serving nothing.
func New(lc fx.Lifecycle, params Params) (*Server, error) {
s := &Server{
params: params,
log: params.Logger.Get(),
mw: params.Middleware,
h: params.Handlers,
metrics: params.Metrics,
serveErr: make(chan error, 1),
}
lc.Append(fx.Hook{
OnStart: s.start,
OnStop: s.stop,
})
return s, nil
}
// Addr returns the address actually bound, which is what a test that
// asked for port 0 needs.
func (s *Server) Addr() string {
if s.listener == nil {
return ""
}
return s.listener.Addr().String()
}
// Handler exposes the router so tests can drive it with
// httptest.NewRecorder without binding a port.
func (s *Server) Handler() http.Handler {
return s.router
}
// ServeHTTP dispatches requests through the router.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
// start builds the routes, binds, and begins serving.
func (s *Server) start(ctx context.Context) error {
s.SetupRoutes()
addr := ":" + strconv.Itoa(s.params.Config.Port)
s.httpServer = s.newHTTPServer(addr)
// ListenConfig rather than net.Listen: the start context bounds
// name resolution and the bind, so a start that cannot bind fails
// within fx's start timeout instead of hanging inside it.
var lc net.ListenConfig
ln, err := lc.Listen(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("listening on %s: %w", addr, err)
}
s.listener = ln
s.log.Info("http listening",
"addr", ln.Addr().String(),
"debug", s.params.Config.Debug,
"metrics_protected", s.metrics.AuthRequired(),
)
go func() {
serveErr := s.httpServer.Serve(ln)
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
s.log.Error("http serve failed", "error", serveErr)
s.serveErr <- serveErr
return
}
s.serveErr <- nil
}()
return nil
}
// stop drains in-flight requests, bounded by the stop context.
//
// The bound is whichever of the two comes first: the deadline fx gives
// this hook, or the configured grace period. Taking the minimum is the
// point — a shutdown that outlives the context it was given is a
// shutdown the supervisor kills with SIGKILL, which is precisely the
// abrupt termination the draining was supposed to avoid.
//
// Past the deadline, Shutdown returns and every remaining connection is
// closed. That is a deliberate choice of a bounded, reported failure
// over an unbounded wait.
func (s *Server) stop(ctx context.Context) error {
if s.httpServer == nil {
return nil
}
grace := s.params.Config.ShutdownGrace
stopCtx, cancel := context.WithTimeout(ctx, grace)
defer cancel()
s.log.Info("http shutting down", "grace", grace.String())
start := time.Now()
err := s.httpServer.Shutdown(stopCtx)
if err != nil {
// Connections were still open at the deadline. Report it —
// silence here would hide requests that were cut off — but do
// not fail the stop sequence: there is nothing left to retry
// and the rest of the graph still has to close cleanly.
s.log.Error("http shutdown did not drain in time",
"error", err,
"waited", time.Since(start).Round(time.Millisecond).String(),
)
return nil
}
s.log.Info("http shutdown complete",
"waited", time.Since(start).Round(time.Millisecond).String(),
)
return nil
}