blob: 9214386254f49e862d2d4d497b948701d7e8b96c [file] [log] [blame]
khenaidooac637102019-01-14 15:44:34 -05001// Package breaker implements the circuit-breaker resiliency pattern for Go.
2package breaker
3
4import (
5 "errors"
6 "sync"
7 "sync/atomic"
8 "time"
9)
10
11// ErrBreakerOpen is the error returned from Run() when the function is not executed
12// because the breaker is currently open.
13var ErrBreakerOpen = errors.New("circuit breaker is open")
14
Abhay Kumara2ae5992025-11-10 14:02:24 +000015// State is a type representing the possible states of a circuit breaker.
16type State uint32
17
khenaidooac637102019-01-14 15:44:34 -050018const (
Abhay Kumara2ae5992025-11-10 14:02:24 +000019 Closed State = iota
20 Open
21 HalfOpen
khenaidooac637102019-01-14 15:44:34 -050022)
23
24// Breaker implements the circuit-breaker resiliency pattern
25type Breaker struct {
26 errorThreshold, successThreshold int
27 timeout time.Duration
28
29 lock sync.Mutex
Abhay Kumara2ae5992025-11-10 14:02:24 +000030 state State
khenaidooac637102019-01-14 15:44:34 -050031 errors, successes int
32 lastError time.Time
33}
34
35// New constructs a new circuit-breaker that starts closed.
36// From closed, the breaker opens if "errorThreshold" errors are seen
37// without an error-free period of at least "timeout". From open, the
38// breaker half-closes after "timeout". From half-open, the breaker closes
39// after "successThreshold" consecutive successes, or opens on a single error.
40func New(errorThreshold, successThreshold int, timeout time.Duration) *Breaker {
41 return &Breaker{
42 errorThreshold: errorThreshold,
43 successThreshold: successThreshold,
44 timeout: timeout,
45 }
46}
47
48// Run will either return ErrBreakerOpen immediately if the circuit-breaker is
49// already open, or it will run the given function and pass along its return
50// value. It is safe to call Run concurrently on the same Breaker.
51func (b *Breaker) Run(work func() error) error {
Abhay Kumara2ae5992025-11-10 14:02:24 +000052 state := b.GetState()
khenaidooac637102019-01-14 15:44:34 -050053
Abhay Kumara2ae5992025-11-10 14:02:24 +000054 if state == Open {
khenaidooac637102019-01-14 15:44:34 -050055 return ErrBreakerOpen
56 }
57
58 return b.doWork(state, work)
59}
60
61// Go will either return ErrBreakerOpen immediately if the circuit-breaker is
62// already open, or it will run the given function in a separate goroutine.
63// If the function is run, Go will return nil immediately, and will *not* return
64// the return value of the function. It is safe to call Go concurrently on the
65// same Breaker.
66func (b *Breaker) Go(work func() error) error {
Abhay Kumara2ae5992025-11-10 14:02:24 +000067 state := b.GetState()
khenaidooac637102019-01-14 15:44:34 -050068
Abhay Kumara2ae5992025-11-10 14:02:24 +000069 if state == Open {
khenaidooac637102019-01-14 15:44:34 -050070 return ErrBreakerOpen
71 }
72
73 // errcheck complains about ignoring the error return value, but
74 // that's on purpose; if you want an error from a goroutine you have to
75 // get it over a channel or something
76 go b.doWork(state, work)
77
78 return nil
79}
80
Abhay Kumara2ae5992025-11-10 14:02:24 +000081// GetState returns the current State of the circuit-breaker at the moment
82// that it is called.
83func (b *Breaker) GetState() State {
84 return (State)(atomic.LoadUint32((*uint32)(&b.state)))
85}
86
87func (b *Breaker) doWork(state State, work func() error) error {
khenaidooac637102019-01-14 15:44:34 -050088 var panicValue interface{}
89
90 result := func() error {
91 defer func() {
92 panicValue = recover()
93 }()
94 return work()
95 }()
96
Abhay Kumara2ae5992025-11-10 14:02:24 +000097 if result == nil && panicValue == nil && state == Closed {
khenaidooac637102019-01-14 15:44:34 -050098 // short-circuit the normal, success path without contending
99 // on the lock
100 return nil
101 }
102
103 // oh well, I guess we have to contend on the lock
104 b.processResult(result, panicValue)
105
106 if panicValue != nil {
107 // as close as Go lets us come to a "rethrow" although unfortunately
108 // we lose the original panicing location
109 panic(panicValue)
110 }
111
112 return result
113}
114
115func (b *Breaker) processResult(result error, panicValue interface{}) {
116 b.lock.Lock()
117 defer b.lock.Unlock()
118
119 if result == nil && panicValue == nil {
Abhay Kumara2ae5992025-11-10 14:02:24 +0000120 if b.state == HalfOpen {
khenaidooac637102019-01-14 15:44:34 -0500121 b.successes++
122 if b.successes == b.successThreshold {
123 b.closeBreaker()
124 }
125 }
126 } else {
127 if b.errors > 0 {
128 expiry := b.lastError.Add(b.timeout)
129 if time.Now().After(expiry) {
130 b.errors = 0
131 }
132 }
133
134 switch b.state {
Abhay Kumara2ae5992025-11-10 14:02:24 +0000135 case Closed:
khenaidooac637102019-01-14 15:44:34 -0500136 b.errors++
137 if b.errors == b.errorThreshold {
138 b.openBreaker()
139 } else {
140 b.lastError = time.Now()
141 }
Abhay Kumara2ae5992025-11-10 14:02:24 +0000142 case HalfOpen:
khenaidooac637102019-01-14 15:44:34 -0500143 b.openBreaker()
144 }
145 }
146}
147
148func (b *Breaker) openBreaker() {
Abhay Kumara2ae5992025-11-10 14:02:24 +0000149 b.changeState(Open)
khenaidooac637102019-01-14 15:44:34 -0500150 go b.timer()
151}
152
153func (b *Breaker) closeBreaker() {
Abhay Kumara2ae5992025-11-10 14:02:24 +0000154 b.changeState(Closed)
khenaidooac637102019-01-14 15:44:34 -0500155}
156
157func (b *Breaker) timer() {
158 time.Sleep(b.timeout)
159
160 b.lock.Lock()
161 defer b.lock.Unlock()
162
Abhay Kumara2ae5992025-11-10 14:02:24 +0000163 b.changeState(HalfOpen)
khenaidooac637102019-01-14 15:44:34 -0500164}
165
Abhay Kumara2ae5992025-11-10 14:02:24 +0000166func (b *Breaker) changeState(newState State) {
khenaidooac637102019-01-14 15:44:34 -0500167 b.errors = 0
168 b.successes = 0
Abhay Kumara2ae5992025-11-10 14:02:24 +0000169 atomic.StoreUint32((*uint32)(&b.state), (uint32)(newState))
khenaidooac637102019-01-14 15:44:34 -0500170}