Phase 5: Application Lifecycle Management Creates internal/app package for proper lifecycle management, signal handling, and graceful shutdown of all services. Changes: - Create internal/app/app.go with App lifecycle manager - Handles SIGINT, SIGTERM, SIGQUIT signals - Graceful shutdown with 30-second timeout - Manages HTTP server shutdown - Manages scheduler start/stop - Update cmd/server/main.go to use app lifecycle manager - Replace defer-based cleanup with proper signal handling - Server starts in background goroutine - Blocks on app.Start() until shutdown signal - Clean shutdown of all services Benefits: - Proper signal handling (Ctrl+C, kill, docker stop) - Graceful shutdown prevents data corruption - No more os.Exit(1) bypassing defer cleanup - All services stopped in correct order - Server stops accepting new connections first - Then scheduler and background services stopped Technical details: - Uses sync.Mutex for shutdown safety - Context with timeout for shutdown operations - Channel-based coordination for shutdown completion - Logs all lifecycle events for debugging Fixes issue where e.Logger.Fatal() would call os.Exit(1) immediately, skipping defer cleanup and causing unclean shutdown.
144 lines
3.3 KiB
Go
144 lines
3.3 KiB
Go
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
|
|
}
|