blob: 9214386254f49e862d2d4d497b948701d7e8b96c [file] [log] [blame]
Scott Baker2c1c4822019-10-16 11:02:41 -07001// 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 Kumar40252eb2025-10-13 13:25:53 +000015// State is a type representing the possible states of a circuit breaker.
16type State uint32
17
Scott Baker2c1c4822019-10-16 11:02:41 -070018const (
Abhay Kumar40252eb2025-10-13 13:25:53 +000019 Closed State = iota
20 Open
21 HalfOpen
Scott Baker2c1c4822019-10-16 11:02:41 -070022)
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 Kumar40252eb2025-10-13 13:25:53 +000030 state State
Scott Baker2c1c4822019-10-16 11:02:41 -070031 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 Kumar40252eb2025-10-13 13:25:53 +000052 state := b.GetState()
Scott Baker2c1c4822019-10-16 11:02:41 -070053
Abhay Kumar40252eb2025-10-13 13:25:53 +000054 if state == Open {
Scott Baker2c1c4822019-10-16 11:02:41 -070055 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 Kumar40252eb2025-10-13 13:25:53 +000067 state := b.GetState()
Scott Baker2c1c4822019-10-16 11:02:41 -070068
Abhay Kumar40252eb2025-10-13 13:25:53 +000069 if state == Open {
Scott Baker2c1c4822019-10-16 11:02:41 -070070 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 Kumar40252eb2025-10-13 13:25:53 +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 {
Scott Baker2c1c4822019-10-16 11:02:41 -070088 var panicValue interface{}
89
90 result := func() error {
91 defer func() {
92 panicValue = recover()
93 }()
94 return work()
95 }()
96
Abhay Kumar40252eb2025-10-13 13:25:53 +000097 if result == nil && panicValue == nil && state == Closed {
Scott Baker2c1c4822019-10-16 11:02:41 -070098 // 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 Kumar40252eb2025-10-13 13:25:53 +0000120 if b.state == HalfOpen {
Scott Baker2c1c4822019-10-16 11:02:41 -0700121 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 Kumar40252eb2025-10-13 13:25:53 +0000135 case Closed:
Scott Baker2c1c4822019-10-16 11:02:41 -0700136 b.errors++
137 if b.errors == b.errorThreshold {
138 b.openBreaker()
139 } else {
140 b.lastError = time.Now()
141 }
Abhay Kumar40252eb2025-10-13 13:25:53 +0000142 case HalfOpen:
Scott Baker2c1c4822019-10-16 11:02:41 -0700143 b.openBreaker()
144 }
145 }
146}
147
148func (b *Breaker) openBreaker() {
Abhay Kumar40252eb2025-10-13 13:25:53 +0000149 b.changeState(Open)
Scott Baker2c1c4822019-10-16 11:02:41 -0700150 go b.timer()
151}
152
153func (b *Breaker) closeBreaker() {
Abhay Kumar40252eb2025-10-13 13:25:53 +0000154 b.changeState(Closed)
Scott Baker2c1c4822019-10-16 11:02:41 -0700155}
156
157func (b *Breaker) timer() {
158 time.Sleep(b.timeout)
159
160 b.lock.Lock()
161 defer b.lock.Unlock()
162
Abhay Kumar40252eb2025-10-13 13:25:53 +0000163 b.changeState(HalfOpen)
Scott Baker2c1c4822019-10-16 11:02:41 -0700164}
165
Abhay Kumar40252eb2025-10-13 13:25:53 +0000166func (b *Breaker) changeState(newState State) {
Scott Baker2c1c4822019-10-16 11:02:41 -0700167 b.errors = 0
168 b.successes = 0
Abhay Kumar40252eb2025-10-13 13:25:53 +0000169 atomic.StoreUint32((*uint32)(&b.state), (uint32)(newState))
Scott Baker2c1c4822019-10-16 11:02:41 -0700170}