ctrl

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2025 License: MIT Imports: 9 Imported by: 0

README

ctrl Build Status Coverage Status godoc

ctrl provides a set of control functions for assertions, error handling, HTTP server management, and graceful shutdown handling in Go applications. Built for Go 1.21+, it offers a clean API with flexible configuration options.

Features

  • Runtime assertions with optional formatted messages
  • Error-returning validation alternatives to assertions
  • HTTP server lifecycle management
  • Graceful shutdown with signal handling
  • Context-based cancellation
  • Configurable timeouts and callbacks
  • Comprehensive error handling
  • No external dependencies except for testing

Quick Start

Here's a practical example showing how to implement graceful shutdown in an HTTP server:

func main() {
    // Set up graceful shutdown
    ctx, cancel := ctrl.GracefulShutdown(
        ctrl.WithTimeout(10*time.Second),
        ctrl.WithOnShutdown(func(sig os.Signal) {
            log.Printf("shutdown initiated by signal: %v", sig)
        }),
    )
    defer cancel()

    // Create a simple HTTP server
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, World!")
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Start server with context-aware shutdown
    errCh := ctrl.RunHTTPServerWithContext(
        ctx,
        server,
        func() error {
            return server.ListenAndServe()
        },
        ctrl.WithHTTPShutdownTimeout(5*time.Second),
    )

    // Wait for server to exit and check for errors
    if err := <-errCh; err != nil {
        log.Fatalf("Server error: %v", err)
    }
}

Core Concepts

Assertions

The package provides assertion functions that panic when conditions are not met, useful for runtime invariant checking:

// Simple assertion
ctrl.Assert(user.IsAuthenticated())

// Formatted assertion
ctrl.Assertf(count > 0, "expected positive count, got %d", count)

// Function-based assertions
ctrl.AssertFunc(func() bool {
    return database.IsConnected()
})

ctrl.AssertFuncf(func() bool {
    return cache.Size() < maxSize
}, "cache size exceeded: %d/%d", cache.Size(), maxSize)
ErrorOr Functions

The package provides variants of assertion functions that return errors instead of panicking. These are useful for validations where you want to return an error to the caller rather than crash the program:

// Basic condition checking
if err := ctrl.ErrorOr(user.IsAuthenticated()); err != nil {
    return err
}

// With formatted error message
if err := ctrl.ErrorOrf(count > 0, "expected positive count, got %d", count); err != nil {
    return err
}

// Function-based variants
if err := ctrl.ErrorOrFunc(func() bool {
    return database.IsConnected()
}); err != nil {
    return err
}

// Function-based with formatted error message
if err := ctrl.ErrorOrFuncf(func() bool {
    return cache.Size() < maxSize
}, "cache size exceeded: %d/%d", cache.Size(), maxSize); err != nil {
    return err
}

// With custom error
customErr := ErrDatabaseNotConnected
if err := ctrl.ErrorOrWithErr(database.IsConnected(), customErr); err != nil {
    return err  // Will return customErr if condition fails
}

// Function-based with custom error
cacheErr := ErrCacheFull
if err := ctrl.ErrorOrFuncWithErr(func() bool {
    return cache.Size() < maxSize
}, cacheErr); err != nil {
    return err  // Will return cacheErr if condition fails
}

These functions are particularly useful in validators, middleware, and other scenarios where returning an error is more appropriate than panicking.

HTTP Server Management

The package helps manage HTTP server lifecycle, particularly graceful shutdown:

// Shutdown an HTTP server with a timeout
err := ctrl.ShutdownHTTPServer(ctx, server, 
    ctrl.WithHTTPShutdownTimeout(5*time.Second))

// Run a server with context-aware shutdown
errCh := ctrl.RunHTTPServerWithContext(ctx, server, 
    func() error { 
        return server.ListenAndServe() 
    },
    ctrl.WithHTTPShutdownTimeout(5*time.Second),
    ctrl.WithHTTPLogger(logger),
)
Graceful Shutdown

The package provides a robust way to handle process termination signals:

// Basic setup
ctx, cancel := ctrl.GracefulShutdown()
defer cancel()

// With custom configuration
ctx, cancel := ctrl.GracefulShutdown(
    ctrl.WithTimeout(30*time.Second),
    ctrl.WithSignals(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP),
    ctrl.WithExitCode(2),
    ctrl.WithOnShutdown(func(sig os.Signal) {
        log.Printf("shutting down due to %s signal", sig)
        database.Close()
    }),
    ctrl.WithOnForceExit(func() {
        log.Printf("force exiting after timeout")
    }),
    ctrl.WithLogger(logger),
)

Install and update

go get -u github.com/go-pkgz/ctrl

Usage Examples

Assertion Usage
func processItems(items []Item) {
    // Ensure we have items to process
    ctrl.Assertf(len(items) > 0, "no items to process")
    
    for _, item := range items {
        // Ensure each item is valid
        ctrl.Assert(item.IsValid())
        
        // Process the item
        process(item)
    }
}
Error Validation Usage
func validateRequest(req Request) error {
    // Return error if ID is empty
    if err := ctrl.ErrorOrf(req.ID != "", "request ID cannot be empty"); err != nil {
        return err
    }
    
    // Return custom error if size exceeds limit
    maxSizeErr := errors.New("max size exceeded")
    if err := ctrl.ErrorOrWithErr(req.Size <= maxSize, maxSizeErr); err != nil {
        return err
    }
    
    return nil
}
HTTP Server with Graceful Shutdown
func startServer() error {
    // Set up graceful shutdown
    ctx, cancel := ctrl.GracefulShutdown(
        ctrl.WithTimeout(10*time.Second),
        ctrl.WithLogger(logger),
    )
    defer cancel()
    
    // Create server
    server := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }
    
    // Run server
    errCh := ctrl.RunHTTPServerWithContext(ctx, server, server.ListenAndServe)
    
    // Wait for shutdown
    return <-errCh
}
Multi-Stage Shutdown
func main() {
    // Create shutdown context
    ctx, cancel := ctrl.GracefulShutdown(
        ctrl.WithOnShutdown(func(sig os.Signal) {
            log.Printf("shutdown sequence initiated by %v", sig)
        }),
    )
    defer cancel()
    
    // Create multiple services with the same shutdown context
    httpServer := setupHTTPServer(ctx)
    grpcServer := setupGRPCServer(ctx)
    cacheService := setupCache(ctx)
    
    // Wait for context cancellation (shutdown signal)
    <-ctx.Done()
    
    // Context was canceled, services will be shutting down
    log.Println("waiting for all services to complete shutdown")
    
    // Additional cleanup if needed
    database.Close()
}

Optional Parameters

The package uses functional options pattern for configuration:

HTTP Server Options
// WithHTTPShutdownTimeout sets the maximum time to wait for server shutdown
WithHTTPShutdownTimeout(timeout time.Duration)

// WithHTTPLogger sets a custom logger for HTTP server operations
WithHTTPLogger(logger *slog.Logger)
Graceful Shutdown Options
// WithSignals sets which signals trigger the shutdown
WithSignals(signals ...os.Signal)

// WithTimeout sets the maximum time to wait for graceful shutdown
WithTimeout(timeout time.Duration)

// WithoutForceExit disables the forced exit after timeout
WithoutForceExit()

// WithExitCode sets the exit code used for forced exits
WithExitCode(code int)

// WithOnShutdown sets a callback function called when shutdown begins
WithOnShutdown(fn func(os.Signal))

// WithOnForceExit sets a callback function called right before forced exit
WithOnForceExit(fn func())

// WithLogger sets a custom logger for shutdown messages
WithLogger(logger *slog.Logger)

Best Practices

  1. Assertions vs ErrorOr: Choose based on failure severity

    // Use Assert for internal invariants that should never fail
    ctrl.Assert(len(buffer) >= headerSize)
    
    // Use ErrorOr for validating external input
    if err := ctrl.ErrorOr(len(userInput) < maxLength); err != nil {
        return err
    }
    
  2. Graceful Shutdown: Allow sufficient time for connections to close

    // Shorter timeout for development
    ctrl.WithTimeout(5*time.Second)
    
    // Longer timeout for production with many connections
    ctrl.WithTimeout(30*time.Second)
    
  3. HTTP Server Context: Create a separate context for each server

    // Each server gets its own timeout and configuration
    apiErrCh := ctrl.RunHTTPServerWithContext(ctx, apiServer, apiServer.ListenAndServe,
        ctrl.WithHTTPShutdownTimeout(10*time.Second))
    
    adminErrCh := ctrl.RunHTTPServerWithContext(ctx, adminServer, adminServer.ListenAndServe,
        ctrl.WithHTTPShutdownTimeout(5*time.Second))
    
  4. Shutdown Callbacks: Use for resource cleanup

    ctrl.WithOnShutdown(func(sig os.Signal) {
        // Close database connections
        db.Close()
    
        // Flush logs
        logger.Sync()
    
        // Release other resources
        cache.Clear()
    })
    

