Build and publish Docker images / Build and publish images (push) Successful in 2m10s
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// Close the shared local GeoIP reader after all HTTP handlers have drained.
|
|
if a.visitorGeoReader != nil {
|
|
if err := a.visitorGeoReader.Close(); err != nil {
|
|
applogger.L().Errorf("GeoIP database close error: %v", err)
|
|
shutdownErrs = append(shutdownErrs, err)
|
|
} else {
|
|
applogger.L().Info("GeoIP database 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)
|
|
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...)
|
|
}
|