graceful

package module
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Feb 7, 2026 License: MIT Imports: 9 Imported by: 18

README

graceful

English | 繁體中文 | 简体中文

Run Tests codecov Go Report Card Go Reference

A lightweight Go package for graceful shutdown and job management. Easily manage long-running jobs and shutdown hooks, ensuring your services exit cleanly and predictably.


Table of Contents


Features

  • Graceful shutdown for Go services with automatic signal handling (SIGINT, SIGTERM)
  • Timeout protection - configurable timeout to prevent indefinite hanging (default: 30s)
  • Multiple shutdown protection - ensures shutdown logic only runs once, even with multiple signals
  • Context-based cancellation - running jobs receive context cancellation signals
  • Parallel shutdown hooks - cleanup tasks run concurrently for faster shutdown
  • Error reporting - collect and report all errors from jobs
  • Custom logger support - integrate with your existing logging solution
  • Thread-safe - all operations are safe for concurrent use
  • Zero dependencies - lightweight and minimal
  • Simple API - easy integration with existing services

Installation

go get github.com/appleboy/graceful

Usage

Basic Usage

Create a manager and wait for graceful shutdown:

package main

import (
  "context"
  "log"
  "time"

  "github.com/appleboy/graceful"
)

func main() {
  // Create a manager with default settings
  m := graceful.NewManager()

  // Add your jobs...

  // Wait for shutdown to complete (blocks until SIGINT/SIGTERM received)
  <-m.Done()

  // Check for errors
  if errs := m.Errors(); len(errs) > 0 {
    log.Printf("Shutdown completed with %d error(s)", len(errs))
    for _, err := range errs {
      log.Printf("  - %v", err)
    }
  }

  log.Println("Service stopped gracefully")
}
Add Running Jobs

Register long-running jobs that will be cancelled on shutdown:

package main

import (
  "context"
  "log"
  "time"

  "github.com/appleboy/graceful"
)

func main() {
  m := graceful.NewManager()

  // Add job 01
  m.AddRunningJob(func(ctx context.Context) error {
    for {
      select {
      case <-ctx.Done():
        return nil
      default:
        log.Println("working job 01")
        time.Sleep(1 * time.Second)
      }
    }
  })

  // Add job 02
  m.AddRunningJob(func(ctx context.Context) error {
    for {
      select {
      case <-ctx.Done():
        return nil
      default:
        log.Println("working job 02")
        time.Sleep(500 * time.Millisecond)
      }
    }
  })

  <-m.Done()
}
Add Shutdown Jobs

Register shutdown hooks to run cleanup logic before exit:

package main

import (
  "context"
  "log"
  "time"

  "github.com/appleboy/graceful"
)

func main() {
  m := graceful.NewManager()

  // Add running jobs (see above)

  // Add shutdown 01
  m.AddShutdownJob(func() error {
    log.Println("shutdown job 01 and wait 1 second")
    time.Sleep(1 * time.Second)
    return nil
  })

  // Add shutdown 02
  m.AddShutdownJob(func() error {
    log.Println("shutdown job 02 and wait 2 second")
    time.Sleep(2 * time.Second)
    return nil
  })

  <-m.Done()
}
Configure Shutdown Timeout

Set a maximum time to wait for graceful shutdown (default: 30 seconds):

package main

import (
  "time"
  "github.com/appleboy/graceful"
)

func main() {
  // Set 10 second timeout
  m := graceful.NewManager(
    graceful.WithShutdownTimeout(10 * time.Second),
  )

  // Or disable timeout (wait indefinitely)
  m := graceful.NewManager(
    graceful.WithShutdownTimeout(0),
  )

  // ... add jobs ...

  <-m.Done()

  // Check if timeout occurred
  if errs := m.Errors(); len(errs) > 0 {
    for _, err := range errs {
      if err.Error() == "shutdown timeout exceeded: 10s" {
        log.Println("Some jobs did not complete within timeout")
      }
    }
  }
}

Why timeout matters:

  • Prevents indefinite hanging if a job doesn't respond to cancellation
  • Critical for containerized environments (Kubernetes terminationGracePeriodSeconds)
  • Ensures predictable shutdown behavior in production
Error Handling

Access all errors that occurred during shutdown:

package main

import (
  "log"
  "github.com/appleboy/graceful"
)

func main() {
  m := graceful.NewManager()

  m.AddRunningJob(func(ctx context.Context) error {
    // ... do work ...
    return fmt.Errorf("something went wrong")  // Error will be collected
  })

  m.AddShutdownJob(func() error {
    // ... cleanup ...
    return nil
  })

  <-m.Done()

  // Get all errors (includes job errors, panics, and timeout errors)
  errs := m.Errors()
  if len(errs) > 0 {
    log.Printf("Shutdown errors: %v", errs)
    os.Exit(1)  // Exit with error code
  }
}

Error types collected:

  • Errors returned by running jobs
  • Errors returned by shutdown jobs
  • Panics recovered from jobs (converted to errors)
  • Timeout errors if shutdown exceeds configured duration
Custom Logger

You can use your own logger (see zerolog example):

