├── go.mod ├── docs.go ├── errors.go ├── interface.go ├── LICENSE ├── deprecated.go ├── multiplexer.go └── breaker.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kamilsk/breaker 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | // Package breaker provides flexible mechanism 2 | // to make execution flow interruptible. 3 | // The breaker carries a cancellation signal 4 | // to interrupt an action execution. 5 | package breaker 6 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | // Interrupted is the error returned by the breaker 4 | // when a cancellation signal occurred. 5 | const Interrupted Error = "operation interrupted" 6 | 7 | // Error defines the package errors. 8 | type Error string 9 | 10 | // Error returns the string representation of an error. 11 | func (err Error) Error() string { 12 | return string(err) 13 | } 14 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | // Interface carries a cancellation signal to interrupt an action execution. 4 | // 5 | // Example based on github.com/kamilsk/retry/v5 module: 6 | // 7 | // if err := retry.Do(breaker.BreakByTimeout(time.Minute), action); err != nil { 8 | // log.Fatal(err) 9 | // } 10 | // 11 | // Example based on github.com/kamilsk/semaphore/v5 module: 12 | // 13 | // if err := semaphore.Acquire(breaker.BreakByTimeout(time.Minute), 5); err != nil { 14 | // log.Fatal(err) 15 | // } 16 | // 17 | type Interface interface { 18 | // Close closes the Done channel and releases resources associated with it. 19 | Close() 20 | // Done returns a channel that's closed when a cancellation signal occurred. 21 | Done() <-chan struct{} 22 | // If Done is not yet closed, Err returns nil. 23 | // If Done is closed, Err returns a non-nil error. 24 | // After Err returns a non-nil error, successive calls to Err return the same error. 25 | Err() error 26 | 27 | // trigger is a private method to guarantee that the breakers come from 28 | // this package and all of them return a valid Done channel. 29 | trigger() Interface 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 OctoLab, https://www.octolab.org/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /deprecated.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | import "context" 4 | 5 | // MultiplexTwo combines two breakers into one. 6 | // 7 | // interrupter := breaker.MultiplexTwo( 8 | // breaker.BreakByContext(req.Context()), 9 | // breaker.BreakBySignal(os.Interrupt), 10 | // ) 11 | // defer interrupter.Close() 12 | // 13 | // background.Job().Do(interrupter) 14 | // 15 | // Deprecated: Multiplex has the same optimization under the hood now. 16 | // It will be removed at v2. 17 | func MultiplexTwo(one, two Interface) Interface { 18 | return newMultiplexedBreaker([]Interface{one, two, stub{}}).trigger() 19 | } 20 | 21 | // MultiplexThree combines three breakers into one. 22 | // It's an optimized version of a more generic Multiplex. 23 | // 24 | // interrupter := breaker.MultiplexThree( 25 | // breaker.BreakByContext(req.Context()), 26 | // breaker.BreakBySignal(os.Interrupt), 27 | // breaker.BreakByTimeout(time.Minute), 28 | // ) 29 | // defer interrupter.Close() 30 | // 31 | // background.Job().Do(interrupter) 32 | // 33 | // Deprecated: Multiplex has the same optimization under the hood now. 34 | // It will be removed at v2. 35 | func MultiplexThree(one, two, three Interface) Interface { 36 | return newMultiplexedBreaker([]Interface{one, two, three}).trigger() 37 | } 38 | 39 | // WithContext returns a new breaker and an associated Context based on the passed one. 40 | // 41 | // interrupter, ctx := breaker.WithContext(req.Context()) 42 | // defer interrupter.Close() 43 | // 44 | // background.Job().Run(ctx) 45 | // 46 | // Deprecated: use BreakByContext instead. 47 | // It will be removed at v2. 48 | func WithContext(ctx context.Context) (Interface, context.Context) { 49 | ctx, cancel := context.WithCancel(ctx) 50 | return (&contextBreaker{ctx, cancel}).trigger(), ctx 51 | } 52 | -------------------------------------------------------------------------------- /multiplexer.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | import "reflect" 4 | 5 | // Multiplex combines multiple breakers into one. 6 | // 7 | // interrupter := breaker.Multiplex( 8 | // breaker.BreakByContext(req.Context()), 9 | // breaker.BreakBySignal(os.Interrupt), 10 | // breaker.BreakByTimeout(time.Minute), 11 | // ) 12 | // defer interrupter.Close() 13 | // 14 | // background.Job().Do(interrupter) 15 | // 16 | func Multiplex(breakers ...Interface) Interface { 17 | if len(breakers) == 0 { 18 | return closedBreaker() 19 | } 20 | for len(breakers) < 3 { 21 | breakers = append(breakers, stub{}) 22 | } 23 | return newMultiplexedBreaker(breakers).trigger() 24 | } 25 | 26 | func newMultiplexedBreaker(breakers []Interface) *multiplexedBreaker { 27 | return &multiplexedBreaker{newBreaker(), make(chan struct{}), breakers} 28 | } 29 | 30 | type multiplexedBreaker struct { 31 | *breaker 32 | internal chan struct{} 33 | external []Interface 34 | } 35 | 36 | // Close closes the Done channel and releases resources associated with it. 37 | func (br *multiplexedBreaker) Close() { 38 | br.closer.Do(func() { close(br.internal) }) 39 | } 40 | 41 | // trigger starts listening to the all Done channels of multiplexed breakers. 42 | func (br *multiplexedBreaker) trigger() Interface { 43 | go func() { 44 | if len(br.external) == 3 { 45 | select { 46 | case <-br.external[0].Done(): 47 | case <-br.external[1].Done(): 48 | case <-br.external[2].Done(): 49 | case <-br.internal: 50 | } 51 | } else { 52 | brs := make([]reflect.SelectCase, 0, len(br.external)+1) 53 | brs = append(brs, reflect.SelectCase{ 54 | Dir: reflect.SelectRecv, 55 | Chan: reflect.ValueOf(br.internal), 56 | }) 57 | for _, br := range br.external { 58 | brs = append(brs, reflect.SelectCase{ 59 | Dir: reflect.SelectRecv, 60 | Chan: reflect.ValueOf(br.Done()), 61 | }) 62 | } 63 | reflect.Select(brs) 64 | } 65 | each(br.external).Close() 66 | br.Close() 67 | close(br.signal) 68 | }() 69 | return br 70 | } 71 | 72 | type each []Interface 73 | 74 | // Close closes all Done channels of a list of breakers 75 | // and releases resources associated with them. 76 | func (list each) Close() { 77 | for _, br := range list { 78 | br.Close() 79 | } 80 | } 81 | 82 | type stub struct{} 83 | 84 | func (br stub) Close() {} 85 | func (br stub) Done() <-chan struct{} { return nil } 86 | func (br stub) Err() error { return Interrupted } 87 | func (br stub) IsReleased() bool { return true } 88 | func (br stub) trigger() Interface { return br } 89 | -------------------------------------------------------------------------------- /breaker.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // New returns a new breaker, which can be interrupted only by a Close call. 12 | // 13 | // interrupter := breaker.New() 14 | // go background.Job().Do(interrupter) 15 | // 16 | // <-time.After(time.Minute) 17 | // interrupter.Close() 18 | // 19 | func New() Interface { 20 | return newBreaker().trigger() 21 | } 22 | 23 | // BreakByChannel returns a new breaker based on the channel. 24 | // 25 | // signal := make(chan struct{}) 26 | // go func() { 27 | // <-time.After(time.Minute) 28 | // close(signal) 29 | // }() 30 | // 31 | // interrupter := breaker.BreakByChannel(signal) 32 | // defer interrupter.Close() 33 | // 34 | // background.Job().Do(interrupter) 35 | // 36 | func BreakByChannel(signal <-chan struct{}) Interface { 37 | return (&channelBreaker{newBreaker(), make(chan struct{}), signal}).trigger() 38 | } 39 | 40 | // BreakByContext returns a new breaker based on the Context. 41 | // 42 | // interrupter := breaker.BreakByContext(context.WithTimeout(req.Context(), time.Minute)) 43 | // defer interrupter.Close() 44 | // 45 | // background.Job().Do(interrupter) 46 | // 47 | func BreakByContext(ctx context.Context, cancel context.CancelFunc) Interface { 48 | return (&contextBreaker{ctx, cancel}).trigger() 49 | } 50 | 51 | // BreakByDeadline closes the Done channel when the deadline occurs. 52 | // 53 | // interrupter := breaker.BreakByDeadline(time.Now().Add(time.Minute)) 54 | // defer interrupter.Close() 55 | // 56 | // background.Job().Do(interrupter) 57 | // 58 | func BreakByDeadline(deadline time.Time) Interface { 59 | timeout := time.Until(deadline) 60 | if timeout < 0 { 61 | return closedBreaker() 62 | } 63 | return newTimeoutBreaker(timeout).trigger() 64 | } 65 | 66 | // BreakBySignal closes the Done channel when the breaker will receive OS signals. 67 | // 68 | // interrupter := breaker.BreakBySignal(os.Interrupt) 69 | // defer interrupter.Close() 70 | // 71 | // background.Job().Do(interrupter) 72 | // 73 | func BreakBySignal(sig ...os.Signal) Interface { 74 | if len(sig) == 0 { 75 | return closedBreaker() 76 | } 77 | return newSignalBreaker(sig).trigger() 78 | } 79 | 80 | // BreakByTimeout closes the Done channel when the timeout happens. 81 | // 82 | // interrupter := breaker.BreakByTimeout(time.Minute) 83 | // defer interrupter.Close() 84 | // 85 | // background.Job().Do(interrupter) 86 | // 87 | func BreakByTimeout(timeout time.Duration) Interface { 88 | if timeout < 0 { 89 | return closedBreaker() 90 | } 91 | return newTimeoutBreaker(timeout).trigger() 92 | } 93 | 94 | // ToContext converts the breaker into the Context. 95 | // 96 | // interrupter := breaker.Multiplex( 97 | // breaker.BreakBySignal(os.Interrupt), 98 | // breaker.BreakByTimeout(time.Minute), 99 | // ) 100 | // defer interrupter.Close() 101 | // 102 | // request, err := http.NewRequestWithContext(breaker.ToContext(interrupter), ...) 103 | // if err != nil { handle(err) } 104 | // 105 | // response, err := http.DefaultClient.Do(request) 106 | // if err != nil { handle(err) } 107 | // handle(response) 108 | // 109 | func ToContext(br Interface) context.Context { 110 | ctx, cancel := context.WithCancel(context.Background()) 111 | go func() { 112 | <-br.Done() 113 | cancel() 114 | }() 115 | return ctx 116 | } 117 | 118 | func closedBreaker() Interface { 119 | br := newBreaker() 120 | br.Close() 121 | return br 122 | } 123 | 124 | func newBreaker() *breaker { 125 | return &breaker{signal: make(chan struct{})} 126 | } 127 | 128 | type breaker struct { 129 | closer sync.Once 130 | signal chan struct{} 131 | } 132 | 133 | // Close closes the Done channel and releases resources associated with it. 134 | func (br *breaker) Close() { 135 | br.closer.Do(func() { close(br.signal) }) 136 | } 137 | 138 | // Done returns a channel that's closed when a cancellation signal occurred. 139 | func (br *breaker) Done() <-chan struct{} { 140 | return br.signal 141 | } 142 | 143 | // Err returns a non-nil error if the Done channel is closed and nil otherwise. 144 | // After Err returns a non-nil error, successive calls to Err return the same error. 145 | func (br *breaker) Err() error { 146 | select { 147 | case <-br.signal: 148 | return Interrupted 149 | default: 150 | return nil 151 | } 152 | } 153 | 154 | // IsReleased returns true if resources associated with the breaker were released. 155 | // 156 | // Deprecated: see the extended interface. 157 | func (br *breaker) IsReleased() bool { 158 | return br.Err() != nil 159 | } 160 | 161 | func (br *breaker) trigger() Interface { 162 | return br 163 | } 164 | 165 | type channelBreaker struct { 166 | *breaker 167 | internal chan struct{} 168 | external <-chan struct{} 169 | } 170 | 171 | // Close closes the Done channel and releases resources associated with it. 172 | func (br *channelBreaker) Close() { 173 | br.closer.Do(func() { close(br.internal) }) 174 | } 175 | 176 | // trigger starts listening to the internal signal to close the Done channel. 177 | func (br *channelBreaker) trigger() Interface { 178 | go func() { 179 | select { 180 | case <-br.external: 181 | br.Close() 182 | case <-br.internal: 183 | } 184 | close(br.signal) 185 | }() 186 | return br 187 | } 188 | 189 | type contextBreaker struct { 190 | context.Context 191 | cancel context.CancelFunc 192 | } 193 | 194 | // Close closes the Done channel and releases resources associated with it. 195 | func (br *contextBreaker) Close() { 196 | br.cancel() 197 | } 198 | 199 | // IsReleased returns true if resources associated with the breaker were released. 200 | // 201 | // Deprecated: see the extended interface. 202 | func (br *contextBreaker) IsReleased() bool { 203 | return br.Err() != nil 204 | } 205 | 206 | func (br *contextBreaker) trigger() Interface { 207 | return br 208 | } 209 | 210 | func newSignalBreaker(signals []os.Signal) *signalBreaker { 211 | return &signalBreaker{newBreaker(), make(chan struct{}), make(chan os.Signal, len(signals)), signals} 212 | } 213 | 214 | type signalBreaker struct { 215 | *breaker 216 | internal chan struct{} 217 | external chan os.Signal 218 | signals []os.Signal 219 | } 220 | 221 | // Close closes the Done channel and releases resources associated with it. 222 | func (br *signalBreaker) Close() { 223 | br.closer.Do(func() { close(br.internal) }) 224 | } 225 | 226 | // trigger starts listening to the required signals to close the Done channel. 227 | func (br *signalBreaker) trigger() Interface { 228 | go func() { 229 | signal.Notify(br.external, br.signals...) 230 | select { 231 | case <-br.external: 232 | br.Close() 233 | case <-br.internal: 234 | } 235 | signal.Stop(br.external) 236 | close(br.external) 237 | close(br.signal) 238 | }() 239 | return br 240 | } 241 | 242 | func newTimeoutBreaker(timeout time.Duration) *timeoutBreaker { 243 | return &timeoutBreaker{newBreaker(), make(chan struct{}), time.NewTimer(timeout)} 244 | } 245 | 246 | type timeoutBreaker struct { 247 | *breaker 248 | internal chan struct{} 249 | external *time.Timer 250 | } 251 | 252 | // Close closes the Done channel and releases resources associated with it. 253 | func (br *timeoutBreaker) Close() { 254 | br.closer.Do(func() { close(br.internal) }) 255 | } 256 | 257 | // trigger starts listening to the internal timer to close the Done channel. 258 | func (br *timeoutBreaker) trigger() Interface { 259 | go func() { 260 | select { 261 | case <-br.external.C: 262 | br.Close() 263 | case <-br.internal: 264 | } 265 | stop(br.external) 266 | close(br.signal) 267 | }() 268 | return br 269 | } 270 | 271 | func stop(timer *time.Timer) { 272 | if !timer.Stop() { 273 | select { 274 | case <-timer.C: 275 | default: 276 | } 277 | } 278 | } 279 | --------------------------------------------------------------------------------