Skip to content

maxbolgarin/contem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

43 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

contem - Graceful Shutdown Made Simple

Go Version GoDoc Build Coverage GoReport

contem logo

contem is a zero-dependency, drop-in replacement for context.Context that makes graceful shutdown trivial. Stop worrying about signal handling, resource cleanup, and shutdown coordinationβ€”just add your cleanup functions and let contem handle the rest.

πŸš€ Quick Start

go get -u github.com/maxbolgarin/contem

The Simplest Example

package main

import (
    "log/slog"
    "net/http"
    "github.com/maxbolgarin/contem"
)

func main() {
    contem.Start(run, slog.Default())
}

func run(ctx contem.Context) error {
    srv := &http.Server{Addr: ":8080"}
    ctx.Add(srv.Shutdown) // That's it! Server will shutdown gracefully on Ctrl+C
    
    // Make run() function non-blocking
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("Server failed", "error", err)
            ctx.Cancel() // Trigger graceful shutdown of the whole application on error in an another goroutine
        }
    }()
    
    return nil
}

Press Ctrl+C and watch your server shutdown gracefully! πŸŽ‰

πŸ” Why contem?

The Problem

Traditional Go applications require boilerplate for graceful shutdown:

// ❌ Traditional approach - lots of boilerplate
func main() {
    file, err := os.OpenFile("important.log", ...) // Open file
    if err != nil {
        log.Fatalf("Cannot open file: %v", err)
    }

    defer func() {
        if err := file.Close(); err != nil {
            log.Fatalf("Cannot close file: %v", err)
        }
    }()

    db, err := sql.Open("postgres", "...")
    if err != nil {
        // log.Fatal() ignores defer statements, GC closes file after returning from main()), but it is not a good practice!
        log.Fatalf("Cannot open database: %v", err)
    }
    defer func() {
        // Close should be idempotent
        if err := db.Close(); err != nil {
            log.Fatalf("Cannot close database: %v", err)
        }
    }()

    app, err := some.Init(db, file)
    if err != nil {
        // log.Fatal() ignores defer statements, so we need to close database manually
        if err := db.Close(); err != nil {
            log.Fatalf("Cannot close database: %v", err)
        }
        log.Fatalf("Cannot initialize application: %v", err)
    }
    
    // Signal handling boilerplate
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    go some.Run(ctx) // Run some other goroutine
    
    <-ctx.Done()
    log.Println("Shutting down application...")
    
    // Manual cleanup with timeout handling
    ctx2, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx2); err != nil {
        // log.Fatal() ignores defer statements, so we need to close database manually
        if err := db.Close(); err != nil {
            log.Fatalf("Cannot close database: %v", err)
        }
        log.Fatalf("Server forced to shutdown: %v", err)
    }
}

The Solution

// βœ… contem approach - clean and simple
func main() {
    contem.Start(run, slog.Default())
}

func run(ctx contem.Context) error {
    file, err := os.OpenFile("important.log", ...) // Open file
    if err != nil {
        return err
    }
    ctx.AddFile(file) // File will be synced and closed on shutdown
    
    db, err := sql.Open("postgres", "...")
    if err != nil {
        return err
    }
    ctx.AddClose(db.Close) // Database will close gracefully too

    app, err := some.Init(db, file)
    if err != nil {
        return err
    }
    ctx.Add(app.Shutdown) // Shutdown will be called on Ctrl+C

    go some.Run(ctx) // Run some other goroutine
    
    return nil
}

🌟 Key Benefits

🎯 Drop-in Replacement

Replace context.Context with contem.Context in your function signatures. Everything else works the same.

⚑ Zero Dependencies

Pure Go standard library. No external dependencies to worry about.

πŸ›‘οΈ Bulletproof Error Handling

  • No more log.Fatal() ignoring your cleanup code
  • Automatic timeout handling for shutdown functions
  • Proper error aggregation and reporting

