github.com/status-im/status-go@v1.1.0/circuitbreaker/circuit_breaker.go (about) 1 package circuitbreaker 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/afex/hystrix-go/hystrix" 8 9 "github.com/ethereum/go-ethereum/log" 10 ) 11 12 type FallbackFunc func() ([]any, error) 13 14 type CommandResult struct { 15 res []any 16 err error 17 } 18 19 func (cr CommandResult) Result() []any { 20 return cr.res 21 } 22 23 func (cr CommandResult) Error() error { 24 return cr.err 25 } 26 27 type Command struct { 28 ctx context.Context 29 functors []*Functor 30 cancel bool 31 } 32 33 func NewCommand(ctx context.Context, functors []*Functor) *Command { 34 return &Command{ 35 ctx: ctx, 36 functors: functors, 37 } 38 } 39 40 func (cmd *Command) Add(ftor *Functor) { 41 cmd.functors = append(cmd.functors, ftor) 42 } 43 44 func (cmd *Command) IsEmpty() bool { 45 return len(cmd.functors) == 0 46 } 47 48 func (cmd *Command) Cancel() { 49 cmd.cancel = true 50 } 51 52 type Config struct { 53 Timeout int 54 MaxConcurrentRequests int 55 RequestVolumeThreshold int 56 SleepWindow int 57 ErrorPercentThreshold int 58 } 59 60 type CircuitBreaker struct { 61 config Config 62 circuitNameHandler func(string) string 63 } 64 65 func NewCircuitBreaker(config Config) *CircuitBreaker { 66 return &CircuitBreaker{ 67 config: config, 68 } 69 } 70 71 type Functor struct { 72 exec FallbackFunc 73 circuitName string 74 } 75 76 func NewFunctor(exec FallbackFunc, circuitName string) *Functor { 77 return &Functor{ 78 exec: exec, 79 circuitName: circuitName, 80 } 81 } 82 83 func accumulateCommandError(result CommandResult, circuitName string, err error) CommandResult { 84 // Accumulate errors 85 if result.err != nil { 86 result.err = fmt.Errorf("%w, %s.error: %w", result.err, circuitName, err) 87 } else { 88 result.err = fmt.Errorf("%s.error: %w", circuitName, err) 89 } 90 return result 91 } 92 93 // Executes the command in its circuit if set. 94 // If the command's circuit is not configured, the circuit of the CircuitBreaker is used. 95 // This is a blocking function. 96 func (cb *CircuitBreaker) Execute(cmd *Command) CommandResult { 97 if cmd == nil || cmd.IsEmpty() { 98 return CommandResult{err: fmt.Errorf("command is nil or empty")} 99 } 100 101 var result CommandResult 102 ctx := cmd.ctx 103 if ctx == nil { 104 ctx = context.Background() 105 } 106 107 for i, f := range cmd.functors { 108 if cmd.cancel { 109 break 110 } 111 112 var err error 113 // if last command, execute without circuit 114 if i == len(cmd.functors)-1 { 115 res, execErr := f.exec() 116 err = execErr 117 if err == nil { 118 result = CommandResult{res: res} 119 } 120 } else { 121 circuitName := f.circuitName 122 if cb.circuitNameHandler != nil { 123 circuitName = cb.circuitNameHandler(circuitName) 124 } 125 126 if hystrix.GetCircuitSettings()[circuitName] == nil { 127 hystrix.ConfigureCommand(circuitName, hystrix.CommandConfig{ 128 Timeout: cb.config.Timeout, 129 MaxConcurrentRequests: cb.config.MaxConcurrentRequests, 130 RequestVolumeThreshold: cb.config.RequestVolumeThreshold, 131 SleepWindow: cb.config.SleepWindow, 132 ErrorPercentThreshold: cb.config.ErrorPercentThreshold, 133 }) 134 } 135 136 err = hystrix.DoC(ctx, circuitName, func(ctx context.Context) error { 137 res, err := f.exec() 138 // Write to result only if success 139 if err == nil { 140 result = CommandResult{res: res} 141 } 142 143 // If the command has been cancelled, we don't count 144 // the error towars breaking the circuit, and then we break 145 if cmd.cancel { 146 result = accumulateCommandError(result, f.circuitName, err) 147 return nil 148 } 149 if err != nil { 150 log.Warn("hystrix error", "error", err, "provider", circuitName) 151 } 152 return err 153 }, nil) 154 } 155 if err == nil { 156 break 157 } 158 159 result = accumulateCommandError(result, f.circuitName, err) 160 161 // Lets abuse every provider with the same amount of MaxConcurrentRequests, 162 // keep iterating even in case of ErrMaxConcurrency error 163 } 164 return result 165 } 166 167 func (c *CircuitBreaker) SetOverrideCircuitNameHandler(f func(string) string) { 168 c.circuitNameHandler = f 169 } 170 171 // Expects a circuit to exist because a new circuit is always closed. 172 // Call CircuitExists to check if a circuit exists. 173 func IsCircuitOpen(circuitName string) bool { 174 circuit, wasCreated, _ := hystrix.GetCircuit(circuitName) 175 return !wasCreated && circuit.IsOpen() 176 } 177 178 func CircuitExists(circuitName string) bool { 179 _, wasCreated, _ := hystrix.GetCircuit(circuitName) 180 return !wasCreated 181 }