shutdown

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jan 23, 2024 License: MIT Imports: 8 Imported by: 1

README

Graceful shutdown utility for Go

GitHub go.mod Go version GitHub Tag GitHub Actions Workflow Status GoReportCard GitHub License

The go-shutdown package offers a collection of utilities designed for managing shutdown procedures, primarily focused on gracefully handling termination signals from the kernel, which is a common and expected use case.

Ensuring graceful shutdowns is crucial for services to support zero-downtime deployments among other reasons. During a deployment, the application that is currently running receives a SIGTERM signal and the process can gracefully finish the work in progress and stop taking new requests in order to ensure no active task is interrupted.

While this may seem straightforward, it is often overlooked when starting a new service or project. The purpose of this package is to make it as easy as writing 2-3 lines of code to handle shutdowns gracefully and also to remain flexible for more advanced usecases.

Features

Installation

go get github.com/meshapi/go-shutdown

Getting started

The way to handle shutdowns is to create and configure a shutdown pipeline and decide how it should get triggered. There is a default shutdown pipeline for convenience but you can create separate pipelines as well. Configuring the pipeline involves defining the steps and their concurrency. Finally deciding on triggering the pipeline manually or subscribing to process signals.

Configure your shutdown pipeline

To create a new shutdown pipeline use shutdown.New or simply use the package level methods to configure the default shutdown pipeline.

There are three different methods to add new steps to the shutdown procedure, each with distinct concurrency behaviors. Each has the same method signature which takes shutdown.NamedHandler instances. For logging purposes, each handler/step must have a name.

Shortcut method shutdown.HandlerWithName(string,Handler) creates a shutdown.NamedHandler from a shutdown.Handler.

Shortcut method shutdown.HandlerFuncWithName(string,HandleFunc) creates a shutdown.NamedHandler from a callback and accepts any of the following function types:

  • func()
  • func() error
  • func(context.Context)
  • func(context.Context) error

As new steps are added to the shutdown pipeline, the pipeline organizes the steps into sequential groups. Unless explicitly specified to form a distinct group, consecutive steps with the same parallel status are grouped together.

  • AddSteps: Adds steps that can run concurrently in separate goroutines.

Example: In the example below, handlers 'a' and 'b' are called concurrently.

shutdown.AddSteps(
    shutdown.HandlerFuncWithName("a", func(){}),
    shutdown.HandlerFuncWithName("b", func(){}))

NOTE: Neighboring steps with the same parallelism status are grouped together. Thus the following lines have the same effect as the code above.

shutdown.AddSteps(shutdown.HandlerFuncWithName("a", func(){}))
shutdown.AddSteps(shutdown.HandlerFuncWithName("b", func(){}))
  • AddSequence: Adds steps that run one after another in the given order, without separate goroutines.

Example: In the example below, handler 'a' is called first, followed by handler 'b'.

shutdown.AddSequence(
    shutdown.HandlerFuncWithName("a", func(){}),
    shutdown.HandlerFuncWithName("b", func(){}))
  • AddParallelSequence: Adds steps to be executed in parallel, with an explicit instruction for the manager to wait for all steps prior to finish before starting the execution of this group.

Example: With the configuration code below:

  1. Handlers 'a' and 'b' are called concurrently.
  2. After 'a' and 'b' finish, handlers 'c', 'd', and 'e' are called in parallel.
  3. After 'c', 'd', and 'e' finish, handler 'e' is called, and upon completion, handler 'f' is called.
manager := shutdown.New()

manager.AddSteps(
    shutdown.HandlerFuncWithName("a", func(){}),
    shutdown.HandlerFuncWithName("b", func(){})).
manager.AddParallelSequence(
    shutdown.HandlerFuncWithName("c", func(){}),
    shutdown.HandlerFuncWithName("d", func(){})).
manager.AddSteps(shutdown.HandlerFuncWithName("e", func(){})).
manager.AddSequence(shutdown.HandlerFuncWithName("f", func(){}))

NOTE: Many servers have a graceful shutdown method and they can be used with shutdown.HandlerFuncWithName(string,HandleFunc) method. The code below is an example of an HTTP server shutdown:

httpServer := &http.Server{}
go func() {
    if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("HTTP server failed: %s", err)
    }
}()

shutdown.AddSteps(shutdown.HandlerFuncWithName("http", httpServer.Shutdown))
shutdown.WaitForInterrupt()
Shutdown trigger

To manually trigger a shutdown, use Trigger(context.Context) method on a shutdown manager instance. shutdown.Trigger is a shortcut method to trigger the default pipeline's shutdown procedure.

To trigger the shutdown when a SIGTERM is received, simply call WaitForInterrupt() method. This method blocks until SIGTERM signal is received and the shutdown pipeline has concluded or deadline reached.

manager := shutdown.New()

manager.Trigger(context.Background()) // manual trigger.
manager.WaitForInterrupt()            // block until SIGTERM is received and shutdown procedures have finished.
Logging

In order to remain flexible with your logging tool, no choice over the logger is made here, instead any type that has Info and Error methods, can be used as a logger.

The default shutdown pipeline uses the log package from the standard library but when you create new instances, no logger is set and when no logger is available, no logging will be made.

Use the SetLogger(Logger) method to set a logger.

var myLogger shutdown.Logger

manager := shutdown.New()
manager.SetLogger(myLogger)  // set the logger on the newly created shutdown pipeline.

shutdown.SetLogger(myLogger) // update the logger on the default pipeline.
Timeout

Use method SetTimeout(time.Duration) to set a deadline on the shutdown pipeline's completion time. By default this is not specified.

shutdown.SetTimeout(15*time.Second)
Completion callback

A completion function callback can be set via SetCompletionFunc. This function will get called in any case, even if there are panics, errors or timeouts.


Contributions

Contributions are absolutely welcome and please write tests for the new functionality added/modified. Additionally, we ask that you include a test function reproducing the issue raised to save everyone's time.

Documentation

Overview

Package shutdown contains utilities to gracefully handle kernel interrupts and shutdown procedures.

This package provides means to define a shutdown pipeline, which can contain sequential and parallel steps to complete a shutdown and also ways to trigger the pipeline either via signals or manually.

To create a pipeline manager, use the following code:

shutdownManager := New()

There is a default shutdown manager that can be accessed via Default() method and many package-level functions are available as a shortcut to accessing the default manager's methods. The default manager uses the standard log package for logging.

The shutdown manager allows the addition of steps to the shutdown procedure, with some steps capable of running in parallel, while others run sequentially. The shutdown procedure can be triggered manually or by subscribing to kernel SIGTERM interrupt signals.

There are three main methods for adding steps into the shutdown manager. The shutdown manager monitors the added steps and organizes them into sequential groups. Unless explicitly specified to form a distinct group, consecutive steps with the same parallel status are grouped together.

* AddSteps: Adds steps that can run concurrently in separate goroutines.

Example:
In the example below, handlers 'a' and 'b' are called concurrently.

AddSteps(
	HandlerFuncWithName("a", func(){}),
	HandlerFuncWithName("b", func(){}))

NOTE: Neighboring steps with the same parallelism status are grouped together. Thus the following lines have the same effect as the code above.

AddSteps(HandlerFuncWithName("a", func(){}))
AddSteps(HandlerFuncWithName("b", func(){}))

* AddSequence: Adds steps that run one after another in the given order, without separate goroutines.

Example:
In the example below, handler 'a' is called first, followed by handler 'b'.

AddSequence(
	HandlerFuncWithName("a", func(){}),
	HandlerFuncWithName("b", func(){}))

* AddParallelSequence: Adds steps to be executed in parallel, with an explicit instruction for the manager to wait for all steps prior to finish before starting the execution of this group.

Example:
In the example below handlers:
	1. Handlers 'a' and 'b' are called concurrently.
	2. After 'a' and 'b' finish, handlers 'c', 'd', and 'e' are called in parallel.
	3. After 'c', 'd', and 'e' finish, handler 'e' is called, and upon completion, handler 'f' is called.

AddSteps(
	HandlerFuncWithName("a", func(){}),
	HandlerFuncWithName("b", func(){})).
AddParallelSequence(
	HandlerFuncWithName("c", func(){}),
	HandlerFuncWithName("d", func(){})).
AddSteps(HandlerFuncWithName("e", func(){})).
AddSequence(HandlerFuncWithName("f", func(){}))

Additional features include: * SetLogger: Set a custom logger for the shutdown pipeline (default is plog.Default()). * SetTimeout: Define a timeout for the execution of the entire shutdown pipeline. * SetCompletionFunc: Add a callback function to handle the end of the shutdown pipeline, ensuring it gets called in any case, even in the presence of panics, errors, or timeouts.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddParallelSequence

func AddParallelSequence(handlers ...NamedHandler)

AddParallelSequence is similar to AddSequence but it will execute the handlers all at the same time. AddParallelSequence(a) and AddParallelSequence(b) is not the same as AddParallelSequence(a, b). In the former, a runs and upon completion, b starts whereas in the latter case a and b both get started at the same time.

func AddSequence

func AddSequence(handlers ...NamedHandler)

AddSequence adds sequencial steps meaning that these handlers will be executed one at a time and in the same order given. Calling AddSequence(a) and AddSequence(b) is same as AddSequence(a, b)

func AddSteps

func AddSteps(handlers ...NamedHandler)

AddSteps adds parallel shutdown steps. These steps will be executed at the same time together or along with previously added steps if they are also able to run in parallel. In another word, calling AddSteps(a) and AddSteps(b) is same as AddSteps(a, b)

func SetCompletionFunc

func SetCompletionFunc(f func())

SetCompletionFunc sets a function to get called after all of the shutdown steps have been executed. Regardless of panics or errors, this function will always get executed as the very last step. Even when a the pipeline times out, this function gets called before returning.

func SetLogger

func SetLogger(logger Logger)

SetLogger sets the shutdown logger. If set to nil, no logs will be written.

