safedown

package module
v0.2.6 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2023 License: MIT Imports: 3 Imported by: 0

README

Safedown

Safedown is for ensuring that applications shutdown gracefully and correctly. This includes the cases when an interrupt or termination signal is received, or when actions across go routines need to be coordinated.

Safedown is like defer but more graceful and coordinated.

Go Reference

Quick Start

The shutdown actions are initialised through its constructor. Methods are added (in this case cancel) which will be run when a signal is received or when the Shutdown method is called.

package main

import (
	"context"
	"fmt"
	"syscall"
	"time"

	"github.com/PeterEFinch/safedown"
)

func main() {
	// Shutdown actions must be initialised using the constructor.  
	sa := safedown.NewShutdownActions(
		safedown.UseOrder(safedown.FirstInLastDone), // This option is unnecessary because it is the default.
		safedown.UsePostShutdownStrategy(safedown.PerformImmediately),
		safedown.ShutdownOnSignals(syscall.SIGTERM, syscall.SIGINT), // Replace with OS specific signals.
	)

	// Including `defer sa.Shutdown()` is not necessary but can be included to 
	// ensure that the actions are run at the very end even if no signal is 
	// received.
	defer sa.Shutdown()

	// The cancel can be added so that it is ensured that it is always called.
	// In the case `defer sa.Shutdown()` wasn't included, defer cancel() should
	// be added.
	ctx, cancel := context.WithCancel(context.Background())
	sa.AddActions(cancel)

	// This code is just a stand in for a general process.
	fmt.Println("Processing starting")
	t := time.After(time.Minute)
	select {
	case <-ctx.Done():
	case <-t:
	}
	fmt.Println("Finished")
}

For more detailed examples see the examples module.

F.A.Q. (Fictitiously Asked Questions)
  1. What order should I used? First in, last done order is most commonly used because the first things create are usually the last ones that need tearing down. This matches the view that safedown is like defer but graceful and coordinated.

  2. What signals should I listen for? This depends on which OS is being used. For example, for code in docker images running alpine listening for at least syscall.SIGTERM & syscall.SIGINT is recommended. Listening to all signals will likely catch and shutdown in unwanted cases and not all signals can be caught e.g os.Kill isn't caught on ubuntu.

  3. Should I use a post shutdown strategy? This depends on the application and how it is initialised. The post shutdown strategy is usually only used when the application is interrupted during its initialisation.

  4. What OSes has this been test on? The tests have been successfully run locally on macOS Monterey and on ubuntu in the github actions. Tests failed on Windows in github actions because os.Interrupt has not been implemented. It not has not been tested on other OSes such as OpenBSD.

  5. Why are there no dependencies? This repository is intended to be a zero-dependency library. This makes it easier to maintain and prevents adding external vulnerabilities or bugs.

  6. Why is there no logging? There is no convention when it comes to logging, so it was considered best to avoid it. The code is simple enough that it seems unnecessary.

  7. Can I use this in mircoservices? Yes. This was original designed to ensure graceful shutdown in microservices.

  8. Why is there another similar Safedown? I originally wrote a version of safedown as package in personal project, which I rewrote inside a Graphmasters service (while I was an employee), which I finally put inside its own Graphmasters Safedown repository (which I as of writing this I still maintain). Graphmasters and I decided they would make their version open source (yay) and I decided to reimplement my own version from scratch with ideas from the original version because I wanted to expand upon some ideas.

Documentation

Overview

Package safedown is for ensuring that applications shutdown gracefully and correctly. This includes the cases when an interrupt or termination signal is received, or when actions across go routines need to be coordinated.

Example

Example demonstrates how setting up the safedown's shutdown actions works when a signal is received.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/PeterEFinch/safedown"
)

func main() {
	// This will send an interrupt signal after a second to simulate a signal
	// being sent from the outside.
	go func(pid int) {
		time.Sleep(time.Second)
		process := os.Process{Pid: pid}
		if err := process.Signal(os.Interrupt); err != nil {
			panic("unable to continue test: could not send signal to process")
		}
	}(os.Getpid())

	sa := safedown.NewShutdownActions(
		safedown.ShutdownOnSignals(os.Interrupt),
		safedown.UseOnSignalFunc(func(signal os.Signal) {
			fmt.Printf("Signal received: %s\n", signal.String())
		}),
	)
	defer sa.Shutdown()

	ctx, cancel := context.WithCancel(context.Background())
	sa.AddActions(cancel)

	fmt.Println("Processing starting")
	t := time.After(2 * time.Second)
	select {
	case <-ctx.Done():
		fmt.Println("Context cancelled")
	case <-t:
		fmt.Println("Ticker ticked")
	}
	fmt.Println("Finished")

}
Output:

Processing starting
Signal received: interrupt
Context cancelled
Finished
Example (NoSignal)

Example_signalNotReceived demonstrates how setting up the safedown's shutdown actions works when no signal is received (and the program can terminate of its own accord).

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/PeterEFinch/safedown"
)

func main() {
	sa := safedown.NewShutdownActions(
		safedown.ShutdownOnSignals(os.Interrupt),
		safedown.UseOnSignalFunc(func(signal os.Signal) {
			fmt.Printf("Signal received: %s\n", signal.String())
		}),
	)
	defer sa.Shutdown()

	ctx, cancel := context.WithCancel(context.Background())
	sa.AddActions(cancel)

	fmt.Println("Processing starting")
	t := time.After(2 * time.Second)
	select {
	case <-ctx.Done():
		fmt.Println("Context cancelled")
	case <-t:
		fmt.Println("Ticker ticked")
	}
	fmt.Println("Finished")

}
Output:

Processing starting
Ticker ticked
Finished

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*config)

Option represents an option of the shutdown actions.

func ShutdownOnAnySignal

func ShutdownOnAnySignal() Option

ShutdownOnAnySignal will enable shutdown to be triggered by any signal.

Using this option will likely lead to overzealous shutting down. It is recommended to use the option ShutdownOnSignals with the signals of interest.

This option will override ShutdownOnSignals if included after it.

func ShutdownOnSignals

func ShutdownOnSignals(signals ...os.Signal) Option

ShutdownOnSignals will enable the shutdown to be triggered by any of the signals included.

The choice of signals depends on the operating system and use-case.

This option will override ShutdownOnAnySignal if included after it.

func UseOnSignalFunc

func UseOnSignalFunc(onSignal func(os.Signal)) Option

UseOnSignalFunc sets a function that will be called that if any signal that is listened for is received.

func UseOrder

func UseOrder(order Order) Option

UseOrder determines the order the actions will be performed relative to the order they are added.

Example (FirstInFirstDone)

ExampleUseOrder_firstInFirstDone demonstrates the "first in, first done" order.

package main

import (
	"fmt"

	"github.com/PeterEFinch/safedown"
)

func main() {
	sa := safedown.NewShutdownActions(
		safedown.UseOrder(safedown.FirstInFirstDone),
	)

	sa.AddActions(func() {
		fmt.Println("The first action added will be done first ...")
	})
	sa.AddActions(func() {
		fmt.Println("... and the last action added will be done last.")
	})

	sa.Shutdown()

}
Output:

The first action added will be done first ...
... and the last action added will be done last.
Example (FirstInLastDone)

ExampleUseOrder_firstInFirstDone demonstrates the "first in, last done" order.

package main

import (
	"fmt"

	"github.com/PeterEFinch/safedown"
)

func main() {
	sa := safedown.NewShutdownActions(
		safedown.UseOrder(safedown.FirstInLastDone),
	)

	sa.AddActions(func() {
		fmt.Println("... and the first action added will be done last.")
	})
	sa.AddActions(func() {
		fmt.Println("The last action added will be done first ...")
	})

	sa.Shutdown()

}
Output:

The last action added will be done first ...
... and the first action added will be done last.

func UsePostShutdownStrategy

func UsePostShutdownStrategy(strategy PostShutdownStrategy) Option

UsePostShutdownStrategy determines how the actions will be handled after Shutdown has been called or triggered via a signal.

The strategy is usually only used when the Shutdown has been triggered during the initialisation of an application.

Example

ExampleUsePostShutdownStrategy demonstrates how to set a post shutdown strategy and its consequences.

package main

import (
	"fmt"
	"sync"

	"github.com/PeterEFinch/safedown"
)

func main() {
	sa := safedown.NewShutdownActions(
		safedown.UsePostShutdownStrategy(safedown.PerformCoordinatelyInBackground),
	)

	sa.AddActions(func() {
		fmt.Println("... and the first action added will be done after that.")
	})
	sa.AddActions(func() {
		fmt.Println("The last action added will be done first ...")
	})

	sa.Shutdown()

	wg := sync.WaitGroup{}
	wg.Add(1)
	sa.AddActions(func() {
		fmt.Println("The action added after shutdown is also done (provided we wait a little).")
		wg.Done()
	})
	wg.Wait()

}
Output:

The last action added will be done first ...
... and the first action added will be done after that.
The action added after shutdown is also done (provided we wait a little).

type Order

type Order uint8

Order represents the order the actions will be performed relative to the order that they were added.

const (
	FirstInLastDone  Order = iota // FirstInLastDone means that actions added first will be performed last on shutdown.
	FirstInFirstDone              // FirstInFirstDone means that actions added first will be performed first on shutdown.

)

type PostShutdownStrategy

type PostShutdownStrategy uint8

PostShutdownStrategy represents the strategy that should be applied to action added after shutdown has been triggered.

const (
	DoNothing                       PostShutdownStrategy = iota // DoNothing means that any action added after shutdown has been trigger will not be done.
	PerformImmediately                                          // PerformImmediately means that any action added after shutdown will be performed immediately (and block the AddAction method).
	PerformImmediatelyInBackground                              // PerformImmediatelyInBackground means that any action added after shutdown will be performed immediately in a go routine.
	PerformCoordinatelyInBackground                             // PerformCoordinatelyInBackground means that the shutdown actions will ATTEMPT to coordinate the actions added with all other actions which have already been added.

)

type ShutdownActions

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

ShutdownActions represent a set of actions that are performed, i.e. functions that are called, when a service or computation is shutting down, ending or interrupted.

ShutdownActions must always be initialised using the NewShutdownActions function.

func NewShutdownActions

func NewShutdownActions(options ...Option) *ShutdownActions

NewShutdownActions initialises shutdown actions.

In the event that options conflict, the later option will override the earlier option.

func (*ShutdownActions) AddActions

func (sa *ShutdownActions) AddActions(actions ...func())

AddActions adds actions that are to be performed when Shutdown is called.

If Shutdown has already been called or trigger via a signal then the handling of the actions will depend on the post-shutdown strategy.

func (*ShutdownActions) Shutdown

func (sa *ShutdownActions) Shutdown()

Shutdown will perform all actions that have been added.

This is an idempotent method and successive calls will have no affect.

Example

ExampleShutdownActions_Shutdown demonstrates the default shutdown behaviour.

package main

import (
	"fmt"

	"github.com/PeterEFinch/safedown"
)

func main() {
	sa := safedown.NewShutdownActions()

	sa.AddActions(func() {
		fmt.Println("The action is performed after shutdown is called.")
	})

	fmt.Println("Code runs before shutdown is called.")
	sa.Shutdown()

}
Output:

Code runs before shutdown is called.
The action is performed after shutdown is called.

func (*ShutdownActions) Wait

func (sa *ShutdownActions) Wait()

Wait waits until the shutdown actions to have been performed.

If actions were added after Shutdown has been called or trigger via a signal then whether this method wait for those actions to be performed on the post-shutdown strategy and when the actions were added.

Jump to

Keyboard shortcuts

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