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 }