package app import ( "context" "errors" "fmt" "time" applogger "github.com/gochat/gochat/pkg/logger" ) // Shutdown handles graceful shutdown of the application. // Pattern follows Chatwoot's Puma graceful shutdown (config/puma.rb) // and Rails signal handling (SIGTERM -> graceful stop). // Order: 1. Stop accepting new connections (done by http.Server.Shutdown in main) // 2. Stop config hot-reloader // 3. Close notification delivery service (Watermill router + subscriber) // 4. Close WebSocket Hub (disconnect all clients) // 5. Close PubSub (stop message publishing/consuming) // 6. Close database connection pool // 7. Flush logger buffers func (a *App) Shutdown(timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return a.shutdown(ctx) } func (a *App) shutdown(ctx context.Context) error { start := time.Now() applogger.L().Info("Shutting down GoChat application...") if a.ready != nil { a.ready.Store(false) } var shutdownErrs []error handlers := a.handlers() handlers.Stop() // Step 0: Stop config hot-reloader if a.reloader != nil { a.reloader.Stop() applogger.L().Info("Config hot-reloader stopped") } // Stop claiming new jobs first and let active handlers drain. if a.workerPool != nil { if err := a.workerPool.Shutdown(ctx); err != nil { shutdownErrs = append(shutdownErrs, fmt.Errorf("worker shutdown: %w", err)) } } // Step 0.5: Close notification delivery service (Watermill router + subscriber) if a.notificationDeliverySvc != nil && a.notificationRunning.Swap(false) { if closeErr := a.notificationDeliverySvc.Close(ctx); closeErr != nil { applogger.L().Errorf("Notification delivery service close error: %v", closeErr) shutdownErrs = append(shutdownErrs, closeErr) } else { applogger.L().Info("Notification delivery service closed") } } // Router.Close may time out while HTTP or notification handlers still use // shared dependencies. Keep those dependencies alive until every handler exits. handlers.Wait() // Step 1: Close WebSocket Hub — disconnect all connected clients if a.wsHub != nil { a.wsHub.Shutdown(ctx) applogger.L().Info("WebSocket hub closed") } if a.wsRelay != nil { if err := a.wsRelay.Stop(); err != nil { shutdownErrs = append(shutdownErrs, err) } } // Step 2: Close PubSub — stop event publishing and consuming if a.pubsub != nil { if closer, ok := a.pubsub.(interface{ Close() error }); ok { if closeErr := closer.Close(); closeErr != nil { applogger.L().Errorf("PubSub close error: %v", closeErr) shutdownErrs = append(shutdownErrs, closeErr) } else { applogger.L().Info("PubSub closed") } } } // Step 3: Close database connection pool if a.db != nil { sqlDB, err := a.db.DB() if err == nil { if closeErr := sqlDB.Close(); closeErr != nil { applogger.L().Errorf("Database close error: %v", closeErr) shutdownErrs = append(shutdownErrs, closeErr) } else { applogger.L().Info("Database connection pool closed") } } } // Step 4: Flush logger buffer applogger.Sync() duration := time.Since(start) applogger.L().Infof("GoChat shutdown complete (took %v)", duration) return errors.Join(shutdownErrs...) }