Error Handling

The package provides clear error handling patterns:

// HTTP server shutdown
if err := ctrl.ShutdownHTTPServer(ctx, server); err != nil {
    log.Printf("error during server shutdown: %v", err)
}

// Running HTTP server with context
errCh := ctrl.RunHTTPServerWithContext(ctx, server, server.ListenAndServe)
if err := <-errCh; err != nil {
    log.Fatalf("server error: %v", err)
}

Contributing

Contributions to ctrl are welcome! Please submit a pull request or open an issue for any bugs or feature requests.

License

ctrl is available under the MIT license. See the LICENSE file for more info.

Documentation

Overview

Package ctrl provides a set of control functions for assertions, error handling, HTTP server management, and graceful shutdown handling in Go applications. Built for Go 1.21+, it offers a clean API with flexible configuration options and no external runtime dependencies.

Assertions

The package provides assertion functions that panic when conditions are not met, useful for runtime invariant checking:

ctrl.Assert(user.IsAuthenticated())
ctrl.Assertf(count > 0, "expected positive count, got %d", count)
ctrl.AssertFunc(func() bool { return database.IsConnected() })
ctrl.AssertFuncf(func() bool { return cache.Size() < maxSize }, "cache exceeded: %d", cache.Size())

Error Handling

For scenarios where returning an error is more appropriate than panicking, the package provides ErrorOr variants:

if err := ctrl.ErrorOr(user.IsAuthenticated()); err != nil {
    return err
}
if err := ctrl.ErrorOrf(count > 0, "expected positive count, got %d", count); err != nil {
    return err
}
if err := ctrl.ErrorOrFunc(func() bool { return database.IsConnected() }); err != nil {
    return err
}
if err := ctrl.ErrorOrFuncf(func() bool { return cache.Size() < maxSize },
    "cache size exceeded: %d/%d", cache.Size(), maxSize); err != nil {
    return err
}
customErr := errors.New("database not connected")
if err := ctrl.ErrorOrWithErr(database.IsConnected(), customErr); err != nil {
    return err  // Returns customErr if condition fails
}
if err := ctrl.ErrorOrFuncWithErr(func() bool { return cache.Size() < maxSize }, ErrCacheFull); err != nil {
    return err  // Returns ErrCacheFull if condition fails
}

HTTP Server Management

The package helps manage HTTP server lifecycle, particularly graceful shutdown:

// Shutdown an HTTP server with a timeout
err := ctrl.ShutdownHTTPServer(ctx, server,
    ctrl.WithHTTPShutdownTimeout(5*time.Second))

// Run a server with context-aware shutdown
errCh := ctrl.RunHTTPServerWithContext(ctx, server,
    func() error { return server.ListenAndServe() },
    ctrl.WithHTTPShutdownTimeout(5*time.Second),
    ctrl.WithHTTPLogger(logger))

Graceful Shutdown

The package provides robust handling of process termination signals:

// Basic setup
ctx, cancel := ctrl.GracefulShutdown()
defer cancel()

// With custom configuration
ctx, cancel := ctrl.GracefulShutdown(
    ctrl.WithTimeout(30*time.Second),
    ctrl.WithSignals(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP),
    ctrl.WithExitCode(2),
    ctrl.WithOnShutdown(func(sig os.Signal) {
        log.Printf("shutting down due to %s signal", sig)
        database.Close()
    }),
    ctrl.WithOnForceExit(func() {
        log.Printf("force exiting after timeout")
    }),
    ctrl.WithLogger(logger))

Best Practices

Use assertions for internal invariants that should never fail in correct code:

ctrl.Assert(len(buffer) >= headerSize)  // Internal invariant

Use ErrorOr variants for validating external input or recoverable conditions:

if err := ctrl.ErrorOr(len(userInput) < maxLength); err != nil {
    return err  // External input validation
}

For HTTP servers, combine graceful shutdown with server lifecycle management:

ctx, cancel := ctrl.GracefulShutdown(ctrl.WithTimeout(10*time.Second))
defer cancel()
errCh := ctrl.RunHTTPServerWithContext(ctx, server, server.ListenAndServe)
if err := <-errCh; err != nil {
    log.Fatalf("server error: %v", err)
}
Example (GracefulShutdown)

Example_gracefulShutdown demonstrates basic usage of the GracefulShutdown function.

package main

import (
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/go-pkgz/ctrl"
)

func main() {
	// normally you would use slog.Default(), but for the example we'll create a no-op logger
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))

	// set up graceful shutdown
	_, cancel := ctrl.GracefulShutdown(
		ctrl.WithLogger(logger),
		ctrl.WithTimeout(5*time.Second),
	)
	defer cancel()

	fmt.Println("Application is running")
	fmt.Println("When SIGINT or SIGTERM is received, shutdown will be initiated")

	fmt.Println("Example complete (no actual signal sent)")

}
Output:

Application is running
When SIGINT or SIGTERM is received, shutdown will be initiated
Example complete (no actual signal sent)
Example (GracefulShutdownCustomConfiguration)

Example_gracefulShutdownCustomConfiguration demonstrates custom shutdown configuration.

package main

import (
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/go-pkgz/ctrl"
)

func main() {
	// create a noop logger for the example
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))

	// set up graceful shutdown with several options
	_, cancel := ctrl.GracefulShutdown(
		ctrl.WithLogger(logger),
		ctrl.WithTimeout(3*time.Second),
		ctrl.WithExitCode(2),           // Non-zero exit code
		ctrl.WithoutForceExit(),        // Disable forced exit
		ctrl.WithSignals(os.Interrupt), // Only listen for Ctrl+C, not SIGTERM
	)
	defer cancel()

	fmt.Println("Application running with custom shutdown configuration")
	fmt.Println("- 3 second timeout")
	fmt.Println("- Exit code 2")
	fmt.Println("- Forced exit disabled")
	fmt.Println("- Only listening for SIGINT")

	// for the example to complete
	fmt.Println("Example complete (no signal sent)")

}
Output:

Application running with custom shutdown configuration
- 3 second timeout
- Exit code 2
- Forced exit disabled
- Only listening for SIGINT
Example complete (no signal sent)
Example (GracefulShutdownWithCallbacks)

Example_gracefulShutdownWithCallbacks demonstrates using callbacks during shutdown.

package main

import (
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/go-pkgz/ctrl"
)

func main() {
	// for testing examples
	exampleDone := make(chan struct{})

	_, cancel := ctrl.GracefulShutdown(
		ctrl.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))),
		ctrl.WithTimeout(3*time.Second),
		ctrl.WithOnShutdown(func(sig os.Signal) {
			fmt.Printf("Shutdown initiated by signal: %v\n", sig)
			fmt.Println("Closing database connections...")
			time.Sleep(10 * time.Millisecond) // Simulate work
		}),
		ctrl.WithOnForceExit(func() {
			fmt.Println("Forced shutdown - cleanup incomplete!")
		}),
	)
	defer cancel()

	fmt.Println("Application running with shutdown callbacks configured")

	// for the example only, we'll manually cancel the context
	go func() {
		time.Sleep(50 * time.Millisecond)
		cancel() // Simulate a shutdown signal
		fmt.Println("Simulated shutdown signal")
		close(exampleDone)
	}()

	// for the example to complete
	<-exampleDone

	fmt.Println("Context canceled, starting graceful shutdown")

}
Output:

Application running with shutdown callbacks configured
Simulated shutdown signal
Context canceled, starting graceful shutdown
Example (HttpServerWithContext)

Example_httpServerWithContext demonstrates how to run an HTTP server that shuts down gracefully when the parent context is canceled.

package main

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"github.com/go-pkgz/ctrl"
)

func main() {
	// create a context that we can cancel to trigger shutdown
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// create a simple HTTP handler
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})

	// create server
	server := &http.Server{
		Addr:    "localhost:0", // random port
		Handler: mux,
	}

	// create a custom logger for the example
	var logBuf bytes.Buffer
	logger := slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelInfo}))

	// start server with options
	errCh := ctrl.RunHTTPServerWithContext(
		ctx,
		server,
		func() error {
			return server.ListenAndServe()
		},
		ctrl.WithHTTPShutdownTimeout(15*time.Second),
		ctrl.WithHTTPLogger(logger),
	)

	// for example only - trigger shutdown after a brief delay
	go func() {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("Triggering shutdown...")
		cancel()
	}()

	// wait for server to exit and check for errors
	err := <-errCh
	if err != nil {
		fmt.Println("Server error:", err)
	} else {
		fmt.Println("Server stopped gracefully")
	}

}
Output:

