graterm

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2022 License: MIT Imports: 9 Imported by: 0

README

graterm

Lint Tests codecov Go Report Card License GoDoc Release

Provides primitives to perform ordered GRAceful TERMination (aka shutdown) in Go application.

⚡ ️️Description

Library provides fluent methods to register ordered application termination (aka shutdown) hooks, and block the main goroutine until the registered os.Signal will occur.

Termination hooks registered with the same Order will be executed concurrently.

It is possible to set individual timeouts for each registered termination hook and global termination timeout for the whole application.

🎯 Features

  • Dependency only on a standard Go library (except tests).
  • Component-agnostic (can be adapted to any 3rd party technology).
  • Clean and tested code: 100% test coverage, including goroutine leak tests.
  • Rich set of examples.

⚙️ Usage

Get the library:

go get -u github.com/skovtunenko/graterm

Import the library into the project:

import (
    "github.com/skovtunenko/graterm"
)

Create a new instance of Terminator and get an application context that will be cancelled when one of the registered os.Signals will occur:

// create new Terminator instance:
terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
terminator.SetLogger(log.Default()) // Optionally set the custom logger implementation instead of default NOOP one

Optionally define Order of components to be terminated at the end:

const (
    HTTPServerTerminationOrder graterm.Order = 1
    MessagingTerminationOrder  graterm.Order = 1
    DBTerminationOrder         graterm.Order = 2
	// ..........
)

Register some termination Hooks with priorities:

terminator.WithOrder(HTTPServerTerminationOrder).
    WithName("HTTP Server"). // setting a Name is optional and will be useful only if logger instance provided
    Register(1*time.Second, func(ctx context.Context) {
        if err := httpServer.Shutdown(ctx); err != nil {
            log.Printf("shutdown HTTP Server: %+v\n", err)
        }
    })

Block main goroutine until the application receives one of the registered os.Signals:

if err := terminator.Wait(appCtx, 20*time.Second); err != nil {
    log.Printf("graceful termination period was timed out: %+v", err)
}

👀 Versioning

The library follows SemVer policy. With the release of v1.0.0 the public API is stable.

📚 Example

Each public function has example attached to it. Here is the simple one:

package main

import (
    "context"
    "log"
    "syscall"
    "time"

    "github.com/skovtunenko/graterm"
)

func main() {
	// Define Orders:
	const (
		HTTPServerTerminationOrder graterm.Order = 1
		MessagingTerminationOrder  graterm.Order = 1
		DBTerminationOrder         graterm.Order = 2
	)

	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	terminator.SetLogger(log.Default()) // Optional step

	// Register HTTP Server termination hook:
	terminator.WithOrder(HTTPServerTerminationOrder).
		WithName("HTTP Server"). // setting a Name is optional and will be useful only if logger instance provided
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating HTTP Server...")
			defer log.Println("...HTTP Server terminated")
		})

	// Register nameless Messaging termination hook:
	terminator.WithOrder(MessagingTerminationOrder).
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating Messaging...")
			defer log.Println("...Messaging terminated")
		})

	// Register Database termination hook:
	terminator.WithOrder(DBTerminationOrder).
		WithName("DB"). // setting a Name is optional and will be useful only if logger instance provided
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating DB...")
			defer log.Println("...DB terminated")

			const sleepTime = 3 * time.Second
			select {
			case <-time.After(sleepTime):
				log.Printf("DB termination sleep time %v is over\n", sleepTime)
			case <-ctx.Done():
				log.Printf("DB termination Context is Done because of: %+v\n", ctx.Err())
			}
		})

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 20 seconds:
	if err := terminator.Wait(appCtx, 20*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}

💡 Integration with HTTP server

The library doesn't have out of the box support to start/terminate the HTTP server, but that's easy to handle:

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "syscall"
    "time"

    "github.com/skovtunenko/graterm"
)

func main() {
    // Define Order for HTTP Server termination:
    const HTTPServerTerminationOrder graterm.Order = 1
	
    // create new Terminator instance:
    terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	terminator.SetLogger(log.Default()) // Optional step

    // Create an HTTP Server and add one simple handler into it:
    httpServer := &http.Server{
		ReadHeaderTimeout: 60 * time.Second, // fix for potential Slowloris Attack
        Addr:              ":8080",
        Handler:           http.DefaultServeMux,
    }
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello, world!")
    })

    // Start HTTP server in a separate goroutine:
    go func() { 
        if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Printf("terminated HTTP Server: %+v\n", err)
        }
    }()

    // Register HTTP Server termination hook:
    terminator.WithOrder(HTTPServerTerminationOrder).
        WithName("HTTPServer"). // setting a Name is optional and will be useful only if logger instance provided
        Register(10*time.Second, func(ctx context.Context) {
            if err := httpServer.Shutdown(ctx); err != nil {
                log.Printf("shutdown HTTP Server: %+v\n", err)
            }
        })

    // Wait for os.Signal to occur, then terminate application with maximum timeout of 30 seconds:
    if err := terminator.Wait(appCtx, 30*time.Second); err != nil {
        log.Printf("graceful termination period is timed out: %+v\n", err)
    }
}

The full-fledged example located here: example.go

📖 Testing

Unit-tests with code coverage:

make test

Run linter:

make code-quality

⚠️ LICENSE

MIT

🕶️ AUTHORS

Documentation

Overview

Package graterm provides capabilities to create a Terminator instance, register ordered termination Hooks, and block application execution until one of the registered os.Signal events occurs.

Termination hooks registered with the same Order will be executed concurrently.

It is possible to set individual timeouts for each registered termination hook and global termination timeout for the whole application.

Optionally a Hook may have a name (using Hook.WithName). It might be handy only if the Logger injected into Terminator instance to log internal termination lifecycle events.

Examples

Example code for generic application components:

func main() {
	// Define termination Orders:
	const (
		HTTPServerTerminationOrder graterm.Order = 1
		DBTerminationOrder         graterm.Order = 2
	)

	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	terminator.SetLogger(log.Default()) // Optional step

	// Register HTTP Server termination hook:
	terminator.WithOrder(HTTPServerTerminationOrder).
		WithName("HTTP Server"). // setting a Name is optional and will be useful only if logger instance provided
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating HTTP Server...")
			defer log.Println("...HTTP Server terminated")
		})

	// Register nameless DB termination hook:
	terminator.WithOrder(DBTerminationOrder).
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating Database...")
			defer log.Println("...Database terminated")

			const sleepTime = 3 * time.Second
			select {
			case <-time.After(sleepTime):
				log.Printf("Database termination sleep time %v is over\n", sleepTime)
			case <-ctx.Done():
				log.Printf("Database termination Context is Done because of: %+v\n", ctx.Err())
			}
		})

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 20 seconds:
	if err := terminator.Wait(appCtx, 20*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}

Example code for HTTP server integration:

func main() {
	// Define Order for HTTP Server termination:
	const HTTPServerTerminationOrder graterm.Order = 1

	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	terminator.SetLogger(log.Default()) // Optional step

	// Create an HTTP Server and add one simple handler into it:
	httpServer := &http.Server{
		ReadHeaderTimeout: 60 * time.Second, // fix for potential Slowloris Attack
		Addr:              ":8080",
		Handler:           http.DefaultServeMux,
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "hello, world!")
	})

	// Start HTTP server in a separate goroutine:
	go func() {
		if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Printf("terminated HTTP Server: %+v\n", err)
		}
	}()

	// Register HTTP Server termination hook:
	terminator.WithOrder(HTTPServerTerminationOrder).
		WithName("HTTPServer"). // setting a Name is optional and will be useful only if logger instance provided
		Register(10*time.Second, func(ctx context.Context) {
			if err := httpServer.Shutdown(ctx); err != nil {
				log.Printf("shutdown HTTP Server: %+v\n", err)
			}
		})

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 30 seconds:
	if err := terminator.Wait(appCtx, 30*time.Second); err != nil {
		log.Printf("graceful termination period is timed out: %+v\n", err)
	}
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Hook added in v0.4.0

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

Hook is a registered ordered termination unit of work. I.e., the code that needs to be executed to perform resource cleanup or any other maintenance while shutting down the application.

Do NOT create a Hook instance manually, use Terminator.WithOrder() method instead to get a Hook instance.

func (*Hook) Register added in v0.4.0

func (h *Hook) Register(timeout time.Duration, hookFunc func(ctx context.Context))

Register registers termination Hook that should finish execution in less than given timeout.

Timeout duration must be greater than zero; if not, timeout of 1 min will be used.

The context value passed into hookFunc will be used only for cancellation signaling. I.e. to signal that Terminator will no longer wait on Hook to finish termination.

