Skip to content

skovtunenko/graterm

Repository files navigation

graterm

Mentioned in Awesome Go 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{
        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