github.com/aldelo/common@v1.5.1/wrapper/hystrixgo/hystrixgo.go (about)

     1  package hystrixgo
     2  
     3  /*
     4   * Copyright 2020-2023 Aldelo, LP
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at
     9   *
    10   *     http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"github.com/afex/hystrix-go/hystrix"
    23  	metricCollector "github.com/afex/hystrix-go/hystrix/metric_collector"
    24  	"github.com/afex/hystrix-go/plugins"
    25  	util "github.com/aldelo/common"
    26  	data "github.com/aldelo/common/wrapper/zap"
    27  	"net"
    28  	"net/http"
    29  	"strconv"
    30  )
    31  
    32  // CircuitBreaker defines one specific circuit breaker by command name
    33  //
    34  // Config Properties:
    35  //  1. Timeout = how long to wait for command to complete, in milliseconds, default = 1000
    36  //  2. MaxConcurrentRequests = how many commands of the same type can run at the same time, default = 10
    37  //  3. RequestVolumeThreshold = minimum number of requests needed before a circuit can be tripped due to health, default = 20
    38  //  4. SleepWindow = how long to wait after a circuit opens before testing for recovery, in milliseconds, default = 5000
    39  //  5. ErrorPercentThreshold = causes circuits to open once the rolling measure of errors exceeds this percent of requests, default = 50
    40  //  6. Logger = indicates the logger that will be used in the Hystrix package, default = logs nothing
    41  type CircuitBreaker struct {
    42  	// circuit breaker command name for this instance
    43  	CommandName string
    44  
    45  	// config fields
    46  	TimeOut                int
    47  	MaxConcurrentRequests  int
    48  	RequestVolumeThreshold int
    49  	SleepWindow            int
    50  	ErrorPercentThreshold  int
    51  
    52  	// config logger
    53  	Logger *data.ZapLog
    54  
    55  	// config to disable circuit breaker temporarily
    56  	DisableCircuitBreaker bool
    57  
    58  	//
    59  	// local state variables
    60  	//
    61  	streamHandler *hystrix.StreamHandler
    62  }
    63  
    64  // RunLogic declares func alias for internal Run logic handler
    65  type RunLogic func(dataIn interface{}, ctx ...context.Context) (dataOut interface{}, err error)
    66  
    67  // FallbackLogic declares func alias for internal Fallback logic handler
    68  type FallbackLogic func(dataIn interface{}, errIn error, ctx ...context.Context) (dataOut interface{}, err error)
    69  
    70  // Init will initialize the circuit break with the given command name,
    71  // a command name represents a specific service or api method that has circuit breaker being applied
    72  func (c *CircuitBreaker) Init() error {
    73  	// validate
    74  	if util.LenTrim(c.CommandName) <= 0 {
    75  		return errors.New("CircuitBreaker Init Failed: " + "Command Name is Required")
    76  	}
    77  
    78  	// set config fields to proper value
    79  	if c.TimeOut <= 0 {
    80  		c.TimeOut = 1000
    81  	}
    82  
    83  	if c.MaxConcurrentRequests <= 0 {
    84  		c.MaxConcurrentRequests = 10
    85  	}
    86  
    87  	if c.RequestVolumeThreshold <= 0 {
    88  		c.RequestVolumeThreshold = 20
    89  	}
    90  
    91  	if c.SleepWindow <= 0 {
    92  		c.SleepWindow = 5000
    93  	}
    94  
    95  	if c.ErrorPercentThreshold <= 0 {
    96  		c.ErrorPercentThreshold = 50
    97  	}
    98  
    99  	// setup circuit breaker for the given command name
   100  	hystrix.ConfigureCommand(c.CommandName, hystrix.CommandConfig{
   101  		Timeout:                c.TimeOut,
   102  		MaxConcurrentRequests:  c.MaxConcurrentRequests,
   103  		RequestVolumeThreshold: c.RequestVolumeThreshold,
   104  		SleepWindow:            c.SleepWindow,
   105  		ErrorPercentThreshold:  c.ErrorPercentThreshold,
   106  	})
   107  
   108  	// setup logger
   109  	if c.Logger != nil {
   110  		hystrix.SetLogger(c.Logger)
   111  	} else {
   112  		hystrix.SetLogger(hystrix.NoopLogger{})
   113  	}
   114  
   115  	// success
   116  	return nil
   117  }
   118  
   119  // FlushAll will purge all circuits and metrics from memory
   120  func (c *CircuitBreaker) FlushAll() {
   121  	hystrix.Flush()
   122  }
   123  
   124  // UpdateConfig will update the hystrixgo command config data to the current value in struct for a given command name
   125  func (c *CircuitBreaker) UpdateConfig() {
   126  	// command name must exist
   127  	if util.LenTrim(c.CommandName) <= 0 {
   128  		return
   129  	}
   130  
   131  	// set config fields to proper value
   132  	if c.TimeOut <= 0 {
   133  		c.TimeOut = 1000
   134  	}
   135  
   136  	if c.MaxConcurrentRequests <= 0 {
   137  		c.MaxConcurrentRequests = 10
   138  	}
   139  
   140  	if c.RequestVolumeThreshold <= 0 {
   141  		c.RequestVolumeThreshold = 20
   142  	}
   143  
   144  	if c.SleepWindow <= 0 {
   145  		c.SleepWindow = 5000
   146  	}
   147  
   148  	if c.ErrorPercentThreshold <= 0 {
   149  		c.ErrorPercentThreshold = 50
   150  	}
   151  
   152  	// setup circuit breaker for the given command name
   153  	hystrix.ConfigureCommand(c.CommandName, hystrix.CommandConfig{
   154  		Timeout:                c.TimeOut,
   155  		MaxConcurrentRequests:  c.MaxConcurrentRequests,
   156  		RequestVolumeThreshold: c.RequestVolumeThreshold,
   157  		SleepWindow:            c.SleepWindow,
   158  		ErrorPercentThreshold:  c.ErrorPercentThreshold,
   159  	})
   160  }
   161  
   162  // UpdateLogger will udpate the hystrixgo package wide logger,
   163  // based on the Logger set in the struct field
   164  func (c *CircuitBreaker) UpdateLogger() {
   165  	if c.Logger != nil {
   166  		hystrix.SetLogger(c.Logger)
   167  	} else {
   168  		hystrix.SetLogger(hystrix.NoopLogger{})
   169  	}
   170  }
   171  
   172  // Go will execute async with circuit breaker
   173  //
   174  // Parameters:
   175  //  1. run = required, defines either inline or external function to be executed,
   176  //     it is meant for a self contained function and accepts no parameter, returns error
   177  //  2. fallback = optional, defines either inline or external function to be executed as fallback when run fails,
   178  //     it is meat for a self contained function and accepts only error parameter, returns error,
   179  //     set to nil if fallback is not specified
   180  //  3. dataIn = optional, input parameter to run and fallback func, may be nil if not needed
   181  func (c *CircuitBreaker) Go(run RunLogic,
   182  	fallback FallbackLogic,
   183  	dataIn interface{}) (interface{}, error) {
   184  	// validate
   185  	if util.LenTrim(c.CommandName) <= 0 {
   186  		return nil, errors.New("Exec Async Failed: " + "CircuitBreaker Command Name is Required")
   187  	}
   188  
   189  	if run == nil {
   190  		return nil, errors.New("Exec Async for '" + c.CommandName + "' Failed: " + "Run Func Implementation is Required")
   191  	}
   192  
   193  	// execute async via circuit breaker
   194  	if !c.DisableCircuitBreaker {
   195  		//
   196  		// using circuit breaker
   197  		//
   198  		result := make(chan interface{})
   199  
   200  		errChan := hystrix.Go(c.CommandName,
   201  			func() error {
   202  				//
   203  				// run func
   204  				//
   205  				outInf, outErr := run(dataIn)
   206  
   207  				if outErr != nil {
   208  					// pass error back
   209  					return outErr
   210  				} else {
   211  					// pass result back
   212  					if outInf != nil {
   213  						result <- outInf
   214  					} else {
   215  						result <- true
   216  					}
   217  
   218  					return nil
   219  				}
   220  			},
   221  			func(er error) error {
   222  				//
   223  				// fallback func
   224  				//
   225  				if fallback != nil {
   226  					// fallback is defined
   227  					outInf, outErr := fallback(dataIn, er)
   228  
   229  					if outErr != nil {
   230  						// pass error back
   231  						return outErr
   232  					} else {
   233  						// pass result back
   234  						if outInf != nil {
   235  							result <- outInf
   236  						} else {
   237  							result <- true
   238  						}
   239  
   240  						return nil
   241  					}
   242  				} else {
   243  					// fallback is not defined
   244  					return er
   245  				}
   246  			})
   247  
   248  		var err error
   249  		var output interface{}
   250  
   251  		select {
   252  		case output = <-result:
   253  			// when no error
   254  		case err = <-errChan:
   255  			// when has error
   256  		}
   257  
   258  		if err != nil {
   259  			return nil, errors.New("Exec Async for '" + c.CommandName + "' Failed: (Go Action) " + err.Error())
   260  		} else {
   261  			return output, nil
   262  		}
   263  	} else {
   264  		//
   265  		// not using circuit breaker - pass thru
   266  		//
   267  		if obj, err := run(dataIn); err != nil {
   268  			return nil, errors.New("Exec Directly for '" + c.CommandName + "' Failed: (Non-CircuitBreaker Go Action) " + err.Error())
   269  		} else {
   270  			return obj, nil
   271  		}
   272  	}
   273  }
   274  
   275  // GoC will execute async with circuit breaker in given context
   276  //
   277  // Parameters:
   278  //  1. ctx = required, defines the context in which this method is to be run under
   279  //  2. run = required, defines either inline or external function to be executed,
   280  //     it is meant for a self contained function and accepts context.Context parameter, returns error
   281  //  3. fallback = optional, defines either inline or external function to be executed as fallback when run fails,
   282  //     it is meat for a self contained function and accepts context.Context and error parameters, returns error,
   283  //     set to nil if fallback is not specified
   284  //  4. dataIn = optional, input parameter to run and fallback func, may be nil if not needed
   285  func (c *CircuitBreaker) GoC(ctx context.Context,
   286  	run RunLogic,
   287  	fallback FallbackLogic,
   288  	dataIn interface{}) (interface{}, error) {
   289  	// validate
   290  	if util.LenTrim(c.CommandName) <= 0 {
   291  		return nil, errors.New("Exec with Context Async Failed: " + "CircuitBreaker Command Name is Required")
   292  	}
   293  
   294  	if ctx == nil {
   295  		return nil, errors.New("Exec with Context Async Failed: " + "CircuitBreaker Context is Required")
   296  	}
   297  
   298  	if run == nil {
   299  		return nil, errors.New("Exec with Context Async for '" + c.CommandName + "' Failed: " + "Run Func Implementation is Required")
   300  	}
   301  
   302  	// execute async via circuit breaker
   303  	if !c.DisableCircuitBreaker {
   304  		//
   305  		// using circuit breaker
   306  		//
   307  		result := make(chan interface{})
   308  
   309  		errChan := hystrix.GoC(ctx, c.CommandName,
   310  			func(ct context.Context) error {
   311  				//
   312  				// run func
   313  				//
   314  				outInf, outErr := run(dataIn, ct)
   315  
   316  				if outErr != nil {
   317  					// pass error back
   318  					return outErr
   319  				} else {
   320  					// pass result back
   321  					if outInf != nil {
   322  						result <- outInf
   323  					} else {
   324  						result <- true
   325  					}
   326  
   327  					return nil
   328  				}
   329  			},
   330  			func(ct context.Context, er error) error {
   331  				//
   332  				// fallback func
   333  				//
   334  				if fallback != nil {
   335  					// fallback is defined
   336  					outInf, outErr := fallback(dataIn, er, ct)
   337  
   338  					if outErr != nil {
   339  						// pass error back
   340  						return outErr
   341  					} else {
   342  						// pass result back
   343  						if outInf != nil {
   344  							result <- outInf
   345  						} else {
   346  							result <- true
   347  						}
   348  
   349  						return nil
   350  					}
   351  				} else {
   352  					// fallback is not defined
   353  					return er
   354  				}
   355  			})
   356  
   357  		var err error
   358  		var output interface{}
   359  
   360  		select {
   361  		case output = <-result:
   362  			// when no error
   363  		case err = <-errChan:
   364  			// when has error
   365  		}
   366  
   367  		if err != nil {
   368  			return nil, errors.New("Exec with Context Async for '" + c.CommandName + "' Failed: (GoC Action) " + err.Error())
   369  		} else {
   370  			return output, nil
   371  		}
   372  	} else {
   373  		//
   374  		// not using circuit breaker - pass thru
   375  		//
   376  		if obj, err := run(dataIn, ctx); err != nil {
   377  			return nil, errors.New("Exec with Context Directly for '" + c.CommandName + "' Failed: (Non-CircuitBreaker GoC Action) " + err.Error())
   378  		} else {
   379  			return obj, nil
   380  		}
   381  	}
   382  }
   383  
   384  // Do will execute synchronous with circuit breaker
   385  //
   386  // Parameters:
   387  //  1. run = required, defines either inline or external function to be executed,
   388  //     it is meant for a self contained function and accepts no parameter, returns error
   389  //  2. fallback = optional, defines either inline or external function to be executed as fallback when run fails,
   390  //     it is meat for a self contained function and accepts only error parameter, returns error,
   391  //     set to nil if fallback is not specified
   392  //  3. dataIn = optional, input parameter to run and fallback func, may be nil if not needed
   393  func (c *CircuitBreaker) Do(run RunLogic, fallback FallbackLogic, dataIn interface{}) (interface{}, error) {
   394  	// validate
   395  	if util.LenTrim(c.CommandName) <= 0 {
   396  		return nil, errors.New("Exec Synchronous Failed: " + "CircuitBreaker Command Name is Required")
   397  	}
   398  
   399  	if run == nil {
   400  		return nil, errors.New("Exec Synchronous for '" + c.CommandName + "' Failed: " + "Run Func Implementation is Required")
   401  	}
   402  
   403  	// execute synchronous via circuit breaker
   404  	if !c.DisableCircuitBreaker {
   405  		// circuit breaker
   406  		var result interface{}
   407  
   408  		if err := hystrix.Do(c.CommandName,
   409  			func() error {
   410  				// run func
   411  				outInf, outErr := run(dataIn)
   412  
   413  				if outErr != nil {
   414  					// pass error back
   415  					return outErr
   416  				} else {
   417  					// pass result back
   418  					if outInf != nil {
   419  						result = outInf
   420  					} else {
   421  						result = true
   422  					}
   423  
   424  					return nil
   425  				}
   426  			},
   427  			func(er error) error {
   428  				// fallback func
   429  				if fallback != nil {
   430  					// fallback is defined
   431  					outInf, outErr := fallback(dataIn, er)
   432  
   433  					if outErr != nil {
   434  						// pass error back
   435  						return outErr
   436  					} else {
   437  						// pass result back
   438  						if outInf != nil {
   439  							result = outInf
   440  						} else {
   441  							result = true
   442  						}
   443  
   444  						return nil
   445  					}
   446  				} else {
   447  					// fallback is not defined
   448  					return er
   449  				}
   450  			}); err != nil {
   451  			return nil, errors.New("Exec Synchronous for '" + c.CommandName + "' Failed: (Do Action) " + err.Error())
   452  		} else {
   453  			return result, nil
   454  		}
   455  	} else {
   456  		// non circuit breaker - pass thru
   457  		if obj, err := run(dataIn); err != nil {
   458  			return nil, errors.New("Exec Directly for '" + c.CommandName + "' Failed: (Non-CircuitBreaker Do Action) " + err.Error())
   459  		} else {
   460  			return obj, nil
   461  		}
   462  	}
   463  }
   464  
   465  // DoC will execute synchronous with circuit breaker in given context
   466  //
   467  // Parameters:
   468  //  1. ctx = required, defines the context in which this method is to be run under
   469  //  2. run = required, defines either inline or external function to be executed,
   470  //     it is meant for a self contained function and accepts context.Context parameter, returns error
   471  //  3. fallback = optional, defines either inline or external function to be executed as fallback when run fails,
   472  //     it is meant for a self contained function and accepts context.Context and error parameters, returns error,
   473  //     set to nil if fallback is not specified
   474  //  4. dataIn = optional, input parameter to run and fallback func, may be nil if not needed
   475  func (c *CircuitBreaker) DoC(ctx context.Context, run RunLogic, fallback FallbackLogic, dataIn interface{}) (interface{}, error) {
   476  	// validate
   477  	if util.LenTrim(c.CommandName) <= 0 {
   478  		return nil, errors.New("Exec with Context Synchronous Failed: " + "CircuitBreaker Command Name is Required")
   479  	}
   480  
   481  	if ctx == nil {
   482  		return nil, errors.New("Exec with Context Synchronous for '" + c.CommandName + "' Failed: " + "CircuitBreaker Context is Required")
   483  	}
   484  
   485  	if run == nil {
   486  		return nil, errors.New("Exec with Context Synchronous for '" + c.CommandName + "' Failed: " + "Run Func Implementation is Required")
   487  	}
   488  
   489  	// execute synchronous via circuit breaker
   490  	if !c.DisableCircuitBreaker {
   491  		// circuit breaker
   492  		var result interface{}
   493  
   494  		if err := hystrix.DoC(ctx, c.CommandName,
   495  			func(ct context.Context) error {
   496  				// run func
   497  				outInf, outErr := run(dataIn, ct)
   498  
   499  				if outErr != nil {
   500  					// pass error back
   501  					return outErr
   502  				} else {
   503  					// pass result back
   504  					if outInf != nil {
   505  						result = outInf
   506  					} else {
   507  						result = true
   508  					}
   509  
   510  					return nil
   511  				}
   512  			},
   513  			func(ct context.Context, er error) error {
   514  				// fallback func
   515  				if fallback != nil {
   516  					// fallback is defined
   517  					outInf, outErr := fallback(dataIn, er, ct)
   518  
   519  					if outErr != nil {
   520  						// pass error back
   521  						return outErr
   522  					} else {
   523  						// pass result back
   524  						if outInf != nil {
   525  							result = outInf
   526  						} else {
   527  							result = true
   528  						}
   529  
   530  						return nil
   531  					}
   532  				} else {
   533  					// fallback is not defined
   534  					return er
   535  				}
   536  			}); err != nil {
   537  			return nil, errors.New("Exec with Context Synchronous for '" + c.CommandName + "' Failed: (DoC Action) " + err.Error())
   538  		} else {
   539  			return result, nil
   540  		}
   541  	} else {
   542  		// non circuit breaker - pass thru
   543  		if obj, err := run(dataIn, ctx); err != nil {
   544  			return nil, errors.New("Exec with Context Directly for '" + c.CommandName + "' Failed: (Non-CircuitBreaker DoC Action) " + err.Error())
   545  		} else {
   546  			return obj, nil
   547  		}
   548  	}
   549  }
   550  
   551  // StartStreamHttpServer will start a simple HTTP server on local host with given port,
   552  // this will launch in goroutine, and return immediately
   553  //
   554  // # This method call is on entire hystrixgo package, not just the current circuit breaker struct
   555  //
   556  // To view stream data, launch browser, point to http://localhost:port
   557  //
   558  // default port = 81
   559  func (c *CircuitBreaker) StartStreamHttpServer(port ...int) {
   560  	if c.streamHandler == nil {
   561  		c.streamHandler = hystrix.NewStreamHandler()
   562  		c.streamHandler.Start()
   563  
   564  		p := 81
   565  
   566  		if len(port) > 0 {
   567  			p = port[0]
   568  		}
   569  
   570  		go http.ListenAndServe(net.JoinHostPort("", strconv.Itoa(p)), c.streamHandler)
   571  	}
   572  }
   573  
   574  // StopStreamHttpServer will stop the currently running stream server if already started
   575  func (c *CircuitBreaker) StopStreamHttpServer() {
   576  	if c.streamHandler != nil {
   577  		c.streamHandler.Stop()
   578  		c.streamHandler = nil
   579  	}
   580  }
   581  
   582  // StartStatsdCollector will register and initiate the hystrixgo package for metric collection into statsd,
   583  // each action from hystrixgo will be tracked into metrics and pushed into statsd via udp
   584  //
   585  // Parameters:
   586  //  1. appName = name of the app working with hystrixgo
   587  //  2. statsdIp = IP address of statsd service host
   588  //  3. statsdPort = Port of statsd service host
   589  //
   590  // statsd and graphite is a service that needs to be running on a host, either localhost or another reachable host in linux,
   591  // the easiest way to deploy statsd and graphite is via docker image:
   592  //
   593  //	see full docker install info at = https://github.com/graphite-project/docker-graphite-statsd
   594  //
   595  // docker command to run statsd and graphite as a unit as follows:
   596  //
   597  //	docker run -d --name graphite --restart=always -p 80:80 -p 2003-2004:2003-2004 -p 2023-2024:2023-2024 -p 8125:8125/udp -p 8126:8126 graphiteapp/graphite-statsd
   598  //
   599  // once docker image is running, to view graphite:
   600  //
   601  //	http://localhost/dashboard
   602  func (c *CircuitBreaker) StartStatsdCollector(appName string, statsdIp string, statsdPort ...int) error {
   603  	// validate
   604  	if util.LenTrim(appName) <= 0 {
   605  		return errors.New("Start Statsd Collector Failed: " + "App Name is Required")
   606  	}
   607  
   608  	// get ip and port
   609  	p := 8125
   610  
   611  	if len(statsdPort) > 0 {
   612  		p = statsdPort[0]
   613  	}
   614  
   615  	ip := "127.0.0.1"
   616  
   617  	if util.LenTrim(statsdIp) > 0 {
   618  		ip = statsdIp
   619  	}
   620  
   621  	// compose statsd collection
   622  	sdc, err := plugins.InitializeStatsdCollector(&plugins.StatsdCollectorConfig{
   623  		StatsdAddr: ip + ":" + strconv.Itoa(p),
   624  		Prefix:     appName + ".hystrixgo",
   625  	})
   626  
   627  	// register statsd
   628  	if err != nil {
   629  		return errors.New("Start Statsd Collector Failed: (Init Statsd Collector Action) " + err.Error())
   630  	} else {
   631  		metricCollector.Registry.Register(sdc.NewStatsdCollector)
   632  		return nil
   633  	}
   634  }