// Package app wires the Game Lobby Service process lifecycle and // coordinates component startup and graceful shutdown. package app import ( "context" "errors" "fmt" "sync" "galaxy/lobby/internal/config" ) // Component is a long-lived Game Lobby Service subsystem that participates // in coordinated startup and graceful shutdown. type Component interface { // Run starts the component and blocks until it stops. Run(context.Context) error // Shutdown stops the component within the provided timeout-bounded // context. Shutdown(context.Context) error } // App owns the process-level lifecycle of Game Lobby Service and its // registered components. type App struct { cfg config.Config components []Component } // New constructs App with a defensive copy of the supplied components. func New(cfg config.Config, components ...Component) *App { clonedComponents := append([]Component(nil), components...) return &App{ cfg: cfg, components: clonedComponents, } } // Run starts all configured components, waits for cancellation or the first // component failure, and then executes best-effort graceful shutdown. func (app *App) Run(ctx context.Context) error { if ctx == nil { return errors.New("run lobby app: nil context") } if err := app.validate(); err != nil { return err } if len(app.components) == 0 { <-ctx.Done() return nil } runCtx, cancel := context.WithCancel(ctx) defer cancel() results := make(chan componentResult, len(app.components)) var runWaitGroup sync.WaitGroup for index, component := range app.components { runWaitGroup.Add(1) go func(componentIndex int, component Component) { defer runWaitGroup.Done() results <- componentResult{ index: componentIndex, err: component.Run(runCtx), } }(index, component) } var runErr error select { case <-ctx.Done(): case result := <-results: runErr = classifyComponentResult(ctx, result) } cancel() shutdownErr := app.shutdownComponents() waitErr := app.waitForComponents(&runWaitGroup) return errors.Join(runErr, shutdownErr, waitErr) } type componentResult struct { index int err error } func (app *App) validate() error { if app.cfg.ShutdownTimeout <= 0 { return fmt.Errorf("run lobby app: shutdown timeout must be positive, got %s", app.cfg.ShutdownTimeout) } for index, component := range app.components { if component == nil { return fmt.Errorf("run lobby app: component %d is nil", index) } } return nil } func classifyComponentResult(parentCtx context.Context, result componentResult) error { switch { case result.err == nil: if parentCtx.Err() != nil { return nil } return fmt.Errorf("run lobby app: component %d exited without error before shutdown", result.index) case errors.Is(result.err, context.Canceled) && parentCtx.Err() != nil: return nil default: return fmt.Errorf("run lobby app: component %d: %w", result.index, result.err) } } func (app *App) shutdownComponents() error { var shutdownWaitGroup sync.WaitGroup errs := make(chan error, len(app.components)) for index, component := range app.components { shutdownWaitGroup.Add(1) go func(componentIndex int, component Component) { defer shutdownWaitGroup.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), app.cfg.ShutdownTimeout) defer cancel() if err := component.Shutdown(shutdownCtx); err != nil { errs <- fmt.Errorf("shutdown lobby component %d: %w", componentIndex, err) } }(index, component) } shutdownWaitGroup.Wait() close(errs) var joined error for err := range errs { joined = errors.Join(joined, err) } return joined } func (app *App) waitForComponents(runWaitGroup *sync.WaitGroup) error { done := make(chan struct{}) go func() { runWaitGroup.Wait() close(done) }() waitCtx, cancel := context.WithTimeout(context.Background(), app.cfg.ShutdownTimeout) defer cancel() select { case <-done: return nil case <-waitCtx.Done(): return fmt.Errorf("wait for lobby components: %w", waitCtx.Err()) } }