Files
dance-lessons-coach/cmd/server/main.go
Gabriel Radureau f986711974 Add readiness endpoint for graceful shutdown coordination
Implement readiness endpoint (/api/ready) that returns:
- {"ready":true} (HTTP 200) during normal operation
- {"ready":false} (HTTP 503) during graceful shutdown

Key changes:
- Added readiness context to control readiness state
- Modified server.NewServer() to accept readiness context
- Implemented handleReadiness() with context-aware logic
- Updated cmd/server/main.go to manage readiness state
- Readiness set to false when shutdown signal received
- Updated test script to validate readiness behavior
- Added comprehensive documentation for readiness endpoint

This allows Kubernetes/service meshes to stop routing traffic
to the pod during graceful shutdown while allowing existing
requests to complete. Health endpoint continues to return
happy status during shutdown for proper orchestration.
2026-04-03 19:53:14 +02:00

126 lines
3.4 KiB
Go

package main
import (
"context"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"DanceLessonsCoach/pkg/config"
"DanceLessonsCoach/pkg/server"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Initialize Zerolog with default console format first
zerolog.SetGlobalLevel(zerolog.TraceLevel)
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
// Check if JSON logging is requested via environment variable
// This allows JSON logging even during config loading
jsonLogging := os.Getenv("DLC_LOGGING_JSON") == "true"
if jsonLogging {
// JSON output for structured logging
log.Logger = log.Output(os.Stderr)
} else {
// Console output for initial logging
log.Logger = log.Output(consoleWriter)
}
// Load configuration
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal().Err(err).Msg("Failed to load configuration")
}
// Reconfigure logging based on loaded configuration (overrides env var)
if cfg.Logging.JSON {
// JSON output for structured logging
log.Logger = log.Output(os.Stderr)
} else {
// Keep console output
log.Logger = log.Output(consoleWriter)
}
log.Info().Bool("json_logging", cfg.Logging.JSON).Msg("Logging configured")
// Setup signal context for graceful shutdown
rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Create root context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create ongoing context for active requests
ongoingCtx, stopOngoingGracefully := context.WithCancel(context.Background())
// Create readiness context to control readiness state
readyCtx, readyCancel := context.WithCancel(context.Background())
defer readyCancel()
// Start server in goroutine
server := server.NewServer(cfg, readyCtx)
serverCtx, serverStop := context.WithCancel(ctx)
go func() {
log.Info().Str("address", cfg.GetServerAddress()).Msg("Server running")
srv := &http.Server{
Addr: cfg.GetServerAddress(),
Handler: server.Router(),
BaseContext: func(_ net.Listener) context.Context {
return ongoingCtx
},
}
// Start the HTTP server in a separate goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error().Err(err).Msg("Server error")
}
}()
// Wait for signal
<-rootCtx.Done()
stop()
log.Info().Msg("Shutdown signal received")
// Cancel readiness context to stop accepting new requests
readyCancel()
log.Info().Msg("Readiness set to false, no longer accepting new requests")
// Give time for readiness check to propagate (simplified for our case)
time.Sleep(1 * time.Second)
log.Info().Msg("Readiness check propagated, now waiting for ongoing requests to finish.")
// Create shutdown context with timeout from config
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.Shutdown.Timeout)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Error().Err(err).Msg("Server shutdown failed")
} else {
log.Info().Msg("Server shutdown complete")
}
// Stop ongoing requests context
stopOngoingGracefully()
cancel()
serverStop()
log.Info().Msg("Server exited")
// Force log flush by writing to stderr directly
// This ensures logs are written before process exits
time.Sleep(100 * time.Millisecond)
}()
// Wait for shutdown
<-serverCtx.Done()
}