m := graceful.NewManager(
  graceful.WithLogger(logger{}),
)

Configuration Options

All configuration is done through functional options passed to NewManager():

Option Description Default
WithContext(ctx) Use a custom parent context. Shutdown triggers when context is cancelled. context.Background()
WithLogger(logger) Use a custom logger implementation. Built-in logger
WithShutdownTimeout(duration) Maximum time to wait for graceful shutdown. Set to 0 for no timeout. 30 * time.Second

Example with multiple options:

m := graceful.NewManager(
  graceful.WithContext(ctx),
  graceful.WithShutdownTimeout(15 * time.Second),
  graceful.WithLogger(customLogger),
)

Examples


Best Practices

1. Always Wait for Done()
m := graceful.NewManager()
// ... add jobs ...
<-m.Done()  // ✅ REQUIRED: Wait for shutdown to complete

Why: If your program exits before calling <-m.Done(), cleanup may not complete, leading to:

  • Resource leaks (open connections, files)
  • Data loss (unflushed buffers)
  • Orphaned goroutines
2. Respond to Context Cancellation
m.AddRunningJob(func(ctx context.Context) error {
  ticker := time.NewTicker(1 * time.Second)
  defer ticker.Stop()

  for {
    select {
    case <-ctx.Done():
      // ✅ Always handle ctx.Done() to enable graceful shutdown
      log.Println("Shutting down gracefully...")
      return ctx.Err()
    case <-ticker.C:
      // Do work
    }
  }
})

Why: Jobs that don't respect ctx.Done() will block shutdown until timeout is reached.

3. Make Shutdown Jobs Idempotent
m.AddShutdownJob(func() error {
  // ✅ Safe to call multiple times (though graceful ensures it's only called once)
  if db != nil {
    db.Close()
    db = nil
  }
  return nil
})

Why: Although the manager ensures shutdown jobs only run once, defensive coding prevents issues.

4. Set Appropriate Timeout
// For Kubernetes pods with terminationGracePeriodSeconds: 30
m := graceful.NewManager(
  graceful.WithShutdownTimeout(25 * time.Second),  // ✅ Leave 5s buffer for SIGKILL
)

Why: If your shutdown timeout exceeds the container termination period, the process will be forcefully killed (SIGKILL).

5. Check Errors After Shutdown
<-m.Done()

if errs := m.Errors(); len(errs) > 0 {
  log.Printf("Shutdown errors: %v", errs)
  os.Exit(1)  // ✅ Exit with error code for monitoring/alerting
}

Why: Allows you to detect and respond to shutdown issues in production.

6. Shutdown Order with Multiple Jobs

Shutdown jobs run in parallel by design. If you need sequential shutdown:

m.AddShutdownJob(func() error {
  // Do all shutdown in sequence within a single job
  stopAcceptingRequests()
  waitForInflightRequests()
  closeDatabase()
  flushLogs()
  return nil
})

Why: Parallel execution is faster, but some cleanup requires specific ordering.


License

MIT

Documentation

Overview

Package graceful provides a Logger implementation using Go's log/slog.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Logger

type Logger interface {
	Infof(format string, args ...interface{})
	Errorf(format string, args ...interface{})
}

Logger interface is used throughout gorush

func NewEmptyLogger

func NewEmptyLogger() Logger

NewEmptyLogger for simple logger.

func NewLogger

func NewLogger() Logger

NewLogger for simple logger.

func NewSlogLogger added in v1.2.0

func NewSlogLogger(opts ...SlogLoggerOption) Logger

NewSlogLogger creates a Logger using flexible option pattern.

Usage:

NewSlogLogger()                        // text mode (default)
NewSlogLogger(WithJSON())              // json mode
NewSlogLogger(WithSlog(loggerObj))     // inject custom *slog.Logger, which overrides other options

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager manages the graceful shutdown process.

The Manager uses a singleton pattern - only one instance can exist per process. It handles OS signals (SIGINT, SIGTERM) and context cancellation to trigger shutdown.

Shutdown behavior:

  • When a shutdown signal is received, all running jobs receive context cancellation
  • Running jobs should respect context.Done() and exit gracefully
  • After running jobs complete, shutdown jobs are executed in parallel
  • If shutdown timeout is reached, remaining jobs are interrupted

Signal handling:

  • Unix: SIGINT (Ctrl+C), SIGTERM (kill), SIGTSTP
  • Windows: SIGINT, SIGTERM
  • Note: This will override any existing signal.Notify() for these signals

Context behavior:

  • If the parent context (from WithContext) is cancelled, shutdown is triggered
  • ShutdownContext() returns a context that is cancelled when shutdown starts
  • Done() returns a channel that is closed when all jobs complete

func GetManager

func GetManager() *Manager

GetManager returns the existing Manager instance.

This will panic if NewManager() has not been called first. Use this in places where you need access to the manager but can't pass it directly.

Example:

// In main.go
m := graceful.NewManager()

// In another package
m := graceful.GetManager()
m.AddShutdownJob(cleanup)

func NewManager

func NewManager(opts ...Option) *Manager