Example
package main

import (
	"context"
	"log"
	"syscall"
	"time"

	"github.com/skovtunenko/graterm"
)

func main() {
	// Define Order:
	const (
		HTTPServerTerminationOrder graterm.Order = 1
	)

	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)

	// Register some hooks:
	terminator.WithOrder(HTTPServerTerminationOrder).
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating HTTP Server...")
			defer log.Println("...HTTP Server terminated")
		})

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 20 seconds:
	if err := terminator.Wait(appCtx, 20*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}
Output:

func (*Hook) String added in v0.4.0

func (h *Hook) String() string

String returns string representation of a Hook.

func (*Hook) WithName added in v0.4.0

func (h *Hook) WithName(name string) *Hook

WithName sets (optional) human-readable name of the registered termination Hook.

The Hook name will be useful only if Logger instance has been injected (using Terminator.SetLogger method) into Terminator to log internal termination lifecycle events.

Example
package main

import (
	"context"
	"log"
	"syscall"
	"time"

	"github.com/skovtunenko/graterm"
)

func main() {
	// Define Order:
	const (
		HTTPServerTerminationOrder graterm.Order = 1
	)

	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	terminator.SetLogger(log.Default()) // Optional step

	// Register some hooks:
	terminator.WithOrder(HTTPServerTerminationOrder).
		WithName("HTTP Server"). // Define (optional) Hook name
		Register(1*time.Second, func(ctx context.Context) {
			log.Println("terminating HTTP Server...")
			defer log.Println("...HTTP Server terminated")
		})

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 20 seconds:
	if err := terminator.Wait(appCtx, 20*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}
Output:

type Logger

type Logger interface {
	Printf(format string, v ...interface{})
}

Logger specifies the interface for internal Terminator log operations.

By default, library will not log anything. To set the logger, use Terminator.SetLogger() method.

type Order added in v0.4.0

type Order int

Order is an application components termination order. Termination Hooks registered with the same order will be executed concurrently.

Lower order - higher priority.

type Terminator added in v0.3.0

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

Terminator is a component terminator that executes registered termination Hooks in a specified order.

func NewWithSignals

func NewWithSignals(appCtx context.Context, sig ...os.Signal) (*Terminator, context.Context)

NewWithSignals creates a new instance of component Terminator.

If the given appCtx parameter is canceled, the termination process will start for already registered Hook instances after calling Terminator.Wait method.

Example of useful signals might be: syscall.SIGINT, syscall.SIGTERM.

Note: this method will start internal monitoring goroutine.

Example
package main

import (
	"context"
	"log"
	"syscall"
	"time"

	"github.com/skovtunenko/graterm"
)

func main() {
	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)

	// register hooks...

	// Wire other components ...

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 40 seconds:
	if err := terminator.Wait(appCtx, 40*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}
Output:

func (*Terminator) SetLogger added in v0.3.0

func (t *Terminator) SetLogger(log Logger)

SetLogger sets the Logger implementation.

If log is nil, then NOOP logger implementation will be used.

Example
package main

import (
	"context"
	"log"
	"syscall"

	"github.com/skovtunenko/graterm"
)

func main() {
	// create new Terminator instance:
	terminator, _ := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)

	// Set custom logger implementation instead of default NOOP one:
	terminator.SetLogger(log.Default())
}
Output:

func (*Terminator) Wait added in v0.3.0

func (t *Terminator) Wait(appCtx context.Context, timeout time.Duration) error

Wait waits (with timeout) for Terminator to finish termination after the appCtx is done.

Example
package main

import (
	"context"
	"log"
	"syscall"
	"time"

	"github.com/skovtunenko/graterm"
)

func main() {
	// create new Terminator instance:
	terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)

	// register hooks...

	// Wire other components ...

	// Wait for os.Signal to occur, then terminate application with maximum timeout of 40 seconds:
	if err := terminator.Wait(appCtx, 40*time.Second); err != nil {
		log.Printf("graceful termination period was timed out: %+v", err)
	}
}
Output:

func (*Terminator) WithOrder added in v0.3.0

func (t *Terminator) WithOrder(order Order) *Hook

WithOrder sets the Order for the termination hook. It starts registration chain to register termination hook with priority.

The lower the Order the higher the execution priority, the earlier it will be executed. If there are multiple hooks with the same Order they will be executed in parallel.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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