graterm

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 16, 2025 License: MIT Imports: 9 Imported by: 1

README

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)
// Optionally set the custom logger implementation instead of default NOOP one:
terminator.SetLogger(log.Default()) 

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

Documentation

Overview

Package graterm provides a structured API for managing graceful application shutdown in response to specific os.Signal events. It ensures a controlled and predictable shutdown process by allowing the registration of ordered termination hooks.

Purpose

The graterm package simplifies application shutdown handling by introducing a centralized shutdown manager, the Terminator. It listens for specified os.Signal events and orchestrates the orderly execution of termination hooks. Hooks allow resources to be properly cleaned up before the application exits.

Key Concepts

  • Terminator: A singleton shutdown manager responsible for capturing OS termination signals and executing registered hooks.
  • Hook: A termination callback function that performs cleanup tasks during shutdown.
  • Order: A numeric priority assigned to each hook, dictating execution order. Hooks with the same Order execute concurrently, while hooks with a lower Order value complete before those with a higher Order value.

Features

  • Enforced Hook Registration: Hooks must only be registered via Terminator methods, ensuring proper ordering and execution.
  • Ordered Execution: Hooks execute sequentially based on their assigned Order; those with the same priority run concurrently.
  • Configurable Timeouts: Each Hook may have an individual timeout, and a global shutdown timeout can be set to enforce an upper limit on the termination process.
  • Optional Logging: the library does not depend on a specific logging framework.
  • Traceability: Hooks can be assigned a name for logging purposes when a Logger is attached to the Terminator.
  • Panic Safety: Panics inside hooks are caught, logged, and do not disrupt the overall shutdown sequence.

Additional Considerations

  • Concurrent execution of hooks with the same Order requires careful management of shared resources to avoid race conditions.
  • Incorrect timeout configurations may cause delays in shutdown; ensure timeouts are set to appropriate values.

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)
	}
}

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)
	}
}

type Logger

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

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)
	}
}

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())
}

func (*Terminator) Wait added in v0.3.0

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

Wait blocks execution until the provided appCtx is canceled and then executes all registered termination hooks.

This function initiates the shutdown sequence when appCtx is done, typically due to receiving an os.Signal events. After appCtx is canceled, it waits for all registered hooks to complete execution within the specified shutdownTimeout.

Hooks are executed in order of priority (lower order values execute first). Hooks with the same order run concurrently. If the shutdownTimeout expires before all hooks complete, the function returns an error.

This is a blocking call that should be placed at the end of the application's lifecycle to ensure a proper shutdown.

Parameters:

  • appCtx: The application context that, when canceled, triggers the termination process.
  • shutdownTimeout: The maximum time allowed for all hooks to complete execution.

Returns:

  • error: If termination exceeds the shutdownTimeout, an error is returned indicating a timeout.
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)
	}
}

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.

Example (GenericApplicationComponents)
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)
	}
}
Example (HttpServer)
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)
	}
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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