breaker

package module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Dec 5, 2020 License: MIT Imports: 7 Imported by: 3

README

🚧 breaker Awesome

Flexible mechanism to make execution flow interruptible.

Build Documentation Quality Template Coverage Mirror

💡 Idea

The breaker carries a cancellation signal to interrupt an action execution.

var NewYear = time.Time{}.AddDate(time.Now().Year(), 0, 0)

interrupter := breaker.Multiplex(
	breaker.BreakByContext(context.WithTimeout(req.Context(), time.Minute)),
	breaker.BreakByDeadline(NewYear),
	breaker.BreakBySignal(os.Interrupt),
)
defer interrupter.Close()

<-interrupter.Done() // wait context cancellation, timeout or interrupt signal

A full description of the idea is available here.

🏆 Motivation

I have to make modules github.com/kamilsk/retry/v5:

if err := retry.Retry(breaker.BreakByTimeout(time.Minute), action); err != nil {
	log.Fatal(err)
}

and github.com/kamilsk/semaphore/v5:

if err := semaphore.Acquire(breaker.BreakByTimeout(time.Minute), 5); err != nil {
	log.Fatal(err)
}

more consistent and reliable.

Additionally, I want to implement a Graceful Shutdown on the same mechanism.

🤼‍♂️ How to

Do HTTP request with retries
interrupter := breaker.Multiplex(
	breaker.BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
	breaker.BreakByTimeout(timeout),
)
defer interrupter.Close()

ctx := breaker.ToContext(interrupter)
ctx = context.WithValue(ctx, header, "...")

req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
if err != nil {
	panic(err)
}

var resp *http.Response
action := func(ctx context.Context) (err error) {
	req = req.Clone(ctx)

	source := ctx.Value(header).(string)
	req.Header.Set(header, source)

	resp, err = http.DefaultClient.Do(req)
	return err
}

if err := retry.Do(ctx, action); err != nil {
	panic(err)
}
Full example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"syscall"
	"time"

	"github.com/kamilsk/breaker"
	"github.com/kamilsk/retry/v5"
)

func main() {
	const (
		header  = "X-Message"
		timeout = time.Minute
	)

	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		time.Sleep(timeout / 10)
		_, _ = rw.Write([]byte(req.Header.Get(header)))
	}))
	defer server.Close()

	interrupter := breaker.Multiplex(
		breaker.BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
		breaker.BreakByTimeout(timeout),
	)
	defer interrupter.Close()

	ctx := breaker.ToContext(interrupter)
	ctx = context.WithValue(ctx, header, "flexible mechanism to make execution flow interruptible")

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
	if err != nil {
		panic(err)
	}

	var resp *http.Response
	action := func(ctx context.Context) (err error) {
		req = req.Clone(ctx)

		source := ctx.Value(header).(string)
		req.Header.Set(header, source)

		resp, err = http.DefaultClient.Do(req)
		return err
	}

	if err := retry.Do(ctx, action); err != nil {
		fmt.Println("error:", err)
		return
	}
	_, _ = io.Copy(os.Stdout, resp.Body)
}

Play it!

Graceful Shutdown HTTP server
interrupter := breaker.Multiplex(
	breaker.BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
	breaker.BreakByTimeout(timeout),
)
defer interrupter.Close()

server := http.Server{
	BaseContext: func(net.Listener) context.Context {
		return breaker.ToContext(interrupter)
	},
}
go func() {
	if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

<-interrupter.Done()
if errors.Is(interrupter.Err(), breaker.Interrupted) {
	if err := server.Shutdown(context.TODO()); err != nil {
		panic(err)
	}
}
Full example
package main

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

	"github.com/kamilsk/breaker"
)

func main() {
	const timeout = time.Minute

	interrupter := breaker.Multiplex(
		breaker.BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
		breaker.BreakByTimeout(timeout),
	)
	defer interrupter.Close()

	server := http.Server{
		Addr:    ":8080",
		Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}),
		BaseContext: func(net.Listener) context.Context {
			return breaker.ToContext(interrupter)
		},
	}
	go func() {
		if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
			log.Fatal(err)
		}
	}()

	<-interrupter.Done()
	if err := interrupter.Err(); errors.Is(err, breaker.Interrupted) {
		if err := server.Shutdown(context.TODO()); err != nil {
			panic(err)
		}
	}
	fmt.Println("graceful shutdown")
}

Play it!

🧩 Integration

The library uses SemVer for versioning, and it is not BC-safe through major releases. You can use go modules to manage its version.

$ go get github.com/kamilsk/breaker@latest

🤲 Outcomes

Console tool to execute commands for a limited time

The example shows how to execute console commands for ten minutes.

$ date
# Thu Jan  7 21:02:21
$ breakit after 10m -- server run --port=8080
$ breakit ps
# +--------------------------+----------------------------+----------+----------+
# | Process                  | Status                     | Since    | Until    |
# +--------------------------+----------------------------+----------+----------+
# | server run --port=8080   | exit 1; panic: database... | 21:02:36 | -        |
# +--------------------------+----------------------------+----------+----------+
# |                          |                            |    Total |        1 |
# +--------------------------+----------------------------+----------+----------+
$ breakit after 10m -- database run --port=5432
$ breakit after 10m delay 5s -- server run --port=8080
$ breakit ps
# +--------------------------+----------------------------+----------+----------+
# | Process                  | Status                     | Since    | Until    |
# +--------------------------+----------------------------+----------+----------+
# | database run --port=5432 | running                    | 21:04:09 | 21:14:09 |
# | server run --port=8080   | delayed                    | 21:04:30 | 21:14:25 |
# +--------------------------+----------------------------+----------+----------+
# |                          |                            |    Total |        2 |
# +--------------------------+----------------------------+----------+----------+

See more details here.


made with ❤️ for everyone

Documentation

Overview

Package breaker provides flexible mechanism to make your code breakable.

Example (GracefulShutdown)
example := make(chan struct{})

breaker := Multiplex(
	BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
	BreakByTimeout(250*time.Millisecond),
)
defer breaker.Close()

server := http.Server{
	BaseContext: func(net.Listener) context.Context {
		return ToContext(breaker)
	},
}
go func() {
	if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
		panic(err)
	}
	close(example)
}()

<-breaker.Done()
if err := server.Shutdown(context.TODO()); err == nil && errors.Is(breaker.Err(), Interrupted) {
	fmt.Println("works well")
}
<-example
Output:

works well
Example (HttpRequest)
const url = "http://example.com/"

example := make(chan struct{})
close(example)

breaker := Multiplex(
	BreakByChannel(example),
	BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM),
	BreakByTimeout(time.Hour),
)
defer breaker.Close()

req, err := http.NewRequestWithContext(ToContext(breaker), http.MethodGet, url, nil)
if err != nil {
	panic(err)
}

//nolint:bodyclose
if _, err := http.DefaultClient.Do(req); errors.Is(err, context.Canceled) && errors.Is(breaker.Err(), Interrupted) {
	fmt.Println("works well")
}
Output:

works well

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ToContext added in v1.2.0

func ToContext(br Interface) context.Context

ToContext converts the breaker into the Context.

interrupter := breaker.Multiplex(
	breaker.BreakBySignal(os.Interrupt),
	breaker.BreakByTimeout(time.Minute),
)
defer interrupter.Close()

request, err := http.NewRequestWithContext(breaker.ToContext(interrupter), ...)
if err != nil { handle(err) }

response, err := http.DefaultClient.Do(request)
if err != nil { handle(err) }
handle(response)

Types

type Error added in v1.2.0

type Error string

Error defines the package errors.

const Interrupted Error = "operation interrupted"

Interrupted is the error returned by the breaker when a cancellation signal occurred.

func (Error) Error added in v1.2.0

func (err Error) Error() string

Error returns the string representation of an error.

type Interface

type Interface interface {
	// Close closes the Done channel and releases resources associated with it.
	Close()
	// Done returns a channel that's closed when a cancellation signal occurred.
	Done() <-chan struct{}
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	Err() error
	// contains filtered or unexported methods
}

Interface carries a cancellation signal to interrupt an action execution.

Example based on github.com/kamilsk/retry/v5 module:

if err := retry.Do(breaker.BreakByTimeout(time.Minute), action); err != nil {
	log.Fatal(err)
}

Example based on github.com/kamilsk/semaphore/v5 module:

if err := semaphore.Acquire(breaker.BreakByTimeout(time.Minute), 5); err != nil {
	log.Fatal(err)
}

func BreakByChannel added in v1.2.0

func BreakByChannel(signal <-chan struct{}) Interface

BreakByChannel returns a new breaker based on the channel.

signal := make(chan struct{})
go func() {
	<-time.After(time.Minute)
	close(signal)
}()

interrupter := breaker.BreakByChannel(signal)
defer interrupter.Close()

background.Job().Do(interrupter)

func BreakByContext added in v1.1.1

func BreakByContext(ctx context.Context, cancel context.CancelFunc) Interface

BreakByContext returns a new breaker based on the Context.

interrupter := breaker.BreakByContext(context.WithTimeout(req.Context(), time.Minute))
defer interrupter.Close()

background.Job().Do(interrupter)

func BreakByDeadline

func BreakByDeadline(deadline time.Time) Interface

BreakByDeadline closes the Done channel when the deadline occurs.

interrupter := breaker.BreakByDeadline(time.Now().Add(time.Minute))
defer interrupter.Close()

background.Job().Do(interrupter)

func BreakBySignal

func BreakBySignal(sig ...os.Signal) Interface

BreakBySignal closes the Done channel when the breaker will receive OS signals.

interrupter := breaker.BreakBySignal(os.Interrupt)
defer interrupter.Close()

background.Job().Do(interrupter)

func BreakByTimeout

func BreakByTimeout(timeout time.Duration) Interface

BreakByTimeout closes the Done channel when the timeout happens.

interrupter := breaker.BreakByTimeout(time.Minute)
defer interrupter.Close()

background.Job().Do(interrupter)

func Multiplex

func Multiplex(breakers ...Interface) Interface

Multiplex combines multiple breakers into one.

interrupter := breaker.Multiplex(
	breaker.BreakByContext(req.Context()),
	breaker.BreakBySignal(os.Interrupt),
	breaker.BreakByTimeout(time.Minute),
)
defer interrupter.Close()

background.Job().Do(interrupter)

func MultiplexThree deprecated

func MultiplexThree(one, two, three Interface) Interface

MultiplexThree combines three breakers into one. It's an optimized version of a more generic Multiplex.

interrupter := breaker.MultiplexThree(
	breaker.BreakByContext(req.Context()),
	breaker.BreakBySignal(os.Interrupt),
	breaker.BreakByTimeout(time.Minute),
)
defer interrupter.Close()

background.Job().Do(interrupter)

Deprecated: Multiplex has the same optimization under the hood now. TODO:v2 will be removed.

func MultiplexTwo deprecated

func MultiplexTwo(one, two Interface) Interface

MultiplexTwo combines two breakers into one.

interrupter := breaker.MultiplexTwo(
	breaker.BreakByContext(req.Context()),
	breaker.BreakBySignal(os.Interrupt),
)
defer interrupter.Close()

background.Job().Do(interrupter)

Deprecated: Multiplex has the same optimization under the hood now. TODO:v2 will be removed.

func New added in v1.2.0

func New() Interface

New returns a new breaker, which can be interrupted only by a Close call.

interrupter := breaker.New()
go background.Job().Do(interrupter)

<-time.After(time.Minute)
interrupter.Close()

func WithContext deprecated

func WithContext(ctx context.Context) (Interface, context.Context)

WithContext returns a new breaker and an associated Context based on the passed one.

interrupter, ctx := breaker.WithContext(req.Context())
defer interrupter.Close()

background.Job().Run(ctx)

Deprecated: use BreakByContext instead. TODO:v2 will be removed.

Jump to

Keyboard shortcuts

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