πŸ”„ Resource Management

  • Automatic cleanup of databases, files, connections
  • Proper shutdown order (general resources first, files last)
  • Parallel shutdown for better performance (you can disable it with contem.WithNoParallel() option)

πŸ“ File Safety

Special handling for files that need syncing before closing:

file, _ := os.Create("important.log")
ctx.AddFile(file) // Automatically syncs AND closes on shutdown

πŸŽ›οΈ Flexible Configuration

Fine-tune behavior with options:

ctx := contem.New(
    contem.WithLogger(logger),
    contem.WithShutdownTimeout(30*time.Second),
    contem.WithExit(&err), // Exit with proper code
)

πŸ“š Complete Example: Web Server with Database without Start() function

package main

import (
	"database/sql"
	"log/slog"
	"net/http"
	"os"

	"github.com/maxbolgarin/contem"
)

func main() {
	var err error

	// Create a new context with Exit option to exit with code 1 in case of error (no need for log.Fatal())
	ctx := contem.New(contem.Exit(&err), contem.WithLogger(slog.Default()))
	defer ctx.Shutdown()

	logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		slog.Error("cannot open log file", "error", err)
		return
	}

	// Automatically syncs and closes file in the last order to log every error during shutdown
	ctx.AddFile(logFile)

	// Set default logger to use the file
	fileLogger := slog.New(slog.NewTextHandler(logFile, nil))
	fileLogger.Info("Starting application")

	db, err := sql.Open("sqlite3", "app.db")
	if err != nil {
		slog.Error("cannot open database", "error", err)
		return
	}
	// Will close database connection gracefully on shutdown
	ctx.AddClose(db.Close)

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

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

	go func() {
		slog.Info("Server starting", "addr", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			slog.Error("Server failed", "error", err)
			ctx.Cancel() // Trigger graceful shutdown and release all added resources
		}
	}()

	// Will shutdown HTTP server gracefully on Ctrl+C
	ctx.Add(srv.Shutdown)

	ctx.Wait() // Wait for Ctrl+C
	// it will start defered shutdown function, which will make graceful shutdown of the whole application
}

πŸ”§ API Reference

Core Functions

Function Description
contem.Start(run, logger, opts...) Easiest way to start an app with graceful shutdown
contem.New(opts...) Create a new context with options
contem.NewEmpty() Create empty context for testing

Context Methods

Method Description
Add(ShutdownFunc) Add function that accepts context and returns error
AddClose(CloseFunc) Add function that returns error (like io.Closer)
AddFunc(func()) Add simple function with no return
AddFile(File) Add file that needs sync + close
Wait() Block until interrupt signal received
Cancel() Manually trigger shutdown (not recommended)
Shutdown() Execute all shutdown functions

Configuration Options

Option Description
WithLogger(logger) Add structured logging
WithShutdownTimeout(duration) Set shutdown timeout (default: 15s)
WithExit(&err, code...) Exit process after shutdown
WithAutoShutdown() Auto-shutdown when context cancelled
WithNoParallel() Disable parallel shutdown
WithSignals(signals...) Custom signals (default: SIGINT, SIGTERM)
WithDontCloseFiles() Skip file closing
WithRegularCloseFilesOrder() Close files with other resources

πŸ”„ Migration Guide

From Standard Context

// Before
func MyFunction(ctx context.Context) error {
    // ...
}

// After - just change the type!
func MyFunction(ctx contem.Context) error {
    // All context.Context methods still work
    // Plus you get Add(), AddClose(), etc.
}

From Manual Signal Handling

// Before
func main() {
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    
    // ... setup code ...
    
    <-quit
    // Manual cleanup
}

// After
func main() {
    contem.Start(run, logger)
}

func run(ctx contem.Context) error {
    // Setup code + add cleanup functions
    return nil
}

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

πŸ“„ License

This project is licensed under the MIT License. See the LICENSE file for details.


⭐ Star this repo if it helped you build better Go applications!

About

Zero-dependency drop-in context.Context replacement for graceful shutdown

Topics

Resources

License

Stars

Watchers

Forks

Languages