- Created ShutdownManager for coordinated graceful shutdown of all services - Added Shutdowner interface for services that need graceful shutdown - Implemented parallel shutdown with individual timeouts (10s per service) - Added global shutdown timeout (30s total) - Integrated shutdown manager in main.go for: - HTTP server shutdown - JobWorker cancellation - Config.Close() (DB, Redis, RabbitMQ) - Logger sync - Sentry flush - Added comprehensive unit tests for shutdown manager - Prevents registration of new services during shutdown Phase: PHASE-6 Priority: P2 Progress: 113/267 (42.32%)
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
_ "net/http/pprof" // MOD-P2-006: Activer pprof pour profiling
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/getsentry/sentry-go"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/joho/godotenv"
|
||
"go.uber.org/zap"
|
||
|
||
"veza-backend-api/internal/api"
|
||
"veza-backend-api/internal/config"
|
||
"veza-backend-api/internal/metrics"
|
||
"veza-backend-api/internal/shutdown"
|
||
|
||
_ "veza-backend-api/docs" // Import docs for swagger
|
||
)
|
||
|
||
// @title Veza Backend API
|
||
// @version 1.2.0
|
||
// @description Backend API for Veza platform.
|
||
// @termsOfService http://swagger.io/terms/
|
||
|
||
// @contact.name API Support
|
||
// @contact.url http://www.veza.app/support
|
||
// @contact.email support@veza.app
|
||
|
||
// @license.name Apache 2.0
|
||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||
|
||
// @host localhost:8080
|
||
// @BasePath /api/v1
|
||
|
||
// @securityDefinitions.apikey BearerAuth
|
||
// @in header
|
||
// @name Authorization
|
||
|
||
func main() {
|
||
// Charger les variables d'environnement
|
||
if err := godotenv.Load(); err != nil {
|
||
log.Printf("ℹ️ Note: Fichier .env non trouvé, utilisation des variables d'environnement système")
|
||
}
|
||
|
||
// Configuration du logger
|
||
logger, err := zap.NewProduction()
|
||
if err != nil {
|
||
log.Fatalf("Impossible d'initialiser le logger: %v", err)
|
||
}
|
||
defer logger.Sync()
|
||
|
||
logger.Info("🚀 Démarrage de Veza Backend API")
|
||
|
||
// Charger la configuration
|
||
cfg, err := config.NewConfig()
|
||
if err != nil {
|
||
logger.Fatal("❌ Impossible de charger la configuration", zap.Error(err))
|
||
}
|
||
|
||
// Valider la configuration
|
||
if err := cfg.Validate(); err != nil {
|
||
logger.Fatal("❌ Configuration invalide", zap.Error(err))
|
||
}
|
||
|
||
// Initialiser Sentry si DSN configuré
|
||
if cfg.SentryDsn != "" {
|
||
err := sentry.Init(sentry.ClientOptions{
|
||
Dsn: cfg.SentryDsn,
|
||
Environment: cfg.SentryEnvironment,
|
||
TracesSampleRate: cfg.SentrySampleRateTransactions,
|
||
SampleRate: cfg.SentrySampleRateErrors,
|
||
// AttachStacktrace pour capturer les stack traces
|
||
AttachStacktrace: true,
|
||
})
|
||
if err != nil {
|
||
logger.Warn("❌ Impossible d'initialiser Sentry", zap.Error(err))
|
||
} else {
|
||
logger.Info("✅ Sentry initialisé", zap.String("environment", cfg.SentryEnvironment))
|
||
}
|
||
// Flush les événements Sentry avant shutdown
|
||
defer sentry.Flush(2 * time.Second)
|
||
} else {
|
||
logger.Info("ℹ️ Sentry non configuré (SENTRY_DSN non défini)")
|
||
}
|
||
|
||
// Initialisation de la base de données
|
||
db := cfg.Database
|
||
if db == nil {
|
||
logger.Fatal("❌ Base de données non initialisée")
|
||
}
|
||
defer db.Close()
|
||
|
||
if err := db.Initialize(); err != nil {
|
||
logger.Fatal("❌ Impossible d'initialiser la base de données", zap.Error(err))
|
||
}
|
||
|
||
// MOD-P2-004: Démarrer le collecteur de métriques DB pool
|
||
// Collecte les stats DB pool toutes les 10 secondes et les expose via Prometheus
|
||
metrics.StartDBPoolStatsCollector(db.DB, 10*time.Second)
|
||
logger.Info("✅ Collecteur de métriques DB pool démarré")
|
||
|
||
// Fail-Fast: Vérifier RabbitMQ si activé
|
||
if cfg.RabbitMQEnable {
|
||
if cfg.RabbitMQEventBus == nil {
|
||
logger.Fatal("❌ RabbitMQ activé (RABBITMQ_ENABLE=true) mais non initialisé (problème de connexion?)")
|
||
} else {
|
||
// Optionnel: Check connection status if RabbitMQEventBus exposes it
|
||
// For now, assume if initialized it's connected or retrying.
|
||
// If we want STRICT fail fast, we would need to verify connection is Open here.
|
||
logger.Info("✅ RabbitMQ actif")
|
||
}
|
||
} else {
|
||
logger.Info("ℹ️ RabbitMQ désactivé")
|
||
}
|
||
|
||
// BE-SVC-017: Créer le gestionnaire de shutdown gracieux
|
||
shutdownManager := shutdown.NewShutdownManager(logger)
|
||
|
||
// Démarrer le Job Worker avec contexte pour shutdown gracieux
|
||
var workerCtx context.Context
|
||
var workerCancel context.CancelFunc
|
||
if cfg.JobWorker != nil {
|
||
workerCtx, workerCancel = context.WithCancel(context.Background())
|
||
cfg.JobWorker.Start(workerCtx)
|
||
logger.Info("✅ Job Worker démarré")
|
||
|
||
// Enregistrer le Job Worker pour shutdown gracieux
|
||
shutdownManager.Register(shutdown.NewShutdownFunc("job_worker", func(ctx context.Context) error {
|
||
if workerCancel != nil {
|
||
workerCancel()
|
||
// Attendre un peu pour que les workers se terminent
|
||
time.Sleep(2 * time.Second)
|
||
}
|
||
return nil
|
||
}))
|
||
} else {
|
||
logger.Warn("⚠️ Job Worker non initialisé")
|
||
}
|
||
|
||
// Configuration du mode Gin
|
||
// Correction: Utilisation directe de la variable d'env car non exposée dans Config
|
||
appEnv := os.Getenv("APP_ENV")
|
||
if appEnv == "production" {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
} else {
|
||
gin.SetMode(gin.DebugMode)
|
||
}
|
||
|
||
// Créer le router Gin
|
||
router := gin.New()
|
||
|
||
// Middleware globaux (Logger, Recovery) recommandés par ORIGIN
|
||
router.Use(gin.Logger(), gin.Recovery())
|
||
|
||
// Configuration des routes
|
||
apiRouter := api.NewAPIRouter(db, cfg) // Instantiate APIRouter
|
||
apiRouter.Setup(router) // Call its Setup method
|
||
|
||
// Configuration du serveur HTTP
|
||
port := fmt.Sprintf("%d", cfg.AppPort)
|
||
if cfg.AppPort == 0 {
|
||
port = "8080"
|
||
}
|
||
|
||
server := &http.Server{
|
||
Addr: fmt.Sprintf(":%s", port),
|
||
Handler: router,
|
||
ReadTimeout: 30 * time.Second, // Standards ORIGIN
|
||
WriteTimeout: 30 * time.Second,
|
||
}
|
||
|
||
// BE-SVC-017: Enregistrer tous les services pour shutdown gracieux
|
||
// Enregistrer le serveur HTTP
|
||
shutdownManager.Register(shutdown.NewShutdownFunc("http_server", func(ctx context.Context) error {
|
||
return server.Shutdown(ctx)
|
||
}))
|
||
|
||
// Enregistrer la configuration (ferme DB, Redis, RabbitMQ, etc.)
|
||
shutdownManager.Register(shutdown.NewShutdownFunc("config", func(ctx context.Context) error {
|
||
return cfg.Close()
|
||
}))
|
||
|
||
// Enregistrer le logger pour flush final
|
||
shutdownManager.Register(shutdown.NewShutdownFunc("logger", func(ctx context.Context) error {
|
||
if logger != nil {
|
||
return logger.Sync()
|
||
}
|
||
return nil
|
||
}))
|
||
|
||
// Enregistrer Sentry pour flush final
|
||
if cfg.SentryDsn != "" {
|
||
shutdownManager.Register(shutdown.NewShutdownFunc("sentry", func(ctx context.Context) error {
|
||
sentry.Flush(2 * time.Second)
|
||
return nil
|
||
}))
|
||
}
|
||
|
||
// Gestion de l'arrêt gracieux
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
|
||
go func() {
|
||
logger.Info("🌐 Serveur HTTP démarré", zap.String("port", port))
|
||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
logger.Fatal("❌ Erreur du serveur HTTP", zap.Error(err))
|
||
}
|
||
}()
|
||
|
||
// Attendre le signal d'arrêt
|
||
<-quit
|
||
logger.Info("🔄 Signal d'arrêt reçu, démarrage du shutdown gracieux...")
|
||
|
||
// BE-SVC-017: Arrêt gracieux coordonné de tous les services
|
||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer shutdownCancel()
|
||
|
||
if err := shutdownManager.Shutdown(shutdownCtx); err != nil {
|
||
logger.Error("❌ Erreur lors du shutdown gracieux", zap.Error(err))
|
||
} else {
|
||
logger.Info("✅ Shutdown gracieux terminé avec succès")
|
||
}
|
||
}
|