package launcher import ( "context" "os" "os/signal" "sync" "syscall" "time" "code.nochebuena.dev/go/logz" ) // Hook is a function executed during the assembly phase (between OnInit and OnStart). // Use hooks for dependency injection wiring that requires all components to be // initialized before connections are established. type Hook func() error // Component is the lifecycle interface implemented by all managed infrastructure components. type Component interface { // OnInit initializes the component (open connections, allocate resources). OnInit() error // OnStart starts background services (goroutines, listeners). OnStart() error // OnStop stops the component and releases all resources. OnStop() error } // Options configures a Launcher instance. // The zero value is valid: 15-second component stop timeout. type Options struct { // ComponentStopTimeout is the maximum time allowed for each component's OnStop. // Default: 15 seconds. ComponentStopTimeout time.Duration } // Launcher manages the application lifecycle: init → assemble → start → wait → shutdown. type Launcher interface { // Append adds one or more components. Registered in the order they are appended; // shutdown runs in reverse order. Append(components ...Component) // BeforeStart registers hooks that run after all OnInit calls and before all OnStart // calls. Use for dependency injection wiring. BeforeStart(hooks ...Hook) // Run executes the full application lifecycle. Blocks until an OS shutdown signal is // received or Shutdown is called. Returns an error if any lifecycle step fails. // The caller is responsible for calling os.Exit(1) when needed. Run() error // Shutdown triggers a graceful shutdown and waits for Run to return. // ctx controls the caller-side wait timeout — it does NOT override // Options.ComponentStopTimeout for individual components. // Safe to call multiple times (idempotent). Shutdown(ctx context.Context) error } const defaultComponentStopTimeout = 15 * time.Second // launcher is the concrete implementation of Launcher. type launcher struct { logger logz.Logger opts Options components []Component beforeStart []Hook shutdownCh chan struct{} doneCh chan struct{} shutdownOnce sync.Once } // New returns a Launcher configured by opts. The zero value of Options is valid. func New(logger logz.Logger, opts ...Options) Launcher { o := Options{ComponentStopTimeout: defaultComponentStopTimeout} if len(opts) > 0 { if opts[0].ComponentStopTimeout > 0 { o.ComponentStopTimeout = opts[0].ComponentStopTimeout } } return &launcher{ logger: logger, opts: o, components: make([]Component, 0), shutdownCh: make(chan struct{}), doneCh: make(chan struct{}), } } // Append adds components to the launcher's registry. func (l *launcher) Append(components ...Component) { l.components = append(l.components, components...) } // BeforeStart registers hooks to be executed before the OnStart phase. func (l *launcher) BeforeStart(hooks ...Hook) { l.beforeStart = append(l.beforeStart, hooks...) } // Run executes the full application lifecycle: // 1. OnInit for all components (in registration order). // 2. BeforeStart hooks (in registration order). // 3. OnStart for all components (in registration order). // 4. Blocks until an OS signal or Shutdown() is called. // 5. stopAll — OnStop for all components (in reverse order). func (l *launcher) Run() error { defer close(l.doneCh) l.logger.Info("launcher: starting init phase (OnInit)") for _, c := range l.components { if err := c.OnInit(); err != nil { return err } } l.logger.Info("launcher: running assembly hooks (BeforeStart)") for _, hook := range l.beforeStart { if err := hook(); err != nil { return err } } l.logger.Info("launcher: starting components (OnStart)") for _, c := range l.components { if err := c.OnStart(); err != nil { l.logger.Error("launcher: OnStart failed, triggering shutdown", err) l.stopAll() return err } } l.logger.Info("launcher: application ready") quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) defer signal.Stop(quit) select { case s := <-quit: l.logger.Info("launcher: termination signal received", "signal", s.String()) case <-l.shutdownCh: l.logger.Info("launcher: programmatic shutdown requested") } l.stopAll() return nil } // Shutdown triggers a graceful shutdown and waits for Run to return. // Idempotent — safe to call multiple times. func (l *launcher) Shutdown(ctx context.Context) error { l.shutdownOnce.Do(func() { close(l.shutdownCh) }) select { case <-l.doneCh: return nil case <-ctx.Done(): return ctx.Err() } } // stopAll stops all components in reverse registration order. // Each component gets at most Options.ComponentStopTimeout to complete OnStop. func (l *launcher) stopAll() { l.logger.Info("launcher: stopping all components") for i := len(l.components) - 1; i >= 0; i-- { done := make(chan struct{}) go func(c Component) { if err := c.OnStop(); err != nil { l.logger.Error("launcher: error during OnStop", err) } close(done) }(l.components[i]) select { case <-done: case <-time.After(l.opts.ComponentStopTimeout): l.logger.Error("launcher: component OnStop timed out", nil) } } l.logger.Info("launcher: all components stopped") }