Lifecycle

Use lifecycle hooks for initialization, cleanup, reload, and event handling.

Overview

The app package provides lifecycle hooks for managing application state. Registering a hook after the router is frozen (e.g. after Start()) returns an error; register all hooks before calling Start().

  • OnStart - Called before server starts. Runs sequentially. Stops on first error.
  • OnReady - Called when server is ready to accept connections. Runs async. Non-blocking.
  • OnReload - Called when SIGHUP is received or Reload() is called. Runs sequentially. Errors logged.
  • OnShutdown - Called during graceful shutdown. LIFO order.
  • OnStop - Called after shutdown completes. Best-effort.
  • OnRoute - Called when a route is registered. Synchronous.

OnStart Hook

Basic Usage

Initialize resources before the server starts:

a := app.MustNew()

if err := a.OnStart(func(ctx context.Context) error {
    log.Println("Connecting to database...")
    return db.Connect(ctx)
}); err != nil {
    log.Fatal(err)
}

if err := a.OnStart(func(ctx context.Context) error {
    log.Println("Running migrations...")
    return db.Migrate(ctx)
}); err != nil {
    log.Fatal(err)
}

// Start server - hooks execute before listening
a.Start(ctx)

Error Handling

OnStart hooks run sequentially and stop on first error:

a.OnStart(func(ctx context.Context) error {
    if err := db.Connect(ctx); err != nil {
        return fmt.Errorf("database connection failed: %w", err)
    }
    return nil
})

// If this hook fails, server won't start
if err := a.Start(ctx); err != nil {
    log.Fatalf("Startup failed: %v", err)
}

Common Use Cases

// Database connection
a.OnStart(func(ctx context.Context) error {
    return db.PingContext(ctx)
})

// Load configuration
a.OnStart(func(ctx context.Context) error {
    return config.Load("config.yaml")
})

// Initialize caches
a.OnStart(func(ctx context.Context) error {
    return cache.Warmup(ctx)
})

// Check external dependencies
a.OnStart(func(ctx context.Context) error {
    return checkExternalServices(ctx)
})

OnReady Hook

Basic Usage

Execute tasks after the server starts listening:

a.OnReady(func() {
    log.Println("Server is ready!")
    log.Printf("Listening on :8080")
})

a.OnReady(func() {
    // Register with service discovery
    consul.Register("my-service", ":8080")
})

Async Execution

OnReady hooks run asynchronously and don’t block startup:

a.OnReady(func() {
    // Long-running warmup task
    time.Sleep(5 * time.Second)
    cache.Preload()
})

// Server accepts connections immediately, warmup runs in background

Error Handling

Panics in OnReady hooks are caught and logged:

a.OnReady(func() {
    // If this panics, it's logged but doesn't crash the server
    doSomethingRisky()
})

OnReload Hook

What is it?

The OnReload hook lets you reload your app’s configuration without stopping the server. When you register this hook, your app automatically listens for SIGHUP signals on Unix systems (Linux, macOS). No extra setup needed!

Basic Usage

Here’s how to reload configuration when you get a SIGHUP signal:

a := app.MustNew(
    app.WithServiceName("my-api"),
)

// Register a reload hook - SIGHUP is now automatically enabled!
a.OnReload(func(ctx context.Context) error {
    log.Println("Reloading configuration...")
    
    // Load new config
    newConfig, err := loadConfig("config.yaml")
    if err != nil {
        return fmt.Errorf("failed to load config: %w", err)
    }
    
    // Apply new config
    applyConfig(newConfig)
    return nil
})

// Start server
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
a.Start(ctx)

Now you can reload without restarting:

# Send SIGHUP to reload
kill -HUP <pid>

# Or use killall
killall -HUP my-api

How it works

When you register an OnReload hook:

  • On Unix/Linux/macOS: Your app automatically listens for SIGHUP signals
  • On Windows: SIGHUP doesn’t exist, but you can still call Reload() programmatically
  • All platforms: You can trigger reload from your code using app.Reload(ctx)

When no OnReload hooks are registered, SIGHUP is ignored on Unix so the process is not terminated (e.g. by kill -HUP or terminal disconnect).

Error Handling

If reload fails, your app keeps running with the old configuration:

