Files
silo-server/internal/server/server.go
T

104 lines
2.7 KiB
Go

// Package server provides HTTP server lifecycle management with graceful
// shutdown support for Silo.
package server
import (
"context"
"fmt"
"net"
"net/http"
"sync"
"github.com/Silo-Server/silo-server/internal/config"
)
// Server wraps an http.Server with lifecycle management and graceful shutdown.
type Server struct {
httpServer *http.Server
config *config.Config
listener net.Listener
stopCh chan struct{}
doneCh chan struct{}
once sync.Once
}
// New creates a new Server with the given configuration and HTTP handler.
func New(cfg *config.Config, handler http.Handler) *Server {
return &Server{
httpServer: &http.Server{
Addr: cfg.Server.Listen,
Handler: handler,
},
config: cfg,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}
// Start begins serving HTTP requests. It binds the listener and starts
// serving in a background goroutine, returning immediately. Use Shutdown
// to stop the server gracefully.
func (s *Server) Start() error {
ln, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return fmt.Errorf("listening on %s: %w", s.httpServer.Addr, err)
}
s.listener = ln
go func() {
defer close(s.doneCh)
if err := s.httpServer.Serve(ln); err != nil && err != http.ErrServerClosed {
// Log unexpected serve errors; ErrServerClosed is expected during
// graceful shutdown.
fmt.Printf("server: unexpected serve error: %v\n", err)
}
}()
return nil
}
// Shutdown performs graceful shutdown in the ordered sequence:
// 1. Stop accepting new requests
// 2. Wait for active requests to complete (with timeout from context)
// 3. Signal all workers to stop via stopCh
// 4. Close
func (s *Server) Shutdown(ctx context.Context) error {
var shutdownErr error
s.once.Do(func() {
// Step 1 & 2: http.Server.Shutdown stops accepting new connections
// and waits for active requests to complete.
shutdownErr = s.httpServer.Shutdown(ctx)
// Step 3: Signal workers to stop.
close(s.stopCh)
})
// Step 4: Wait for the serve goroutine to exit.
select {
case <-s.doneCh:
case <-ctx.Done():
if shutdownErr == nil {
shutdownErr = ctx.Err()
}
}
return shutdownErr
}
// ListenAddr returns the actual address the server is listening on. This is
// useful when the server was configured with ":0" to get an ephemeral port.
// Returns an empty string if the server has not been started.
func (s *Server) ListenAddr() string {
if s.listener == nil {
return ""
}
return s.listener.Addr().String()
}
// StopCh returns the channel that is closed when the server begins shutting
// down. Workers can select on this channel to know when to stop.
func (s *Server) StopCh() <-chan struct{} {
return s.stopCh
}