package app import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "syscall" "time" "gcy_hpc_server/internal/config" "gcy_hpc_server/internal/handler" "gcy_hpc_server/internal/server" "gcy_hpc_server/internal/service" "gcy_hpc_server/internal/slurm" "gcy_hpc_server/internal/storage" "gcy_hpc_server/internal/store" "go.uber.org/zap" "gorm.io/gorm" ) type App struct { cfg *config.Config logger *zap.Logger db *gorm.DB server *http.Server cancelCleanup context.CancelFunc } // NewApp initializes all application dependencies: DB, Slurm client, services, handlers, router. func NewApp(cfg *config.Config, logger *zap.Logger) (*App, error) { gormDB, err := initDB(cfg, logger) if err != nil { return nil, err } slurmClient, err := initSlurmClient(cfg) if err != nil { closeDB(gormDB) return nil, err } srv, cancelCleanup := initHTTPServer(cfg, gormDB, slurmClient, logger) return &App{ cfg: cfg, logger: logger, db: gormDB, server: srv, cancelCleanup: cancelCleanup, }, nil } // Run starts the HTTP server and blocks until a shutdown signal or server error. func (a *App) Run() error { errCh := make(chan error, 1) go func() { a.logger.Info("starting server", zap.String("addr", a.server.Addr)) if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- fmt.Errorf("server listen: %w", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) select { case err := <-errCh: // Server crashed before receiving a signal — clean up resources before // returning, because the caller may call os.Exit and skip deferred Close(). a.logger.Error("server exited unexpectedly", zap.Error(err)) _ = a.Close() return err case sig := <-quit: a.logger.Info("received shutdown signal", zap.String("signal", sig.String())) } a.logger.Info("shutting down server...") return a.Close() } // Close cleans up all resources: HTTP server and database connections. func (a *App) Close() error { var errs []error if a.cancelCleanup != nil { a.cancelCleanup() } if a.server != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := a.server.Shutdown(ctx); err != nil && err != http.ErrServerClosed { errs = append(errs, fmt.Errorf("shutdown http server: %w", err)) } } if a.db != nil { sqlDB, err := a.db.DB() if err != nil { errs = append(errs, fmt.Errorf("get underlying sql.DB: %w", err)) } else if err := sqlDB.Close(); err != nil { errs = append(errs, fmt.Errorf("close database: %w", err)) } } return errors.Join(errs...) } // --------------------------------------------------------------------------- // Initialization helpers // --------------------------------------------------------------------------- func initDB(cfg *config.Config, logger *zap.Logger) (*gorm.DB, error) { gormDB, err := store.NewGormDB(cfg.MySQLDSN, logger, cfg.Log.GormLevel) if err != nil { return nil, fmt.Errorf("init database: %w", err) } if err := store.AutoMigrate(gormDB); err != nil { closeDB(gormDB) return nil, fmt.Errorf("run migrations: %w", err) } return gormDB, nil } func closeDB(db *gorm.DB) { if db == nil { return } if sqlDB, err := db.DB(); err == nil { _ = sqlDB.Close() } } func initSlurmClient(cfg *config.Config) (*slurm.Client, error) { client, err := service.NewSlurmClient(cfg.SlurmAPIURL, cfg.SlurmUserName, cfg.SlurmJWTKeyPath) if err != nil { return nil, fmt.Errorf("init slurm client: %w", err) } return client, nil } func initHTTPServer(cfg *config.Config, db *gorm.DB, slurmClient *slurm.Client, logger *zap.Logger) (*http.Server, context.CancelFunc) { jobSvc := service.NewJobService(slurmClient, logger) clusterSvc := service.NewClusterService(slurmClient, logger) jobH := handler.NewJobHandler(jobSvc, logger) clusterH := handler.NewClusterHandler(clusterSvc, logger) appStore := store.NewApplicationStore(db) appSvc := service.NewApplicationService(appStore, jobSvc, cfg.WorkDirBase, logger) appH := handler.NewApplicationHandler(appSvc, logger) // File storage initialization minioClient, err := storage.NewMinioClient(cfg.Minio) if err != nil { logger.Warn("failed to initialize MinIO client, file storage disabled", zap.Error(err)) } var uploadH *handler.UploadHandler var fileH *handler.FileHandler var folderH *handler.FolderHandler if minioClient != nil { blobStore := store.NewBlobStore(db) fileStore := store.NewFileStore(db) folderStore := store.NewFolderStore(db) uploadStore := store.NewUploadStore(db) uploadSvc := service.NewUploadService(minioClient, blobStore, fileStore, uploadStore, cfg.Minio, db, logger) folderSvc := service.NewFolderService(folderStore, fileStore, logger) fileSvc := service.NewFileService(minioClient, blobStore, fileStore, cfg.Minio.Bucket, db, logger) uploadH = handler.NewUploadHandler(uploadSvc, logger) fileH = handler.NewFileHandler(fileSvc, logger) folderH = handler.NewFolderHandler(folderSvc, logger) cleanupCtx, cancelCleanup := context.WithCancel(context.Background()) go startCleanupWorker(cleanupCtx, uploadStore, minioClient, cfg.Minio.Bucket, logger) router := server.NewRouter(jobH, clusterH, appH, uploadH, fileH, folderH, logger) addr := ":" + cfg.ServerPort return &http.Server{ Addr: addr, Handler: router, }, cancelCleanup } router := server.NewRouter(jobH, clusterH, appH, uploadH, fileH, folderH, logger) addr := ":" + cfg.ServerPort return &http.Server{ Addr: addr, Handler: router, }, nil }