Lifecycle

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

Overview

The app package provides lifecycle hooks for managing application state:

  • OnStart - Called before server starts. Runs sequentially. Stops on first error.
  • OnReady - Called when server is ready to accept connections. Runs async. Non-blocking.
  • 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()

a.OnStart(func(ctx context.Context) error {
    log.Println("Connecting to database...")
    return db.Connect(ctx)
})

a.OnStart(func(ctx context.Context) error {
    log.Println("Running migrations...")
    return db.Migrate(ctx)
})

// Start server - hooks execute before listening
a.Start(ctx, ":8080")

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, ":8080"); 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()
})

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
    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
    })
    
    a.OnStart(func(ctx context.Context) error {
        log.Println("Running migrations...")
        return db.Migrate(ctx)
    })
    
    // OnRoute: Log route registration
    a.OnRoute(func(rt *route.Route) {
        log.Printf("Route registered: %s %s", rt.Method(), rt.Path())
    })
    
    // OnReady: Post-startup tasks
    a.OnReady(func() {
        log.Println("Server is ready!")
        log.Println("Registering with service discovery...")
        consul.Register("api", ":8080")
    })
    
    // OnShutdown: Graceful cleanup
    a.OnShutdown(func(ctx context.Context) {
        log.Println("Deregistering from service discovery...")
        consul.Deregister("api")
    })
    
    a.OnShutdown(func(ctx context.Context) {
        log.Println("Closing database connection...")
        if err := db.Close(); err != nil {
            log.Printf("Error closing database: %v", err)
        }
    })
    
    // OnStop: Final cleanup
    a.OnStop(func() {
        log.Println("Cleanup complete")
    })
    
    // 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, ":8080"); err != nil {
        log.Fatalf("Server error: %v", err)
    }
}

Hook Execution Flow

1. app.Start(ctx, ":8080") called
2. OnStart hooks execute (sequential, stop on error)
3. Server starts listening
4. OnReady hooks execute (async, non-blocking)
5. Server handles requests...
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