func SetTimeout

func SetTimeout(duration time.Duration)

SetTimeout sets the shutdown pipeline timeout. This indicates that when shutdown is triggered, the entire pipeline iteration must finish within the duration specified.

NOTE: If the pipeline times out, the shutdown method is still called and some of the steps in the pipeline will still get scheduled but the blocking method (Trigger or WaitForInterrupt) will return immediately without waiting for the rest of the shutdown steps to complete.

func Trigger

func Trigger(ctx context.Context)

Trigger starts the shutdown pipeline immediately. It will acquire a lock on the pipeline so all changes to the pipeline get blocked until the pipeline has completed. Panics and errors are all handled.

func WaitForInterrupt

func WaitForInterrupt()

WaitForInterrupt blocks until an interrupt signal is received and all shutdown steps have been executed.

Types

type HandleFunc

type HandleFunc interface {
	func() | func() error | func(context.Context) | func(context.Context) error
}

HandleFunc describes various different function signatures that can be used as a shutdown handler function.

type Handler

type Handler interface {
	HandleShutdown(ctx context.Context) error
}

Handler describes ability to handle a graceful shutdown.

type Logger

type Logger interface {
	// Info writes an information log.
	Info(text string)

	// Error writes an error log.
	Error(text string)
}

Logger describes ability to write logs.

type Manager

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

Manager is a shutdown pipeline manager that can be configured to run through a number of parallel and sequencial steps when a shutdown is triggered. In order to start the shutdown procedure upon receiving a kernel Interrupt signal, use WaitForInterrupt() blocking method.

func Default

func Default() *Manager

Default returns the default manager. This method is thread-safe.

func New

func New() *Manager

New creates a new shutdown pipeline.

func (*Manager) AddParallelSequence

func (m *Manager) AddParallelSequence(handlers ...NamedHandler)

AddParallelSequence is similar to AddSequence but it will execute the handlers all at the same time. AddParallelSequence(a) and AddParallelSequence(b) is not the same as AddParallelSequence(a, b). In the former, a runs and upon completion, b starts whereas in the latter case a and b both get started at the same time.

func (*Manager) AddSequence

func (m *Manager) AddSequence(handlers ...NamedHandler)

AddSequence adds sequencial steps meaning that these handlers will be executed one at a time and in the same order given. Calling AddSequence(a) and AddSequence(b) is same as AddSequence(a, b)

func (*Manager) AddSteps

func (m *Manager) AddSteps(handlers ...NamedHandler)

AddSteps adds parallel shutdown steps. These steps will be executed at the same time together or along with previously added steps if they are also able to run in parallel. In another word, calling AddSteps(a) and AddSteps(b) is same as AddSteps(a, b)

func (*Manager) SetCompletionFunc

func (m *Manager) SetCompletionFunc(f func())

SetCompletionFunc sets a function to get called after all of the shutdown steps have been executed. Regardless of panics or errors, this function will always get executed as the very last step. Even when a the pipeline times out, this function gets called before returning.

func (*Manager) SetLogger

func (m *Manager) SetLogger(logger Logger)

SetLogger sets the shutdown logger. If set to nil, no logs will be written.

func (*Manager) SetTimeout

func (m *Manager) SetTimeout(duration time.Duration)

SetTimeout sets the shutdown pipeline timeout. This indicates that when shutdown is triggered, the entire pipeline iteration must finish within the duration specified.

NOTE: If the pipeline times out, the shutdown method is still called and some of the steps in the pipeline will still get scheduled but the blocking method (Trigger or WaitForInterrupt) will return immediately without waiting for the rest of the shutdown steps to complete.

func (*Manager) Trigger

func (m *Manager) Trigger(ctx context.Context)

Trigger starts the shutdown pipeline immediately. It will acquire a lock on the pipeline so all changes to the pipeline get blocked until the pipeline has completed. Panics and errors are all handled.

func (*Manager) WaitForInterrupt

func (m *Manager) WaitForInterrupt()

WaitForInterrupt blocks until an interrupt signal is received and all shutdown steps have been executed.

type NamedHandler

type NamedHandler interface {
	Handler

	Name() string
}

NamedHandler is a handler that has a specific name.

func HandlerFuncWithName

func HandlerFuncWithName[H HandleFunc](name string, handleFunc H) NamedHandler

HandlerFuncWithName returns a named handler for a function with a specific name.

Accepted function signatures: func () func (context.Context) func () error func (context.Context) error

func HandlerWithName

func HandlerWithName(name string, handler Handler) NamedHandler

HandlerWithName returns a named handler with a specific name.

type StandardLogger

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

StandardLogger is a logger that uses the standard log package.

func NewStandardLogger

func NewStandardLogger(logger *log.Logger) StandardLogger

NewStandardLogger creates a logger wrapping a log.Logger instance.

func (StandardLogger) Error

func (d StandardLogger) Error(text string)

func (StandardLogger) Info

func (d StandardLogger) Info(text string)

Jump to

Keyboard shortcuts

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