package app import ( "context" "os" "os/signal" "syscall" "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 { start := time.Now() applogger.L().Info("Shutting down GoChat application...") // Step 0: Stop config hot-reloader if a.reloader != nil { a.reloader.Stop() applogger.L().Info("Config hot-reloader stopped") } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Step 0.5: Close notification delivery service (Watermill router + subscriber) if a.notificationDeliverySvc != nil { if closeErr := a.notificationDeliverySvc.Close(); closeErr != nil { applogger.L().Errorf("Notification delivery service close error: %v", closeErr) } else { applogger.L().Info("Notification delivery service closed") } } // Step 1: Close WebSocket Hub — disconnect all connected clients if a.wsHub != nil { a.wsHub.Shutdown(ctx) applogger.L().Info("WebSocket hub closed") } // 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) } 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) } 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 nil } // WaitForShutdownSignal blocks until a termination signal is received. func WaitForShutdownSignal() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit applogger.L().Info("Received shutdown signal") }