package httpserver import ( "context" "net" "net/http" "sync" "time" "github.com/toki/oto/services/core/internal/cicdstate" "github.com/toki/oto/services/core/internal/runnerregistry" ) const ( DefaultHeartbeatTimeout = 90 * time.Second DefaultScanInterval = 30 * time.Second ) // ServerConfig holds timeout loop configuration for the server's runner maintenance loop. type ServerConfig struct { HeartbeatTimeout time.Duration ScanInterval time.Duration } // Server wraps the HTTP server for the OTO Core service. type Server struct { httpServer *http.Server registry *runnerregistry.Registry heartbeatTimeout time.Duration scanInterval time.Duration mu sync.Mutex loopCancel context.CancelFunc loopDone <-chan struct{} startOnce sync.Once } // NewServer creates a new instance of Server. func NewServer(addr string) *Server { return newServer(addr, runnerregistry.New(), cicdstate.NewStore(), DefaultHeartbeatTimeout, DefaultScanInterval) } // NewServerWithRegistry creates a server using an injected runner registry. func NewServerWithRegistry(addr string, registry *runnerregistry.Registry) *Server { return newServer(addr, registry, cicdstate.NewStore(), DefaultHeartbeatTimeout, DefaultScanInterval) } // NewServerWithRegistryAndStore creates a server with injected runner registry and CICD store. func NewServerWithRegistryAndStore(addr string, registry *runnerregistry.Registry, store *cicdstate.Store) *Server { return newServer(addr, registry, store, DefaultHeartbeatTimeout, DefaultScanInterval) } // NewServerWithConfig creates a server with custom timeout loop configuration. func NewServerWithConfig(addr string, registry *runnerregistry.Registry, store *cicdstate.Store, cfg ServerConfig) *Server { return newServer(addr, registry, store, cfg.HeartbeatTimeout, cfg.ScanInterval) } func newServer(addr string, registry *runnerregistry.Registry, store *cicdstate.Store, heartbeatTimeout, scanInterval time.Duration) *Server { mux := http.NewServeMux() registerRoutes(mux, registry, store) return &Server{ httpServer: &http.Server{ Addr: addr, Handler: mux, }, registry: registry, heartbeatTimeout: heartbeatTimeout, scanInterval: scanInterval, } } func (s *Server) startTimeoutLoop() { ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) s.mu.Lock() s.loopCancel = cancel s.loopDone = done s.mu.Unlock() go func() { defer close(done) if s.registry == nil || s.scanInterval <= 0 { return } ticker := time.NewTicker(s.scanInterval) defer ticker.Stop() for { select { case <-ticker.C: s.registry.CheckTimeouts(s.heartbeatTimeout) case <-ctx.Done(): return } } }() } func (s *Server) stopTimeoutLoop() { s.mu.Lock() cancel := s.loopCancel done := s.loopDone s.mu.Unlock() if cancel != nil { cancel() <-done } } // Start starts the HTTP server. func (s *Server) Start() error { s.startOnce.Do(s.startTimeoutLoop) err := s.httpServer.ListenAndServe() s.stopTimeoutLoop() return err } // StartListener starts the HTTP server using a custom net.Listener. // Useful for testing with dynamic ports. func (s *Server) StartListener(ln net.Listener) error { s.startOnce.Do(s.startTimeoutLoop) err := s.httpServer.Serve(ln) s.stopTimeoutLoop() return err } // Shutdown gracefully shuts down the server. func (s *Server) Shutdown(ctx context.Context) error { s.stopTimeoutLoop() return s.httpServer.Shutdown(ctx) }