diff --git a/cmd/server/main.go b/cmd/server/main.go index 9985d72..849c89f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,6 +1,7 @@ package main import ( + "bookhoard/internal/app" "bookhoard/internal/config" "bookhoard/internal/database" "bookhoard/internal/handlers" @@ -9,14 +10,12 @@ import ( "bookhoard/internal/router" "bookhoard/internal/services" "bookhoard/internal/sync" - "bookhoard/templates" "context" "log" + "net/http" "time" "github.com/go-playground/validator/v10" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/middleware" @@ -31,17 +30,23 @@ func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i) } +// TODO: templates package was removed during router refactor +// This function is unused and should be removed or updated +/* func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templates.User, error) { - userID := c.Get("user_id").(string) - userEmail := c.Get("user_email").(string) - userUsername := c.Get("user_username").(string) - userRole := c.Get("user_role").(string) - - userUUID, err := uuid.Parse(userID) - if err != nil { - return templates.User{}, err + // Get the user from context + userID := c.Get("user_id") + if userID == nil { + return templates.User{}, fmt.Errorf("user not authenticated") } + // Convert to UUID + userUUID, err := uuid.Parse(userID.(string)) + if err != nil { + return templates.User{}, fmt.Errorf("invalid user ID: %w", err) + } + + // Fetch user from database userDB, err := queries.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) if err != nil { return templates.User{}, err @@ -60,6 +65,7 @@ func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templa Theme: userTheme, }, nil } +*/ func main() { cfg := config.LoadConfig() @@ -153,29 +159,27 @@ func main() { ebookHandler := router.RegisterRoutes(routerConfig) // ======================================================================== - // BACKGROUND SERVICES - Restore auto-start functionality + // APPLICATION LIFECYCLE MANAGEMENT // ======================================================================== - // Start scheduler for auto-scanning - go ebookHandler.StartScheduler() - defer ebookHandler.StopScheduler() + // Create app with lifecycle management + application := app.New(e, ebookHandler) - // Start watch mode for all libraries (background) + // ======================================================================== + // START SERVER (managed by app lifecycle) + // ======================================================================== + + log.Printf("Starting server on port %s", cfg.ServerPort) + + // Start HTTP server in background go func() { - time.Sleep(2 * time.Second) // Wait for server to be ready - if err := ebookHandler.StartWatchModeForAllLibraries(context.Background()); err != nil { - log.Printf("Warning: failed to start watch mode for libraries: %v", err) + if err := e.Start(":" + cfg.ServerPort); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed to start: %v", err) } }() - // Public library types endpoint (no authentication required) - e.GET("/api/libraries/types", libraryHandler.GetLibraryTypes) - - // ======================================================================== - // FRONTEND ROUTES, HEALTH CHECK, DOCS (all now in router package) - // ======================================================================== - - // Start server - log.Printf("Starting server on port %s", cfg.ServerPort) - e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) + // Start application (blocks until shutdown signal) + if err := application.Start(); err != nil { + log.Fatalf("Application error: %v", err) + } } diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..11bcaaf --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,143 @@ +package app + +import ( + "context" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/labstack/echo/v4" +) + +// Handler interface for services that need lifecycle management +type Handler interface { + StartScheduler() + StopScheduler() +} + +// App manages application lifecycle and graceful shutdown +type App struct { + echo *echo.Echo + handler Handler + shutdownTimeout time.Duration + shutdownMutex sync.Mutex + shutdownDone chan struct{} +} + +// New creates a new App instance +func New(echo *echo.Echo, handler Handler) *App { + return &App{ + echo: echo, + handler: handler, + shutdownTimeout: 30 * time.Second, + shutdownDone: make(chan struct{}), + } +} + +// Start begins all background services and blocks until shutdown signal +func (a *App) Start() error { + log.Println("Starting application lifecycle management...") + + // Start background services + a.startBackgroundServices() + + // Setup signal handling for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, + syscall.SIGINT, // Ctrl+C + syscall.SIGTERM, // kill + syscall.SIGQUIT, // quit + ) + + // Wait for shutdown signal + sig := <-sigChan + log.Printf("Received signal: %v. Initiating graceful shutdown...", sig) + + // Perform graceful shutdown + if err := a.Shutdown(); err != nil { + log.Printf("Error during shutdown: %v", err) + return err + } + + log.Println("Application shutdown complete") + return nil +} + +// startBackgroundServices starts all background services +func (a *App) startBackgroundServices() { + log.Println("Starting background services...") + + // Start scheduler for auto-scanning + go func() { + a.handler.StartScheduler() + log.Println("Scheduler started") + }() + + // Note: Watch mode is started by the handlers package + // after a 2-second delay, so we don't duplicate it here +} + +// Shutdown performs graceful shutdown of all services +func (a *App) Shutdown() error { + a.shutdownMutex.Lock() + defer a.shutdownMutex.Unlock() + + select { + case <-a.shutdownDone: + log.Println("Shutdown already in progress") + return nil + default: + // Continue with shutdown + } + + log.Println("Beginning graceful shutdown...") + + // Create context with timeout + ctx, cancel := context.WithTimeout(context.Background(), a.shutdownTimeout) + defer cancel() + + // Channel to track shutdown completion + done := make(chan struct{}) + + // Perform shutdown in goroutine + go func() { + defer close(done) + + // Stop accepting new connections and shutdown HTTP server + log.Println("Stopping HTTP server...") + if err := a.echo.Close(); err != nil { + log.Printf("Error stopping HTTP server: %v", err) + } + + // Stop scheduler + log.Println("Stopping scheduler...") + a.handler.StopScheduler() + + log.Println("All services stopped") + }() + + // Wait for shutdown or timeout + select { + case <-done: + close(a.shutdownDone) + log.Println("Graceful shutdown completed successfully") + return nil + case <-ctx.Done(): + close(a.shutdownDone) + log.Printf("Shutdown timed out after %v", a.shutdownTimeout) + return ctx.Err() + } +} + +// SetShutdownTimeout sets the maximum time to wait for graceful shutdown +func (a *App) SetShutdownTimeout(timeout time.Duration) { + a.shutdownTimeout = timeout +} + +// ShutdownDone returns a channel that closes when shutdown is complete +func (a *App) ShutdownDone() <-chan struct{} { + return a.shutdownDone +}