a.OnReload(func(ctx context.Context) error {
    cfg, err := loadConfig("config.yaml")
    if err != nil {
        // Error is logged, but server keeps running
        return err
    }
    
    // Validate before applying
    if err := cfg.Validate(); err != nil {
        return fmt.Errorf("invalid config: %w", err)
    }
    
    applyConfig(cfg)
    return nil
})

The hooks run one at a time (sequentially) and stop on the first error. This means if you have multiple reload hooks and one fails, the rest won’t run.

Programmatic Reload

You can also trigger reload from your code - useful for admin endpoints:

// Create an admin endpoint to trigger reload
a.POST("/admin/reload", func(c *app.Context) {
    if err := a.Reload(c.Request.Context()); err != nil {
        c.InternalError(err)
        return
    }
    c.JSON(200, map[string]string{"status": "config reloaded"})
})

Multiple Reload Hooks

You can register multiple hooks for different parts of your config:

// Reload database pool settings
a.OnReload(func(ctx context.Context) error {
    log.Println("Reloading database config...")
    return db.ReconfigurePool(ctx)
})

// Reload cache settings
a.OnReload(func(ctx context.Context) error {
    log.Println("Reloading cache config...")
    return cache.Reload(ctx)
})

// Reload log level
a.OnReload(func(ctx context.Context) error {
    log.Println("Reloading log level...")
    return logger.SetLevel(newLevel)
})

Common Use Cases

// Reload TLS certificates
a.OnReload(func(ctx context.Context) error {
    return tlsManager.ReloadCertificates()
})

// Reload feature flags
a.OnReload(func(ctx context.Context) error {
    return features.Reload(ctx)
})

// Reload rate limits
a.OnReload(func(ctx context.Context) error {
    return rateLimiter.UpdateLimits(ctx)
})

// Flush caches
a.OnReload(func(ctx context.Context) error {
    cache.Clear()
    return nil
})

What can’t be reloaded?

Routes and middleware can’t be changed after the server starts - they’re frozen for safety. Only reload things like:

  • Configuration files
  • Database connection settings
  • TLS certificates
  • Cache contents
  • Log levels
  • Feature flags

Platform Differences

  • Unix/Linux/macOS: SIGHUP works automatically
  • Windows: SIGHUP isn’t available, use app.Reload(ctx) instead

Thread Safety

Don’t worry about multiple reload signals at the same time - the framework handles this automatically. If multiple SIGHUPs come in, they’ll run one at a time.

OnShutdown Hook

Basic Usage

Clean up resources during graceful shutdown:

a.OnShutdown(func(ctx context.Context) {
    log.Println("Shutting down gracefully...")
    db.Close()
})

a.OnShutdown(func(ctx context.Context) {
    log.Println("Flushing metrics...")
    metrics.Flush(ctx)
})

LIFO Execution Order

OnShutdown hooks execute in reverse order (Last In, First Out):

a.OnShutdown(func(ctx context.Context) {
    log.Println("1. First registered")
})

a.OnShutdown(func(ctx context.Context) {
    log.Println("2. Second registered")
})

// During shutdown, prints:
// "2. Second registered"
// "1. First registered"

This ensures cleanup happens in reverse dependency order.

Timeout Handling

OnShutdown hooks must complete within the shutdown timeout:

a, err := app.New(
    app.WithServer(
        app.WithShutdownTimeout(30 * time.Second),
    ),
)

a.OnShutdown(func(ctx context.Context) {
    // This context has a 30s deadline
    select {
    case <-flushComplete:
        log.Println("Flush completed")
    case <-ctx.Done():
        log.Println("Flush timed out")
    }
})

Common Use Cases

// Close database connections
a.OnShutdown(func(ctx context.Context) {
    db.Close()
})

// Flush metrics and traces
a.OnShutdown(func(ctx context.Context) {
    metrics.Shutdown(ctx)
    tracing.Shutdown(ctx)
})

// Deregister from service discovery
a.OnShutdown(func(ctx context.Context) {
    consul.Deregister("my-service")
})

// Close external connections
a.OnShutdown(func(ctx context.Context) {
    redis.Close()
    messageQueue.Close()
})

OnStop Hook

Basic Usage

Final cleanup after shutdown completes:

a.OnStop(func() {
    log.Println("Cleanup complete")
    cleanupTempFiles()
})

Best-Effort Execution

