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  }