diff --git a/cbreaker/cbreaker.go b/cbreaker/cbreaker.go index b330bf01..5c778d06 100644 --- a/cbreaker/cbreaker.go +++ b/cbreaker/cbreaker.go @@ -56,6 +56,9 @@ type CircuitBreaker struct { checkPeriod time.Duration lastCheck clock.Time + windowSize time.Duration + slideInterval time.Duration + fallback http.Handler next http.Handler @@ -89,7 +92,13 @@ func New(next http.Handler, expression string, options ...Option) (*CircuitBreak cb.condition = condition - mt, err := memmetrics.NewRTMetrics() + var mtOpts []memmetrics.RTOption + if cb.windowSize > 0 && cb.slideInterval > 0 { + mtOpts = []memmetrics.RTOption{ + memmetrics.WithRTSlidingWindow(cb.windowSize, cb.slideInterval), + } + } + mt, err := memmetrics.NewRTMetrics(mtOpts...) if err != nil { return nil, err } diff --git a/cbreaker/cbreaker_test.go b/cbreaker/cbreaker_test.go index 41d53b24..d10ac173 100644 --- a/cbreaker/cbreaker_test.go +++ b/cbreaker/cbreaker_test.go @@ -340,6 +340,22 @@ func TestCircuitBreaker_requestThreshold(t *testing.T) { assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode) } +func TestCircuitBreaker_SlidingWindowOption(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("hello")) + }) + + cb, err := New(handler, triggerNetRatio, + WithSlidingWindow(20*time.Second, 2*time.Second), + ) + require.NoError(t, err) + assert.NotNil(t, cb) + + cb2, err := New(handler, triggerNetRatio) + require.NoError(t, err) + assert.NotNil(t, cb2) +} + func statsOK() *memmetrics.RTMetrics { m, err := memmetrics.NewRTMetrics() if err != nil { diff --git a/cbreaker/options.go b/cbreaker/options.go index 5c395a99..c8f929f9 100644 --- a/cbreaker/options.go +++ b/cbreaker/options.go @@ -80,6 +80,17 @@ func Fallback(h http.Handler) Option { } } +// WithSlidingWindow sets shared sliding window config for both counter and latency histogram. +// size: total duration of statistics sliding window. +// interval: bucket rotation period, equals counter resolution and histogram period. +func WithSlidingWindow(size, interval time.Duration) Option { + return func(c *CircuitBreaker) error { + c.windowSize = size + c.slideInterval = interval + return nil + } +} + // ResponseFallbackOption represents an option you can pass to NewResponseFallback. type ResponseFallbackOption func(*ResponseFallback) error diff --git a/memmetrics/options.go b/memmetrics/options.go index 34ccf9c3..781a090c 100644 --- a/memmetrics/options.go +++ b/memmetrics/options.go @@ -1,5 +1,7 @@ package memmetrics +import "time" + // RTOption represents an option you can pass to NewRTMetrics. type RTOption func(r *RTMetrics) error @@ -19,5 +21,16 @@ func RTHistogram(fn NewRollingHistogramFn) RTOption { } } +// WithRTSlidingWindow sets shared sliding window config for RTMetrics counter and HDR histogram. +// window: total duration of sliding statistics window. +// interval: bucket rotation period, matches RollingCounter resolution and RollingHDRHistogram period. +func WithRTSlidingWindow(window, interval time.Duration) RTOption { + return func(m *RTMetrics) error { + m.windowSize = window + m.slideInterval = interval + return nil + } +} + // RatioOption represents an option you can pass to NewRatioCounter. type RatioOption func(r *RatioCounter) error diff --git a/memmetrics/roundtrip.go b/memmetrics/roundtrip.go index 5b7e0d09..2fb262f0 100644 --- a/memmetrics/roundtrip.go +++ b/memmetrics/roundtrip.go @@ -33,6 +33,12 @@ type RTMetrics struct { newCounter NewCounterFn newHist NewRollingHistogramFn + // windowSize total duration of rolling statistics window. + // Both windowSize and slideInterval must be non-zero to take effect; both zero uses metrics inner default values. + windowSize time.Duration + // slideInterval bucket rotation period shared by counter & HDR histogram. + // Both windowSize and slideInterval must be non-zero to take effect; both zero uses metrics inner default values. + slideInterval time.Duration } // NewRTMetrics returns new instance of metrics collector. @@ -48,14 +54,38 @@ func NewRTMetrics(settings ...RTOption) (*RTMetrics, error) { } if m.newCounter == nil { + buckets := counterBuckets + resolution := counterResolution + if m.windowSize > 0 && m.slideInterval > 0 { + buckets = int(m.windowSize / m.slideInterval) + if buckets < 1 { + buckets = 1 + } + resolution = m.slideInterval + if resolution < time.Second { + resolution = time.Second + } + } m.newCounter = func() (*RollingCounter, error) { - return NewCounter(counterBuckets, counterResolution) + return NewCounter(buckets, resolution) } } if m.newHist == nil { + buckets := histBuckets + resolution := histPeriod + if m.windowSize > 0 && m.slideInterval > 0 { + buckets = int(m.windowSize / m.slideInterval) + if buckets < 1 { + buckets = 1 + } + resolution = m.slideInterval + if resolution < time.Second { + resolution = time.Second + } + } m.newHist = func() (*RollingHDRHistogram, error) { - return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets) + return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, resolution, buckets) } } diff --git a/memmetrics/roundtrip_test.go b/memmetrics/roundtrip_test.go index c263c642..a32e2304 100644 --- a/memmetrics/roundtrip_test.go +++ b/memmetrics/roundtrip_test.go @@ -151,3 +151,21 @@ func TestRTMetric_Export_returnsNewCopy(t *testing.T) { } } } + +func TestNewRTMetrics_SlidingWindowOption(t *testing.T) { + testutils.FreezeTime(t) + + m1, err := NewRTMetrics() + require.NoError(t, err) + require.NotNil(t, m1) + + m1.Record(200, time.Second) + assert.EqualValues(t, 1, m1.TotalCount()) + + m2, err := NewRTMetrics(WithRTSlidingWindow(20*time.Second, 2*time.Second)) + require.NoError(t, err) + require.NotNil(t, m2) + + m2.Record(200, time.Second) + assert.EqualValues(t, 1, m2.TotalCount()) +}