2024-03-11 11:09:50 +00:00
package circuitbreaker
import (
2024-07-01 15:46:57 +00:00
"context"
2024-03-11 11:09:50 +00:00
"errors"
2024-07-01 15:46:57 +00:00
"fmt"
2024-03-11 11:09:50 +00:00
"testing"
"time"
2024-07-01 15:46:57 +00:00
"github.com/afex/hystrix-go/hystrix"
"github.com/stretchr/testify/assert"
2024-03-11 11:09:50 +00:00
"github.com/stretchr/testify/require"
)
const success = "Success"
func TestCircuitBreaker_ExecuteSuccessSingle ( t * testing . T ) {
cb := NewCircuitBreaker ( Config {
Timeout : 1000 ,
MaxConcurrentRequests : 100 ,
RequestVolumeThreshold : 10 ,
SleepWindow : 10 ,
ErrorPercentThreshold : 10 ,
} )
expectedResult := success
2024-07-01 15:46:57 +00:00
circuitName := "SuccessSingle"
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return [ ] any { expectedResult } , nil
} , circuitName ) } ,
)
2024-03-11 11:09:50 +00:00
result := cb . Execute ( cmd )
require . NoError ( t , result . Error ( ) )
require . Equal ( t , expectedResult , result . Result ( ) [ 0 ] . ( string ) )
}
func TestCircuitBreaker_ExecuteMultipleFallbacksFail ( t * testing . T ) {
cb := NewCircuitBreaker ( Config {
Timeout : 10 ,
MaxConcurrentRequests : 100 ,
RequestVolumeThreshold : 10 ,
SleepWindow : 10 ,
ErrorPercentThreshold : 10 ,
} )
2024-08-01 22:29:05 +00:00
circuitName := fmt . Sprintf ( "ExecuteMultipleFallbacksFail_%d" , time . Now ( ) . Nanosecond ( ) ) // unique name to avoid conflicts with go tests `-count` option
2024-07-01 15:46:57 +00:00
errSecProvFailed := errors . New ( "provider 2 failed" )
errThirdProvFailed := errors . New ( "provider 3 failed" )
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
time . Sleep ( 100 * time . Millisecond ) // will cause hystrix: timeout
return [ ] any { success } , nil
2024-08-01 22:29:05 +00:00
} , circuitName + "1" ) ,
2024-07-01 15:46:57 +00:00
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return nil , errSecProvFailed
2024-08-01 22:29:05 +00:00
} , circuitName + "2" ) ,
2024-07-01 15:46:57 +00:00
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return nil , errThirdProvFailed
2024-08-01 22:29:05 +00:00
} , circuitName + "3" ) ,
2024-07-01 15:46:57 +00:00
} )
2024-03-11 11:09:50 +00:00
result := cb . Execute ( cmd )
require . Error ( t , result . Error ( ) )
2024-07-01 15:46:57 +00:00
assert . True ( t , errors . Is ( result . Error ( ) , hystrix . ErrTimeout ) )
assert . True ( t , errors . Is ( result . Error ( ) , errSecProvFailed ) )
assert . True ( t , errors . Is ( result . Error ( ) , errThirdProvFailed ) )
2024-03-11 11:09:50 +00:00
}
func TestCircuitBreaker_ExecuteMultipleFallbacksFailButLastSuccessStress ( t * testing . T ) {
cb := NewCircuitBreaker ( Config {
Timeout : 10 ,
MaxConcurrentRequests : 100 ,
RequestVolumeThreshold : 10 ,
SleepWindow : 10 ,
ErrorPercentThreshold : 10 ,
} )
expectedResult := success
2024-07-01 15:46:57 +00:00
circuitName := fmt . Sprintf ( "LastSuccessStress_%d" , time . Now ( ) . Nanosecond ( ) ) // unique name to avoid conflicts with go tests `-count` option
2024-03-11 11:09:50 +00:00
// These are executed sequentially, but I had an issue with the test failing
// because of the open circuit
for i := 0 ; i < 1000 ; i ++ {
2024-07-01 15:46:57 +00:00
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return nil , errors . New ( "provider 1 failed" )
} , circuitName + "1" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return nil , errors . New ( "provider 2 failed" )
} , circuitName + "2" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return [ ] any { expectedResult } , nil
} , circuitName + "3" ) ,
} ,
)
2024-03-11 11:09:50 +00:00
result := cb . Execute ( cmd )
require . NoError ( t , result . Error ( ) )
require . Equal ( t , expectedResult , result . Result ( ) [ 0 ] . ( string ) )
}
}
2024-07-01 15:46:57 +00:00
func TestCircuitBreaker_ExecuteSwitchToWorkingProviderOnVolumeThresholdReached ( t * testing . T ) {
cb := NewCircuitBreaker ( Config {
RequestVolumeThreshold : 10 ,
} )
expectedResult := success
circuitName := fmt . Sprintf ( "SwitchToWorkingProviderOnVolumeThresholdReached_%d" , time . Now ( ) . Nanosecond ( ) ) // unique name to avoid conflicts with go tests `-count` option
prov1Called := 0
prov2Called := 0
prov3Called := 0
// These are executed sequentially
for i := 0 ; i < 20 ; i ++ {
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov1Called ++
return nil , errors . New ( "provider 1 failed" )
} , circuitName + "1" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov2Called ++
return nil , errors . New ( "provider 2 failed" )
} , circuitName + "2" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov3Called ++
return [ ] any { expectedResult } , nil
} , circuitName + "3" ) ,
} )
result := cb . Execute ( cmd )
require . NoError ( t , result . Error ( ) )
require . Equal ( t , expectedResult , result . Result ( ) [ 0 ] . ( string ) )
}
assert . Equal ( t , 10 , prov1Called )
assert . Equal ( t , 10 , prov2Called )
assert . Equal ( t , 20 , prov3Called )
}
func TestCircuitBreaker_ExecuteHealthCheckOnWindowTimeout ( t * testing . T ) {
sleepWindow := 10
cb := NewCircuitBreaker ( Config {
RequestVolumeThreshold : 1 , // 1 failed request is enough to trip the circuit
SleepWindow : sleepWindow ,
ErrorPercentThreshold : 1 , // Trip on first error
} )
expectedResult := success
circuitName := fmt . Sprintf ( "SwitchToWorkingProviderOnWindowTimeout_%d" , time . Now ( ) . Nanosecond ( ) ) // unique name to avoid conflicts with go tests `-count` option
prov1Called := 0
prov2Called := 0
// These are executed sequentially
for i := 0 ; i < 10 ; i ++ {
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov1Called ++
return nil , errors . New ( "provider 1 failed" )
} , circuitName + "1" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov2Called ++
return [ ] any { expectedResult } , nil
} , circuitName + "2" ) ,
} )
result := cb . Execute ( cmd )
require . NoError ( t , result . Error ( ) )
require . Equal ( t , expectedResult , result . Result ( ) [ 0 ] . ( string ) )
}
2024-07-17 14:05:51 +00:00
assert . Less ( t , prov1Called , 3 ) // most of the time only 1 call is made, but occasionally 2 can happen
2024-07-01 15:46:57 +00:00
assert . Equal ( t , 10 , prov2Called )
2024-08-01 22:29:05 +00:00
assert . True ( t , CircuitExists ( circuitName + "1" ) )
assert . True ( t , IsCircuitOpen ( circuitName + "1" ) )
2024-07-01 15:46:57 +00:00
// Wait for the sleep window to expire
time . Sleep ( time . Duration ( sleepWindow + 1 ) * time . Millisecond )
cmd := NewCommand ( context . TODO ( ) , [ ] * Functor {
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov1Called ++
return [ ] any { expectedResult } , nil // Now it is working
} , circuitName + "1" ) ,
NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov2Called ++
return [ ] any { expectedResult } , nil
} , circuitName + "2" ) ,
} )
result := cb . Execute ( cmd )
require . NoError ( t , result . Error ( ) )
2024-07-17 14:05:51 +00:00
assert . Less ( t , prov1Called , 4 ) // most of the time only 2 calls are made, but occasionally 3 can happen
2024-07-01 15:46:57 +00:00
assert . Equal ( t , 10 , prov2Called )
}
func TestCircuitBreaker_CommandCancel ( t * testing . T ) {
cb := NewCircuitBreaker ( Config { } )
circuitName := fmt . Sprintf ( "CommandCancel_%d" , time . Now ( ) . Nanosecond ( ) ) // unique name to avoid conflicts with go tests `-count` option
prov1Called := 0
prov2Called := 0
2024-07-03 13:10:10 +00:00
var ctx context . Context
2024-07-01 15:46:57 +00:00
expectedErr := errors . New ( "provider 1 failed" )
2024-07-03 13:10:10 +00:00
cmd := NewCommand ( ctx , nil )
2024-07-01 15:46:57 +00:00
cmd . Add ( NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov1Called ++
cmd . Cancel ( )
return nil , expectedErr
} , circuitName + "1" ) )
cmd . Add ( NewFunctor ( func ( ) ( [ ] interface { } , error ) {
prov2Called ++
return nil , errors . New ( "provider 2 failed" )
} , circuitName + "2" ) )
result := cb . Execute ( cmd )
require . True ( t , errors . Is ( result . Error ( ) , expectedErr ) )
assert . Equal ( t , 1 , prov1Called )
assert . Equal ( t , 0 , prov2Called )
}
2024-07-03 13:10:10 +00:00
func TestCircuitBreaker_EmptyOrNilCommand ( t * testing . T ) {
cb := NewCircuitBreaker ( Config { } )
cmd := NewCommand ( context . TODO ( ) , nil )
result := cb . Execute ( cmd )
require . Error ( t , result . Error ( ) )
result = cb . Execute ( nil )
require . Error ( t , result . Error ( ) )
}
2024-08-01 22:29:05 +00:00
func TestCircuitBreaker_CircuitExistsAndClosed ( t * testing . T ) {
timestamp := time . Now ( ) . Nanosecond ( )
nonExCircuit := fmt . Sprintf ( "nonexistent_%d" , timestamp ) // unique name to avoid conflicts with go tests `-count` option
require . False ( t , CircuitExists ( nonExCircuit ) )
cb := NewCircuitBreaker ( Config { } )
cmd := NewCommand ( context . TODO ( ) , nil )
existCircuit := fmt . Sprintf ( "existing_%d" , timestamp ) // unique name to avoid conflicts with go tests `-count` option
cmd . Add ( NewFunctor ( func ( ) ( [ ] interface { } , error ) {
return nil , nil
} , existCircuit ) )
_ = cb . Execute ( cmd )
require . True ( t , CircuitExists ( existCircuit ) )
require . False ( t , IsCircuitOpen ( existCircuit ) )
}