121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
// Package daemon implements the orca HTTP daemon.
|
|
//
|
|
// The daemon exposes health endpoints (/healthz, /readyz), a status endpoint
|
|
// (/v1/status), and a v1 resource API for jobs, nodes, and tasks. All handlers
|
|
// follow the project conventions:
|
|
//
|
|
// - context.Context propagated to all I/O
|
|
// - errors wrapped with %w
|
|
// - structured JSON via writeJSON
|
|
// - no secrets in logs
|
|
// - input validation on path/query/body
|
|
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Server is the orca HTTP daemon. It holds shared dependencies and lifecycle
|
|
// state. Construct it with NewServer, then call Start/Shutdown.
|
|
type Server struct {
|
|
db *sql.DB
|
|
log *slog.Logger
|
|
addr string
|
|
ready atomic.Bool
|
|
|
|
httpServer *http.Server
|
|
}
|
|
|
|
// Options configures a new Server.
|
|
type Options struct {
|
|
DB *sql.DB
|
|
Log *slog.Logger
|
|
Addr string
|
|
Actor string // used for audit logging from API requests
|
|
}
|
|
|
|
// NewServer constructs a Server with the default mux and route table.
|
|
func NewServer(opts Options) *Server {
|
|
if opts.Log == nil {
|
|
opts.Log = slog.Default()
|
|
}
|
|
if opts.Addr == "" {
|
|
opts.Addr = ":8080"
|
|
}
|
|
if opts.Actor == "" {
|
|
opts.Actor = "api"
|
|
}
|
|
s := &Server{
|
|
db: opts.DB,
|
|
log: opts.Log,
|
|
addr: opts.Addr,
|
|
}
|
|
s.httpServer = &http.Server{
|
|
Addr: opts.Addr,
|
|
Handler: s.mux(),
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Addr returns the configured listen address.
|
|
func (s *Server) Addr() string { return s.addr }
|
|
|
|
// MarkReady flips the readiness flag to true. The /readyz endpoint returns
|
|
// 200 only when this flag is set AND the database is reachable.
|
|
func (s *Server) MarkReady() { s.ready.Store(true) }
|
|
|
|
// MarkNotReady flips the readiness flag to false. Called at shutdown start
|
|
// so load balancers stop routing traffic.
|
|
func (s *Server) MarkNotReady() { s.ready.Store(false) }
|
|
|
|
// Ready reports the current readiness flag.
|
|
func (s *Server) Ready() bool { return s.ready.Load() }
|
|
|
|
// mux builds the route table. Handlers are split across files:
|
|
// - health.go /healthz, /readyz, /v1/status
|
|
// - jobs_handler.go /v1/jobs/*
|
|
// - nodes_handler.go /v1/nodes/*
|
|
// - tasks_handler.go /v1/tasks/*
|
|
func (s *Server) mux() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz", s.handleHealthz)
|
|
mux.HandleFunc("/readyz", s.handleReadyz)
|
|
mux.HandleFunc("/v1/status", s.handleStatus)
|
|
mux.HandleFunc("/v1/jobs", s.handleJobsCollection)
|
|
mux.HandleFunc("/v1/jobs/", s.handleJobsItem)
|
|
mux.HandleFunc("/v1/nodes", s.handleNodesCollection)
|
|
mux.HandleFunc("/v1/tasks", s.handleTasksCollection)
|
|
return loggingMiddleware(s.log, mux)
|
|
}
|
|
|
|
// Start runs the HTTP server. Returns http.ErrServerClosed on clean shutdown.
|
|
func (s *Server) Start() error {
|
|
s.log.Info("daemon starting",
|
|
slog.String("addr", s.addr),
|
|
slog.String("component", "daemon"))
|
|
return s.httpServer.ListenAndServe()
|
|
}
|
|
|
|
// Shutdown gracefully stops the server, bounded by ctx. It also flips the
|
|
// readiness flag to false so /readyz returns 503 immediately.
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
s.MarkNotReady()
|
|
s.log.Info("daemon shutting down", slog.String("component", "daemon"))
|
|
return s.httpServer.Shutdown(ctx)
|
|
}
|
|
|
|
// IsShutdownErr reports whether err is the expected error from a stopped server.
|
|
func IsShutdownErr(err error) bool {
|
|
return errors.Is(err, http.ErrServerClosed)
|
|
}
|