Skip to content

maxbolgarin/contem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 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
    
    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() {
    srv := &http.Server{Addr: ":8080"}
    
    // Signal handling boilerplate
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err) // ❌ log.Fatal ignores cleanup!
        }
    }()
    
    <-quit
    log.Println("Shutting down server...")
    
    // Manual cleanup with timeout handling
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    
    // What about database connections? File handles? Other resources?
    // More cleanup code needed...
}

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
    
    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

package main

import (
    "database/sql"
    "log/slog"
    "net/http"
    "time"
    
    "github.com/maxbolgarin/contem"
    _ "github.com/lib/pq"
)

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

func run(ctx contem.Context) error {
    logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        return err
    }

    // 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
    slog.SetDefault(slog.New(slog.NewTextHandler(logFile, nil))) 
    
    db, err := sql.Open("postgres", "postgres://localhost/mydb?sslmode=disable")
    if err != nil {
        return err // Will log error and close logFile because it has been added to the context
    }
    // 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) 

    return nil
}

🔧 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

🔍 Troubleshooting

Common Issues

Q: My shutdown functions are timing out

// Increase timeout
ctx := contem.New(contem.WithShutdownTimeout(60*time.Second))

Q: I need custom signals

// Listen for custom signals
ctx := contem.New(contem.WithSignals(syscall.SIGUSR1, syscall.SIGUSR2))

Q: Shutdown is too slow

// Files close after other resources by default
// To close everything together:
ctx := contem.New(contem.RegularCloseFilesOrder())

Q: I want to see what's happening during shutdown

// Add logging
ctx := contem.New(contem.WithLogger(slog.Default()))

🔄 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