Skip to content

Commit

Permalink
feat: define Breaker and implement Google SRE circuit breaker.
Browse files Browse the repository at this point in the history
  • Loading branch information
chenyanchen committed Oct 26, 2023
1 parent 7a01e11 commit 905ab09
Show file tree
Hide file tree
Showing 18 changed files with 819 additions and 2 deletions.
72 changes: 72 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Complete configurations: https://golangci-lint.run/usage/configuration/

linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errcheck
- errname
- errorlint
- exportloopref
- gochecknoglobals
- gochecknoinits
- gocritic
- godot
- gofmt
- gofumpt
- goimports
- gomnd
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- nakedret
- nilerr
- nilnil
- noctx
- nolintlint
- prealloc
- predeclared
- promlinter
- reassign
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- stylecheck
- tenv
- testableexamples
- thelper
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign

linters-settings:
gosec:
excludes:
- G404 # Use of weak random number generator (math/rand instead of crypto/rand)
- G501 # Blocklisted import crypto/md5: weak cryptographic primitive
- G401 #Use of weak cryptographic primitive

revive:
rules:
- name: unexported-return
disabled: true

output:
sort-results: true

issues:
exclude-rules:
- path: "_test\\.go"
linters:
- gochecknoglobals
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,42 @@
# breaker
Circuit Breaker
# What is this?

Circuit Breaker in Go.

# Why use it?

A grace way to Handling Overload in client-side.

# How does it work?

There are only one implementation of Circuit Breaker, it is from [Google SRE](https://sre.google/sre-book/handling-overload).

# How to use it?

The abstract of Breaker interface is clear, it only cares about:

- the dependency is available or not

Not care about:

- specific errors
- fallback strategies
- telemetry

There are some examples to show how to use it:

- Use Circuit Breaker to protect your service (e.g. [example/simple/breaker.go](example/simple/breaker.go))
- Handle specific errors (e.g. [example/acceptableerror/breaker.go](example/acceptableerror/breaker.go))
- Add fallback strategies (e.g. [example/fallback/breaker.go](example/fallback/breaker.go))
- Add telemetry middleware (e.g. [example/telemetry/breaker.go](example/telemetry/breaker.go))

# Benchmark

```bash
❯ go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: github.com/chenyanchen/breaker
BenchmarkGoogleBreaker_Do-8 5794507 249.1 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/chenyanchen/breaker 1.658s
```
9 changes: 9 additions & 0 deletions breaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package breaker

import "errors"

type Breaker interface {
Do(func() error) error
}

var ErrServiceUnavailable = errors.New("circuit breaker is open")
54 changes: 54 additions & 0 deletions example/acceptableerror/breaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package acceptableerror

import (
"context"
"errors"

"github.com/chenyanchen/breaker"
"github.com/chenyanchen/breaker/example"
)

type breakerContentService struct {
breaker breaker.Breaker

contentService example.ContentService
}

func NewBreakerContentService(breaker breaker.Breaker, contentService example.ContentService) example.ContentService {
return &breakerContentService{
breaker: breaker,
contentService: contentService,
}
}

func (s *breakerContentService) GetContent(ctx context.Context, req *example.GetContentRequest) (*example.GetContentResponse, error) {
var resp *example.GetContentResponse
getContentFn := func() (err error) {
resp, err = s.contentService.GetContent(ctx, req)
return err
}

// handle acceptable errors
getContentFn = handleAcceptableErrors(getContentFn, example.ErrContentNotFound)

err := s.breaker.Do(getContentFn)
return resp, err
}

func handleAcceptableErrors(fn func() error, acceptableErrors ...error) func() error {
return func() error {
err := fn()
if err == nil {
return nil
}

for _, target := range acceptableErrors {
if errors.Is(err, target) {
// TODO: do something, like log
return nil
}
}

return err
}
}
29 changes: 29 additions & 0 deletions example/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"context"
"fmt"

"github.com/chenyanchen/breaker"
"github.com/chenyanchen/breaker/example"
"github.com/chenyanchen/breaker/example/simple"
)

func main() {
contentService := &contentService{}

breakerContentService := simple.NewBreakerContentService(breaker.NewGoogleBreaker(), contentService)

response, err := breakerContentService.GetContent(context.Background(), &example.GetContentRequest{})
if err != nil {
panic(err)
}

fmt.Println("response:", response)
}

type contentService struct{}

func (*contentService) GetContent(context.Context, *example.GetContentRequest) (*example.GetContentResponse, error) {
return &example.GetContentResponse{}, nil
}
46 changes: 46 additions & 0 deletions example/fallback/fallbackbreaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package fallback

import (
"context"

"github.com/chenyanchen/breaker"
"github.com/chenyanchen/breaker/example"
)

type breakerContentService struct {
breaker breaker.Breaker

contentService example.ContentService

defaultContent *example.GetContentResponse
}

func NewBreakerContentService(
breaker breaker.Breaker,
contentService example.ContentService,
defaultContent *example.GetContentResponse,
) example.ContentService {
return &breakerContentService{
breaker: breaker,
contentService: contentService,
defaultContent: defaultContent,
}
}

func (s *breakerContentService) GetContent(ctx context.Context, req *example.GetContentRequest) (*example.GetContentResponse, error) {
var resp *example.GetContentResponse
err := s.breaker.Do(func() (err error) {
resp, err = s.contentService.GetContent(ctx, req)
return err
})
if err == nil {
return resp, nil
}

// do fallback strategy
if s.defaultContent != nil {
return s.defaultContent, nil
}

return resp, err
}
12 changes: 12 additions & 0 deletions example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/chenyanchen/breaker/example

go 1.21.3

require (
github.com/chenyanchen/breaker v0.0.1
go.opentelemetry.io/otel/metric v1.19.0
)

require go.opentelemetry.io/otel v1.19.0 // indirect

replace github.com/chenyanchen/breaker => ../
20 changes: 20 additions & 0 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
17 changes: 17 additions & 0 deletions example/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package example

import (
"context"
"errors"
)

type ContentService interface {
GetContent(ctx context.Context, req *GetContentRequest) (*GetContentResponse, error)
}

type (
GetContentRequest struct{}
GetContentResponse struct{}
)

var ErrContentNotFound = errors.New("content not found")
31 changes: 31 additions & 0 deletions example/simple/breaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package simple

import (
"context"

"github.com/chenyanchen/breaker"

"github.com/chenyanchen/breaker/example"
)

type breakerContentService struct {
breaker breaker.Breaker

contentService example.ContentService
}

func NewBreakerContentService(breaker breaker.Breaker, contentService example.ContentService) example.ContentService {
return &breakerContentService{
breaker: breaker,
contentService: contentService,
}
}

func (s *breakerContentService) GetContent(ctx context.Context, req *example.GetContentRequest) (*example.GetContentResponse, error) {
var resp *example.GetContentResponse
err := s.breaker.Do(func() (err error) {
resp, err = s.contentService.GetContent(ctx, req)
return err
})
return resp, err
}
Loading

0 comments on commit 905ab09

Please sign in to comment.