Skip to content
Merged
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
36 changes: 36 additions & 0 deletions cbreaker/cbreaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,42 @@ func TestCircuitBreaker_sideEffects(t *testing.T) {
}
}

func TestCircuitBreaker_requestThreshold(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusGatewayTimeout)
})

testutils.FreezeTime(t)

cb, err := New(handler, triggerNetRatio+" && RequestThreshold() >= 20")
require.NoError(t, err)

srv := httptest.NewServer(cb)
t.Cleanup(srv.Close)

cb.metrics = statsResponseCodes(statusCode{Code: http.StatusGatewayTimeout, Count: 18})

clock.Advance(defaultCheckPeriod + clock.Millisecond)

re, _, err := testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusGatewayTimeout, re.StatusCode)
assert.Equal(t, cbState(stateStandby), cb.state)

cb.metrics = statsResponseCodes(statusCode{Code: http.StatusGatewayTimeout, Count: 19})

clock.Advance(defaultCheckPeriod + clock.Millisecond)

re, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusGatewayTimeout, re.StatusCode)
assert.Equal(t, cbState(stateTripped), cb.state)

re, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode)
}

func statsOK() *memmetrics.RTMetrics {
m, err := memmetrics.NewRTMetrics()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cbreaker/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func parseExpression(in string) (hpredicate, error) {
"LatencyAtQuantileMS": latencyAtQuantile,
"NetworkErrorRatio": networkErrorRatio,
"ResponseCodeRatio": responseCodeRatio,
"RequestThreshold": requestThreshold,
},
})
if err != nil {
Expand Down
8 changes: 7 additions & 1 deletion cbreaker/predicates_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"github.com/vulcand/oxy/v2/internal/holsterv4/clock"
)

type toType[T int | float64] func(c *CircuitBreaker) T
type toType[T int | int64 | float64] func(c *CircuitBreaker) T

func latencyAtQuantile(quantile float64) toType[int] {
return func(c *CircuitBreaker) int {
Expand All @@ -29,3 +29,9 @@ func responseCodeRatio(startA, endA, startB, endB int) toType[float64] {
return c.metrics.ResponseCodeRatio(startA, endA, startB, endB)
}
}

func requestThreshold() toType[int64] {
return func(c *CircuitBreaker) int64 {
return c.metrics.TotalCount()
}
}
45 changes: 45 additions & 0 deletions cbreaker/predicates_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func eq(m any, value any) (hpredicate, error) {
switch mapper := m.(type) {
case toType[int]:
return genericEQ(mapper, value)
case toType[int64]:
return int64EQ(mapper, value)
case toType[float64]:
return genericEQ(mapper, value)
}
Expand All @@ -64,6 +66,8 @@ func lt(m any, value any) (hpredicate, error) {
switch mapper := m.(type) {
case toType[int]:
return genericLT(mapper, value)
case toType[int64]:
return int64LT(mapper, value)
case toType[float64]:
return genericLT(mapper, value)
}
Expand Down Expand Up @@ -93,6 +97,8 @@ func gt(m any, value any) (hpredicate, error) {
switch mapper := m.(type) {
case toType[int]:
return genericGT(mapper, value)
case toType[int64]:
return int64GT(mapper, value)
case toType[float64]:
return genericGT(mapper, value)
}
Expand Down Expand Up @@ -149,3 +155,42 @@ func genericGT[T int | float64](m toType[T], val any) (hpredicate, error) {
return m(c) > value
}, nil
}

func int64EQ(m toType[int64], val any) (hpredicate, error) {
// Use `int` instead of `int64` because `vulcand/predicate` only use `int`.
// Note: using `int64` should be the default type of any integer, but changing this will break `vulcand/predicate`.
value, ok := val.(int)
if !ok {
return nil, fmt.Errorf("expected int, got %T", val)
}

return func(c *CircuitBreaker) bool {
return m(c) == int64(value)
}, nil
}

func int64LT(m toType[int64], val any) (hpredicate, error) {
// Use `int` instead of `int64` because `vulcand/predicate` only use `int`.
// Note: using `int64` should be the default type of any integer, but changing this will break `vulcand/predicate`.
value, ok := val.(int)
if !ok {
return nil, fmt.Errorf("expected int, got %T", val)
}

return func(c *CircuitBreaker) bool {
return m(c) < int64(value)
}, nil
}

func int64GT(m toType[int64], val any) (hpredicate, error) {
// Use `int` instead of `int64` because `vulcand/predicate` only use `int`.
// Note: using `int64` should be the default type of any integer, but changing this will break `vulcand/predicate`.
value, ok := val.(int)
if !ok {
return nil, fmt.Errorf("expected int, got %T", val)
}

return func(c *CircuitBreaker) bool {
return m(c) > int64(value)
}, nil
}
Loading