Files
gochat/backend/internal/app/shutdown.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

89 lines
2.6 KiB
Go

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")
}