Triggering shutdown...
Server stopped gracefully

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Assert

func Assert(condition bool)

Assert panics if the condition is false.

func AssertFunc

func AssertFunc(f func() bool)

AssertFunc panics if the function returns false.

func AssertFuncf

func AssertFuncf(f func() bool, format string, args ...any)

AssertFuncf panics if the function returns false, with a formatted message.

func Assertf

func Assertf(condition bool, format string, args ...any)

Assertf panics if the condition is false, with a formatted message.

func ErrorOr

func ErrorOr(condition bool) error

ErrorOr returns nil if condition is true, otherwise returns an error.

func ErrorOrFunc

func ErrorOrFunc(f func() bool) error

ErrorOrFunc returns nil if the function returns true, otherwise returns an error.

func ErrorOrFuncWithErr

func ErrorOrFuncWithErr(f func() bool, err error) error

ErrorOrFuncWithErr returns nil if the function returns true, otherwise returns the given error.

func ErrorOrFuncf

func ErrorOrFuncf(f func() bool, format string, args ...any) error

ErrorOrFuncf returns nil if the function returns true, otherwise returns an error with a formatted message.

func ErrorOrWithErr

func ErrorOrWithErr(condition bool, err error) error

ErrorOrWithErr returns nil if condition is true, otherwise returns the given error.

func ErrorOrf

func ErrorOrf(condition bool, format string, args ...any) error

ErrorOrf returns nil if condition is true, otherwise returns an error with a formatted message.

func GracefulShutdown

func GracefulShutdown(opts ...ShutdownOption) (context.Context, context.CancelFunc)

GracefulShutdown handles process termination with graceful shutdown. It returns a context that is canceled when a termination signal is received and a cancel function that can be called to trigger shutdown manually.

func RunHTTPServerWithContext

func RunHTTPServerWithContext(ctx context.Context, server *http.Server, startFn func() error, opts ...HTTPOption) <-chan error

RunHTTPServerWithContext runs a server start function and ensures it shuts down gracefully when the provided context is canceled. The startFn is responsible for starting the server (e.g., ListenAndServe). It returns a channel that will receive any error from the server.

func ShutdownHTTPServer

func ShutdownHTTPServer(ctx context.Context, server *http.Server, opts ...HTTPOption) error

ShutdownHTTPServer gracefully shuts down an HTTP server with a timeout. It returns any error encountered during shutdown.

Types

type HTTPOption

type HTTPOption func(*httpOptions)

HTTPOption represents a functional option for HTTP server operations.

func WithHTTPLogger

func WithHTTPLogger(logger *slog.Logger) HTTPOption

WithHTTPLogger sets a custom logger for HTTP server operations.

func WithHTTPShutdownTimeout

func WithHTTPShutdownTimeout(timeout time.Duration) HTTPOption

WithHTTPShutdownTimeout sets the maximum time to wait for server shutdown.

type ShutdownOption

type ShutdownOption func(*shutdownConfig)

ShutdownOption configures shutdown behavior

func WithExitCode

func WithExitCode(code int) ShutdownOption

WithExitCode sets the exit code used for forced exits

func WithLogger

func WithLogger(logger *slog.Logger) ShutdownOption

WithLogger sets a custom slog.Logger for shutdown messages

func WithOnForceExit

func WithOnForceExit(fn func()) ShutdownOption

WithOnForceExit sets a callback function that is called right before forced exit

func WithOnShutdown

func WithOnShutdown(fn func(os.Signal)) ShutdownOption

WithOnShutdown sets a callback function that is called when shutdown begins

func WithSignals

func WithSignals(signals ...os.Signal) ShutdownOption

WithSignals sets which signals trigger the shutdown

func WithTimeout

func WithTimeout(timeout time.Duration) ShutdownOption

WithTimeout sets the maximum time to wait for graceful shutdown

func WithoutForceExit

func WithoutForceExit() ShutdownOption

WithoutForceExit disables the forced exit after timeout

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL