Giter Club home page Giter Club logo

breaker's Introduction

๐Ÿšง breaker Awesome Go

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

breaker's People

Contributors

kamilsk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

breaker's Issues

strict guarantees for Close

see the context package

package main

import (
	"fmt"
	"context"
)

func main() {
	ctx, cancel := context.WithCancel(context.TODO())
	cancel()
	
	if ctx.Err() == nil {
		panic("not strict guarantees")
	}
	
	fmt.Println("strict guarantees")
}

I need the same thing.

breaker.ToContext

  • investigate the "hook based" model: when the breaker is released it calls cancel function
  • if it's not possible add overhead: run goroutine above Done channel

Signal trap

  • alias
interrupter := breaker.Termination()
// breaker.BreakBySignal(os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  • error
errors.Is(interrupter.Err(), breaker.SignalTrapped)
// SignalTrapped Error = "signal trapped"

simplify implementation

  • remove trigger
  • remove released
  • ? do not run background goroutine
  • use simplified version of context wrapping
// Wrap wraps the context and its cancel function into BreakCloser.
func Wrap(ctx context.Context, cancel context.CancelFunc) BreakCloser {
	return &wrapper{ctx, cancel}
}

type wrapper struct {
	context.Context
	cancel context.CancelFunc
}

// Close closes the Done channel and releases resources associated with it.
func (breaker *wrapper) Close() {
	breaker.cancel()
}

investigate timer leaks

Whenever a time.Timer is Stopped its channel must be cleared using a
select if the result of the Stop() is false.

BreakByContext

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

extend interface

// Interface carries a cancellation signal to break an action execution.
//
// Example based on github.com/kamilsk/retry package:
//
//  if err := retry.Retry(breaker.BreakByTimeout(time.Minute), action); err != nil {
//  	log.Fatal(err)
//  }
//
// Example based on github.com/kamilsk/semaphore package:
//
//  if err := semaphore.Acquire(breaker.BreakByTimeout(time.Minute), 5); err != nil {
//  	log.Fatal(err)
//  }
//
type Interface interface {
	// Done returns a channel that's closed when a cancellation signal occurred.
	Done() <-chan struct{}
	// Close closes the Done channel and releases resources associated with it.
	Close()
	// trigger is a private method to guarantee that the Breakers come from
	// this package and all of them return a valid Done channel.
	trigger() Interface

	Error() error
}

break by channel

for example

func CloseByChannel(ctx context.Context, ch <-chan struct{}) context.Context {
	ctx, cancel := context.WithCancel(ctx)
	go func () {
		<-ch // ensure that this event will happen
		cancel()
	}
	return ctx
}

make possible to reduce fluky tests

--- FAIL: TestBreakBySignal (0.00s)
    --- FAIL: TestBreakBySignal/with_signal (0.02s)
        breaker_test.go:216: max difference between 2021-01-24 21:27:21.89766 +0300 MSK m=+0.017424561 and 2021-01-24 21:27:21.908426 +0300 MSK m=+0.028190544 allowed is 10ms, but difference was -10.765983ms
FAIL
$ make test
--- FAIL: TestMultiplexThree (0.01s)
    deprecated_test.go:30: a breaker is not released
    deprecated_test.go:30: a breaker has inconsistent state
    deprecated_test.go:30: a breaker has no error
FAIL
FAIL    github.com/kamilsk/breaker      0.884s
FAIL
make: *** [test] Error 1

Wait signal

breaker.Wait(interrupter)
// <-interrupter.Done()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.