OnStop hooks run in best-effort mode - panics are caught and logged:

a.OnStop(func() {
    // Even if this panics, other hooks still run
    cleanupTempFiles()
})

No Timeout

OnStop hooks don’t have a timeout constraint:

a.OnStop(func() {
    // This can take as long as needed
    archiveLogs()
})

OnRoute Hook

Basic Usage

Execute code when routes are registered:

a.OnRoute(func(rt *route.Route) {
    log.Printf("Registered: %s %s", rt.Method(), rt.Path())
})

// Register routes - hook fires for each one
a.GET("/users", handler)
a.POST("/users", handler)

Route Validation

Validate routes during registration:

a.OnRoute(func(rt *route.Route) {
    // Ensure all routes have names
    if rt.Name() == "" {
        log.Printf("Warning: Route %s %s has no name", rt.Method(), rt.Path())
    }
})

Documentation Generation

Use for automatic documentation:

var routes []string

a.OnRoute(func(rt *route.Route) {
    routes = append(routes, fmt.Sprintf("%s %s", rt.Method(), rt.Path()))
})

// After all routes registered
a.OnReady(func() {
    log.Printf("Registered %d routes:", len(routes))
    for _, r := range routes {
        log.Println("  ", r)
    }
})

Complete Example

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    
    "rivaas.dev/app"
)

var db *Database

func main() {
    a := app.MustNew(
        app.WithServiceName("api"),
        app.WithServer(
            app.WithShutdownTimeout(30 * time.Second),
        ),
    )
    
    // OnStart: Initialize resources
    if err := a.OnStart(func(ctx context.Context) error {
        log.Println("Connecting to database...")
        var err error
        db, err = ConnectDB(ctx)
        if err != nil {
            return fmt.Errorf("database connection failed: %w", err)
        }
        return nil
    }); err != nil {
        log.Fatalf("failed to register OnStart: %v", err)
    }
    
    if err := a.OnStart(func(ctx context.Context) error {
        log.Println("Running migrations...")
        return db.Migrate(ctx)
    }); err != nil {
        log.Fatalf("failed to register OnStart: %v", err)
    }
    
    // OnRoute: Log route registration
    if err := a.OnRoute(func(rt *route.Route) {
        log.Printf("Route registered: %s %s", rt.Method(), rt.Path())
    }); err != nil {
        log.Fatalf("failed to register OnRoute: %v", err)
    }
    
    // OnReady: Post-startup tasks
    if err := a.OnReady(func() {
        log.Println("Server is ready!")
        log.Println("Registering with service discovery...")
        consul.Register("api", ":8080")
    }); err != nil {
        log.Fatalf("failed to register OnReady: %v", err)
    }
    
    // OnShutdown: Graceful cleanup
    if err := a.OnShutdown(func(ctx context.Context) {
        log.Println("Deregistering from service discovery...")
        consul.Deregister("api")
    }); err != nil {
        log.Fatalf("failed to register OnShutdown: %v", err)
    }
    
    if err := a.OnShutdown(func(ctx context.Context) {
        log.Println("Closing database connection...")
        if err := db.Close(); err != nil {
            log.Printf("Error closing database: %v", err)
        }
    }); err != nil {
        log.Fatalf("failed to register OnShutdown: %v", err)
    }
    
    // OnStop: Final cleanup
    if err := a.OnStop(func() {
        log.Println("Cleanup complete")
    }); err != nil {
        log.Fatalf("failed to register OnStop: %v", err)
    }
    
    // Register routes
    a.GET("/", homeHandler)
    a.GET("/health", healthHandler)
    
    // Setup graceful shutdown
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer cancel()
    
    // Start server
    log.Println("Starting server...")
    if err := a.Start(ctx); err != nil {
        log.Fatalf("Server error: %v", err)
    }
}

Hook Execution Flow

1. app.Start(ctx) called
2. OnStart hooks execute (sequential, stop on error)
3. Server starts listening
4. OnReady hooks execute (async, non-blocking)
5. Server handles requests...
   → OnReload hooks execute when SIGHUP received (sequential, logged on error)
6. Context canceled (SIGTERM/SIGINT)
7. OnShutdown hooks execute (LIFO order, with timeout)
8. Server shutdown complete
9. OnStop hooks execute (best-effort, no timeout)
10. Process exits

Next Steps