diff --git a/v3/closers/hystrix/closer.go b/v3/closers/hystrix/closer.go index 501d34e..6bd6d16 100644 --- a/v3/closers/hystrix/closer.go +++ b/v3/closers/hystrix/closer.go @@ -36,6 +36,9 @@ var _ circuit.OpenToClosed = &Closer{} // ConfigureCloser configures values for Closer type ConfigureCloser struct { + // AfterFunc should simulate time.AfterFunc + AfterFunc func(time.Duration, func()) *time.Timer `json:"-"` + // SleepWindow is https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakersleepwindowinmilliseconds SleepWindow time.Duration // HalfOpenAttempts is how many attempts to allow per SleepWindow @@ -55,6 +58,9 @@ func (c *ConfigureCloser) Merge(other ConfigureCloser) { if c.RequiredConcurrentSuccessful == 0 { c.RequiredConcurrentSuccessful = other.RequiredConcurrentSuccessful } + if c.AfterFunc == nil { + c.AfterFunc = other.AfterFunc + } } var defaultConfigureCloser = ConfigureCloser{ @@ -141,6 +147,7 @@ func (s *Closer) SetConfigThreadSafe(config ConfigureCloser) { s.mu.Lock() defer s.mu.Unlock() s.config = config + s.reopenCircuitCheck.TimeAfterFunc = config.AfterFunc s.reopenCircuitCheck.SetSleepDuration(config.SleepWindow) s.reopenCircuitCheck.SetEventCountToAllow(config.HalfOpenAttempts) s.closeOnCurrentCount.Set(config.RequiredConcurrentSuccessful) diff --git a/v3/closers/hystrix/closer_test.go b/v3/closers/hystrix/closer_test.go index 55b2d3b..8de443b 100644 --- a/v3/closers/hystrix/closer_test.go +++ b/v3/closers/hystrix/closer_test.go @@ -70,3 +70,68 @@ func TestCloser_ConcurrentAttempts(t *testing.T) { // Should reset closer assertBool(t, !c.ShouldClose(now), "Expected the circuit to not yet close") } + +func TestCloser_AfterFunc(t *testing.T) { + t.Run("afterfunc is used", func(t *testing.T) { + var invocations int + c := Closer{} + c.SetConfigNotThreadSafe(ConfigureCloser{ + AfterFunc: func(d time.Duration, f func()) *time.Timer { + invocations++ + return time.AfterFunc(d, f) + }, + RequiredConcurrentSuccessful: 3, + }) + + now := time.Now() + c.Opened(now) + c.Success(now, time.Second) + c.Success(now, time.Second) + c.Success(now, time.Second) + c.Success(now, time.Second) + + if invocations == 0 { + t.Error("Expected mock AfterFunc to be used") + } + t.Log("invocations: ", invocations) + }) + t.Run("afterfunc is set if previously nil", func(t *testing.T) { + var ( + countD int + c = ConfigureCloser{AfterFunc: nil} + d = ConfigureCloser{AfterFunc: func(d time.Duration, f func()) *time.Timer { + countD++ + return time.AfterFunc(d+1, f) + }} + ) + c.Merge(d) + _ = c.AfterFunc(time.Second, func() {}) + + if countD != 1 { + t.Errorf("expected merge to assign newer AfterFunc") + } + }) + t.Run("afterfunc is not merged if already set", func(t *testing.T) { + var ( + countC, countD int + + c = ConfigureCloser{AfterFunc: func(d time.Duration, f func()) *time.Timer { + countC++ + return time.AfterFunc(d, f) + }} + d = ConfigureCloser{AfterFunc: func(d time.Duration, f func()) *time.Timer { + countD++ + return time.AfterFunc(d+1, f) + }} + ) + c.Merge(d) + _ = c.AfterFunc(time.Second, func() {}) + + if countD > 0 { + t.Errorf("expected merge to maintain an already set AfterFunc") + } + if countC != 1 { + t.Errorf("expected post-merge to invoke initially set AfterFunc") + } + }) +}