Skip to content

appleboy/graceful

Repository files navigation

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

About

graceful shutdown package when a service is turned off by software function

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages