Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cbreaker/cbreaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down
16 changes: 16 additions & 0 deletions cbreaker/cbreaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions cbreaker/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions memmetrics/options.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package memmetrics

import "time"

// RTOption represents an option you can pass to NewRTMetrics.
type RTOption func(r *RTMetrics) error

Expand All @@ -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
34 changes: 32 additions & 2 deletions memmetrics/roundtrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
}

Expand Down
18 changes: 18 additions & 0 deletions memmetrics/roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}