NewManager creates and initializes the graceful shutdown Manager.

This function uses a singleton pattern - calling it multiple times returns the same instance. The Manager automatically starts listening for OS signals (SIGINT, SIGTERM) and will trigger graceful shutdown when received.

Options:

  • WithContext(ctx): Use a custom parent context. Shutdown triggers when context is cancelled.
  • WithLogger(logger): Use a custom logger implementation.
  • WithShutdownTimeout(duration): Set maximum time to wait for shutdown (default: 30s).

Example:

m := graceful.NewManager(
    graceful.WithShutdownTimeout(10 * time.Second),
    graceful.WithLogger(customLogger),
)

Important: Only one Manager can exist per process.

func NewManagerWithContext added in v0.0.2

func NewManagerWithContext(ctx context.Context, opts ...Option) *Manager

NewManagerWithContext initial the Manager with custom context

func (*Manager) AddRunningJob

func (g *Manager) AddRunningJob(f RunningJob)

AddRunningJob adds a long-running task that will receive shutdown signals via context.

Running jobs should:

  • Monitor ctx.Done() and exit gracefully when signaled
  • Return an error if something goes wrong
  • Clean up resources before returning

Example:

m.AddRunningJob(func(ctx context.Context) error {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ctx.Done():
            return nil  // Graceful exit
        case <-ticker.C:
            // Do work
        }
    }
})

Note: This method is thread-safe. Panics are recovered and converted to errors.

func (*Manager) AddShutdownJob added in v0.0.2

func (g *Manager) AddShutdownJob(f ShutdownJob)

AddShutdownJob adds a shutdown task that will be executed when graceful shutdown is triggered.

Shutdown jobs are executed in parallel after all running jobs have completed. Each shutdown job should be idempotent as they may be called during unexpected shutdowns.

Note: This method is thread-safe and can be called from multiple goroutines. However, jobs added after shutdown has started will not be executed.

func (*Manager) Done

func (g *Manager) Done() <-chan struct{}

Done returns a channel that is closed when all jobs (running + shutdown) have completed.

This should be used to block the main goroutine until graceful shutdown is complete:

m := graceful.NewManager()
// ... add jobs ...
<-m.Done()  // Block until shutdown completes
errs := m.Errors()  // Check for errors

Warning: If you don't wait for Done(), your program may exit before cleanup completes, potentially causing goroutine leaks or incomplete shutdown.

func (*Manager) Errors added in v1.3.0

func (g *Manager) Errors() []error

Errors returns all errors that occurred during running jobs and shutdown jobs.

This includes:

  • Errors returned by running jobs
  • Errors returned by shutdown jobs
  • Panics recovered from jobs (converted to errors)
  • Timeout errors if shutdown exceeded the configured timeout

The returned slice is a copy, so modifying it won't affect the internal state.

Example:

<-m.Done()
if errs := m.Errors(); len(errs) > 0 {
    for _, err := range errs {
        log.Printf("Shutdown error: %v", err)
    }
    os.Exit(1)
}

func (*Manager) ShutdownContext added in v0.0.4

func (g *Manager) ShutdownContext() context.Context

ShutdownContext returns a context that is cancelled when shutdown begins.

Use this context for operations that should be cancelled during shutdown. This is the same context passed to running jobs.

Example:

ctx := m.ShutdownContext()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
// Request will be cancelled when shutdown starts

type Option

type Option interface {
	Apply(*Options)
}

Option interface for configuration.

func WithContext

func WithContext(ctx context.Context) Option

WithContext custom context

func WithLogger

func WithLogger(logger Logger) Option

WithLogger custom logger

func WithShutdownTimeout added in v1.3.0

func WithShutdownTimeout(timeout time.Duration) Option

WithShutdownTimeout sets the maximum duration to wait for graceful shutdown to complete. If timeout is reached, the shutdown will proceed anyway and remaining jobs will be interrupted. A timeout of 0 means no timeout (wait indefinitely). Default is 30 seconds.

Example:

m := graceful.NewManager(
    graceful.WithShutdownTimeout(10 * time.Second),
)

type OptionFunc added in v0.1.0

type OptionFunc func(*Options)

OptionFunc is a function that configures a graceful shutdown.

func (OptionFunc) Apply added in v0.1.0

func (f OptionFunc) Apply(option *Options)

Apply calls f(option)

type Options added in v0.1.0

type Options struct {
	// contains filtered or unexported fields
}

Options for graceful shutdown

type RunningJob

type RunningJob func(context.Context) error

type ShutdownJob added in v1.3.0

type ShutdownJob func() error

type SlogLoggerOption added in v1.2.0

type SlogLoggerOption func(*slogLoggerOptions)

SlogLoggerOption applies configuration to NewSlogLogger.

func WithJSON added in v1.2.0

func WithJSON() SlogLoggerOption

WithJSON returns an option to set output as JSON format.

func WithSlog added in v1.2.0

func WithSlog(logger *slog.Logger) SlogLoggerOption

WithSlog injects a custom *slog.Logger instance.

Jump to

Keyboard shortcuts

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