github.com/ethersphere/bee/v2@v2.2.0/pkg/p2p/libp2p/internal/breaker/breaker.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package breaker 6 7 import ( 8 "errors" 9 "sync" 10 "time" 11 ) 12 13 const ( 14 // defaults 15 limit = 100 16 failInterval = 30 * time.Minute 17 maxBackoff = time.Hour 18 backoff = 2 * time.Minute 19 ) 20 21 var ( 22 _ Interface = (*breaker)(nil) 23 24 // ErrClosed is the special error type that indicates that breaker is closed and that is not executing functions at the moment. 25 ErrClosed = errors.New("breaker closed") 26 ) 27 28 type Interface interface { 29 // Execute runs f() if the limit number of consecutive failed calls is not reached within fail interval. 30 // f() call is not locked so it can still be executed concurrently. 31 // Returns `ErrClosed` if the limit is reached or f() result otherwise. 32 Execute(f func() error) error 33 34 // ClosedUntil returns the timestamp when the breaker will become open again. 35 ClosedUntil() time.Time 36 } 37 38 type currentTimeFn = func() time.Time 39 40 type breaker struct { 41 limit int // breaker will not execute any more tasks after limit number of consecutive failures happen 42 consFailedCalls int // current number of consecutive fails 43 firstFailedTimestamp time.Time 44 closedTimestamp time.Time 45 backoff time.Duration // initial backoff duration 46 maxBackoff time.Duration 47 failInterval time.Duration // consecutive failures are counted if they happen within this interval 48 currentTimeFn currentTimeFn 49 mtx sync.Mutex 50 } 51 52 type Options struct { 53 Limit int 54 FailInterval time.Duration 55 StartBackoff time.Duration 56 MaxBackoff time.Duration 57 } 58 59 func NewBreaker(o Options) Interface { 60 return newBreakerWithCurrentTimeFn(o, time.Now) 61 } 62 63 func newBreakerWithCurrentTimeFn(o Options, currentTimeFn currentTimeFn) Interface { 64 breaker := &breaker{ 65 limit: o.Limit, 66 backoff: o.StartBackoff, 67 maxBackoff: o.MaxBackoff, 68 failInterval: o.FailInterval, 69 currentTimeFn: currentTimeFn, 70 } 71 72 if o.Limit == 0 { 73 breaker.limit = limit 74 } 75 76 if o.FailInterval == 0 { 77 breaker.failInterval = failInterval 78 } 79 80 if o.MaxBackoff == 0 { 81 breaker.maxBackoff = maxBackoff 82 } 83 84 if o.StartBackoff == 0 { 85 breaker.backoff = backoff 86 } 87 88 return breaker 89 } 90 91 func (b *breaker) Execute(f func() error) error { 92 if err := b.beforef(); err != nil { 93 return err 94 } 95 96 return b.afterf(f()) 97 } 98 99 func (b *breaker) ClosedUntil() time.Time { 100 b.mtx.Lock() 101 defer b.mtx.Unlock() 102 103 if b.consFailedCalls >= b.limit { 104 return b.closedTimestamp.Add(b.backoff) 105 } 106 107 return b.currentTimeFn() 108 } 109 110 func (b *breaker) beforef() error { 111 b.mtx.Lock() 112 defer b.mtx.Unlock() 113 114 // use currentTimeFn().Sub() instead of time.Since() so it can be deterministically mocked in tests 115 if b.consFailedCalls >= b.limit { 116 if b.closedTimestamp.IsZero() || b.currentTimeFn().Sub(b.closedTimestamp) < b.backoff { 117 return ErrClosed 118 } 119 120 b.resetFailed() 121 if newBackoff := b.backoff * 2; newBackoff <= b.maxBackoff { 122 b.backoff = newBackoff 123 } else { 124 b.backoff = b.maxBackoff 125 } 126 } 127 128 if !b.firstFailedTimestamp.IsZero() && b.currentTimeFn().Sub(b.firstFailedTimestamp) >= b.failInterval { 129 b.resetFailed() 130 } 131 132 return nil 133 } 134 135 func (b *breaker) afterf(err error) error { 136 b.mtx.Lock() 137 defer b.mtx.Unlock() 138 if err != nil { 139 if b.consFailedCalls == 0 { 140 b.firstFailedTimestamp = b.currentTimeFn() 141 } 142 143 b.consFailedCalls++ 144 if b.consFailedCalls == b.limit { 145 b.closedTimestamp = b.currentTimeFn() 146 } 147 148 return err 149 } 150 151 b.resetFailed() 152 return nil 153 } 154 155 func (b *breaker) resetFailed() { 156 b.consFailedCalls = 0 157 b.firstFailedTimestamp = time.Time{} 158 }