package main import ( "context" "database/sql" "flag" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" _ "github.com/lib/pq" amqp "github.com/rabbitmq/amqp091-go" "github.com/redis/go-redis/v9" "github.com/rwadurian/mpc-system/pkg/config" "github.com/rwadurian/mpc-system/pkg/jwt" "github.com/rwadurian/mpc-system/pkg/logger" httphandler "github.com/rwadurian/mpc-system/services/account/adapters/input/http" grpcadapter "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc" jwtadapter "github.com/rwadurian/mpc-system/services/account/adapters/output/jwt" "github.com/rwadurian/mpc-system/services/account/adapters/output/postgres" "github.com/rwadurian/mpc-system/services/account/adapters/output/rabbitmq" redisadapter "github.com/rwadurian/mpc-system/services/account/adapters/output/redis" "github.com/rwadurian/mpc-system/services/account/application/use_cases" "github.com/rwadurian/mpc-system/services/account/domain/services" "go.uber.org/zap" ) func main() { // Parse flags configPath := flag.String("config", "", "Path to config file") flag.Parse() // Load configuration cfg, err := config.Load(*configPath) if err != nil { fmt.Printf("Failed to load config: %v\n", err) os.Exit(1) } // Initialize logger if err := logger.Init(&logger.Config{ Level: cfg.Logger.Level, Encoding: cfg.Logger.Encoding, }); err != nil { fmt.Printf("Failed to initialize logger: %v\n", err) os.Exit(1) } defer logger.Sync() logger.Info("Starting Account Service", zap.String("environment", cfg.Server.Environment), zap.Int("http_port", cfg.Server.HTTPPort)) // Initialize database connection db, err := initDatabase(cfg.Database) if err != nil { logger.Fatal("Failed to connect to database", zap.Error(err)) } defer db.Close() // Initialize Redis connection redisClient := initRedis(cfg.Redis) defer redisClient.Close() // Initialize RabbitMQ connection rabbitConn, err := initRabbitMQ(cfg.RabbitMQ) if err != nil { logger.Fatal("Failed to connect to RabbitMQ", zap.Error(err)) } defer rabbitConn.Close() // Initialize gRPC client for session coordinator sessionCoordinatorAddr := "mpc-session-coordinator:50051" sessionCoordinatorClient, err := grpcadapter.NewSessionCoordinatorClient(sessionCoordinatorAddr) if err != nil { logger.Fatal("Failed to connect to session coordinator", zap.Error(err)) } defer sessionCoordinatorClient.Close() // Initialize repositories accountRepo := postgres.NewAccountPostgresRepo(db) shareRepo := postgres.NewAccountSharePostgresRepo(db) recoveryRepo := postgres.NewRecoverySessionPostgresRepo(db) // Initialize adapters eventPublisher, err := rabbitmq.NewEventPublisherAdapter(rabbitConn) if err != nil { logger.Fatal("Failed to create event publisher", zap.Error(err)) } defer eventPublisher.Close() cacheAdapter := redisadapter.NewCacheAdapter(redisClient) // Initialize JWT service jwtService := jwt.NewJWTService( cfg.JWT.SecretKey, cfg.JWT.Issuer, cfg.JWT.TokenExpiry, cfg.JWT.RefreshExpiry, ) tokenService := jwtadapter.NewTokenServiceAdapter(jwtService) // Initialize domain service domainService := services.NewAccountDomainService(accountRepo, shareRepo, recoveryRepo) // Initialize use cases createAccountUC := use_cases.NewCreateAccountUseCase(accountRepo, shareRepo, domainService, eventPublisher) getAccountUC := use_cases.NewGetAccountUseCase(accountRepo, shareRepo) updateAccountUC := use_cases.NewUpdateAccountUseCase(accountRepo, eventPublisher) listAccountsUC := use_cases.NewListAccountsUseCase(accountRepo) getAccountSharesUC := use_cases.NewGetAccountSharesUseCase(accountRepo, shareRepo) deactivateShareUC := use_cases.NewDeactivateShareUseCase(accountRepo, shareRepo, eventPublisher) loginUC := use_cases.NewLoginUseCase(accountRepo, shareRepo, tokenService, eventPublisher) refreshTokenUC := use_cases.NewRefreshTokenUseCase(accountRepo, tokenService) generateChallengeUC := use_cases.NewGenerateChallengeUseCase(cacheAdapter) initiateRecoveryUC := use_cases.NewInitiateRecoveryUseCase(accountRepo, recoveryRepo, domainService, eventPublisher) completeRecoveryUC := use_cases.NewCompleteRecoveryUseCase(accountRepo, shareRepo, recoveryRepo, domainService, eventPublisher) getRecoveryStatusUC := use_cases.NewGetRecoveryStatusUseCase(recoveryRepo) cancelRecoveryUC := use_cases.NewCancelRecoveryUseCase(accountRepo, recoveryRepo) // Create shutdown context ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start HTTP server errChan := make(chan error, 1) go func() { if err := startHTTPServer( cfg, createAccountUC, getAccountUC, updateAccountUC, listAccountsUC, getAccountSharesUC, deactivateShareUC, loginUC, refreshTokenUC, generateChallengeUC, initiateRecoveryUC, completeRecoveryUC, getRecoveryStatusUC, cancelRecoveryUC, sessionCoordinatorClient, ); err != nil { errChan <- fmt.Errorf("HTTP server error: %w", err) } }() // Wait for shutdown signal sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) select { case sig := <-sigChan: logger.Info("Received shutdown signal", zap.String("signal", sig.String())) case err := <-errChan: logger.Error("Server error", zap.Error(err)) } // Graceful shutdown logger.Info("Shutting down...") cancel() // Give services time to shutdown gracefully time.Sleep(5 * time.Second) logger.Info("Shutdown complete") _ = ctx } func initDatabase(cfg config.DatabaseConfig) (*sql.DB, error) { const maxRetries = 10 const retryDelay = 2 * time.Second var db *sql.DB var err error for i := 0; i < maxRetries; i++ { db, err = sql.Open("postgres", cfg.DSN()) if err != nil { logger.Warn("Failed to open database connection, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) time.Sleep(retryDelay * time.Duration(i+1)) continue } db.SetMaxOpenConns(cfg.MaxOpenConns) db.SetMaxIdleConns(cfg.MaxIdleConns) db.SetConnMaxLifetime(cfg.ConnMaxLife) // Test connection with Ping if err = db.Ping(); err != nil { logger.Warn("Failed to ping database, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) db.Close() time.Sleep(retryDelay * time.Duration(i+1)) continue } // Verify database is actually usable with a simple query ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) var result int err = db.QueryRowContext(ctx, "SELECT 1").Scan(&result) cancel() if err != nil { logger.Warn("Database ping succeeded but query failed, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) db.Close() time.Sleep(retryDelay * time.Duration(i+1)) continue } logger.Info("Connected to PostgreSQL and verified connectivity", zap.Int("attempt", i+1)) return db, nil } return nil, fmt.Errorf("failed to connect to database after %d retries: %w", maxRetries, err) } func initRedis(cfg config.RedisConfig) *redis.Client { const maxRetries = 10 const retryDelay = 2 * time.Second client := redis.NewClient(&redis.Options{ Addr: cfg.Addr(), Password: cfg.Password, DB: cfg.DB, }) // Test connection with retry ctx := context.Background() for i := 0; i < maxRetries; i++ { if err := client.Ping(ctx).Err(); err != nil { logger.Warn("Redis connection failed, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) time.Sleep(retryDelay * time.Duration(i+1)) continue } logger.Info("Connected to Redis") return client } logger.Warn("Redis connection failed after retries, continuing without cache") return client } func initRabbitMQ(cfg config.RabbitMQConfig) (*amqp.Connection, error) { const maxRetries = 10 const retryDelay = 2 * time.Second var conn *amqp.Connection var err error for i := 0; i < maxRetries; i++ { // Attempt to dial RabbitMQ conn, err = amqp.Dial(cfg.URL()) if err != nil { logger.Warn("Failed to dial RabbitMQ, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.String("url", maskPassword(cfg.URL())), zap.Error(err)) time.Sleep(retryDelay * time.Duration(i+1)) continue } // Verify connection is actually usable by opening a channel ch, err := conn.Channel() if err != nil { logger.Warn("RabbitMQ connection established but channel creation failed, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) conn.Close() time.Sleep(retryDelay * time.Duration(i+1)) continue } // Test the channel with a simple operation (declare a test exchange) err = ch.ExchangeDeclare( "mpc.health.check", // name "fanout", // type false, // durable true, // auto-deleted false, // internal false, // no-wait nil, // arguments ) if err != nil { logger.Warn("RabbitMQ channel created but exchange declaration failed, retrying...", zap.Int("attempt", i+1), zap.Int("max_retries", maxRetries), zap.Error(err)) ch.Close() conn.Close() time.Sleep(retryDelay * time.Duration(i+1)) continue } // Clean up test exchange ch.ExchangeDelete("mpc.health.check", false, false) ch.Close() // Setup connection close notification closeChan := make(chan *amqp.Error, 1) conn.NotifyClose(closeChan) go func() { err := <-closeChan if err != nil { logger.Error("RabbitMQ connection closed unexpectedly", zap.Error(err)) } }() logger.Info("Connected to RabbitMQ and verified connectivity", zap.Int("attempt", i+1)) return conn, nil } return nil, fmt.Errorf("failed to connect to RabbitMQ after %d retries: %w", maxRetries, err) } // maskPassword masks the password in the RabbitMQ URL for logging func maskPassword(url string) string { // Simple masking: amqp://user:password@host:port -> amqp://user:****@host:port start := 0 for i := 0; i < len(url); i++ { if url[i] == ':' && i > 0 && url[i-1] != '/' { start = i + 1 break } } if start == 0 { return url } end := start for i := start; i < len(url); i++ { if url[i] == '@' { end = i break } } if end == start { return url } return url[:start] + "****" + url[end:] } func startHTTPServer( cfg *config.Config, createAccountUC *use_cases.CreateAccountUseCase, getAccountUC *use_cases.GetAccountUseCase, updateAccountUC *use_cases.UpdateAccountUseCase, listAccountsUC *use_cases.ListAccountsUseCase, getAccountSharesUC *use_cases.GetAccountSharesUseCase, deactivateShareUC *use_cases.DeactivateShareUseCase, loginUC *use_cases.LoginUseCase, refreshTokenUC *use_cases.RefreshTokenUseCase, generateChallengeUC *use_cases.GenerateChallengeUseCase, initiateRecoveryUC *use_cases.InitiateRecoveryUseCase, completeRecoveryUC *use_cases.CompleteRecoveryUseCase, getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase, cancelRecoveryUC *use_cases.CancelRecoveryUseCase, sessionCoordinatorClient *grpcadapter.SessionCoordinatorClient, ) error { // Set Gin mode if cfg.Server.Environment == "production" { gin.SetMode(gin.ReleaseMode) } router := gin.New() router.Use(gin.Recovery()) router.Use(gin.Logger()) // Create HTTP handler with session coordinator client httpHandler := httphandler.NewAccountHTTPHandler( createAccountUC, getAccountUC, updateAccountUC, listAccountsUC, getAccountSharesUC, deactivateShareUC, loginUC, refreshTokenUC, generateChallengeUC, initiateRecoveryUC, completeRecoveryUC, getRecoveryStatusUC, cancelRecoveryUC, sessionCoordinatorClient, ) // Health check router.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", "service": "account", }) }) // Register API routes api := router.Group("/api/v1") httpHandler.RegisterRoutes(api) logger.Info("Starting HTTP server", zap.Int("port", cfg.Server.HTTPPort)) return router.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort)) }