Skip to content

Commit e3be2e8

Browse files
add lumping_interval feature in Semian Circuit Breaker
1 parent f82188e commit e3be2e8

File tree

2 files changed

+119
-2
lines changed

2 files changed

+119
-2
lines changed

lib/semian/circuit_breaker.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class CircuitBreaker # :nodoc:
1717

1818
def initialize(name, exceptions:, success_threshold:, error_threshold:,
1919
error_timeout:, implementation:, half_open_resource_timeout: nil,
20-
error_threshold_timeout: nil, error_threshold_timeout_enabled: true)
20+
error_threshold_timeout: nil, error_threshold_timeout_enabled: true,
21+
lumping_interval: 0)
2122
@name = name.to_sym
2223
@success_count_threshold = success_threshold
2324
@error_count_threshold = error_threshold
@@ -26,6 +27,11 @@ def initialize(name, exceptions:, success_threshold:, error_threshold:,
2627
@error_timeout = error_timeout
2728
@exceptions = exceptions
2829
@half_open_resource_timeout = half_open_resource_timeout
30+
@lumping_interval = lumping_interval
31+
32+
if @lumping_interval >= @error_threshold_timeout
33+
raise ArgumentError, "lumping_interval (#{@lumping_interval}) must be less than error_threshold_timeout (#{@error_threshold_timeout})"
34+
end
2935

3036
@errors = implementation::SlidingWindow.new(max_size: @error_count_threshold)
3137
@successes = implementation::Integer.new
@@ -137,11 +143,14 @@ def push_error(error)
137143

138144
def push_time
139145
time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
146+
140147
if error_threshold_timeout_enabled
141148
@errors.reject! { |err_time| err_time + @error_threshold_timeout < time }
142149
end
143150

144-
@errors << time
151+
if @errors[-1] < time - @lumping_interval
152+
@errors << time
153+
end
145154
end
146155

147156
def log_state_transition(new_state)

test/circuit_breaker_test.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,112 @@ def test_notify_state_transition
600600
ensure
601601
Semian.unsubscribe(:test_notify_state_transition)
602602
end
603+
604+
def test_lumping_interval_prevents_rapid_error_accumulation
605+
resource = Semian.register(
606+
:lumping_test,
607+
bulkhead: false,
608+
exceptions: [SomeError],
609+
error_threshold: 3,
610+
error_timeout: 10,
611+
success_threshold: 1,
612+
lumping_interval: 2,
613+
)
614+
615+
# Trigger errors within lumping interval
616+
6.times do
617+
trigger_error!(resource)
618+
end
619+
620+
# Should not open circuit since errors are lumped
621+
assert_circuit_closed(resource)
622+
623+
time_travel(3) do
624+
6.times do
625+
trigger_error!(resource)
626+
end
627+
628+
assert_circuit_closed(resource)
629+
630+
time_travel(3) do
631+
6.times do
632+
trigger_error!(resource)
633+
end
634+
635+
assert_circuit_opened(resource)
636+
end
637+
end
638+
ensure
639+
Semian.destroy(:lumping_test)
640+
end
641+
642+
def test_lumping_interval_respects_error_threshold
643+
resource = Semian.register(
644+
:lumping_threshold_test,
645+
bulkhead: false,
646+
exceptions: [SomeError],
647+
error_threshold: 2,
648+
error_timeout: 5,
649+
success_threshold: 1,
650+
lumping_interval: 1,
651+
)
652+
653+
# First error
654+
trigger_error!(resource)
655+
656+
assert_circuit_closed(resource)
657+
658+
# Wait past lumping interval
659+
time_travel(2) do
660+
# Second error should open circuit
661+
trigger_error!(resource)
662+
663+
assert_circuit_opened(resource)
664+
end
665+
ensure
666+
Semian.destroy(:lumping_threshold_test)
667+
end
668+
669+
def test_lumping_interval_with_zero_value
670+
resource = Semian.register(
671+
:lumping_zero_test,
672+
bulkhead: false,
673+
exceptions: [SomeError],
674+
error_threshold: 2,
675+
error_timeout: 5,
676+
success_threshold: 1,
677+
lumping_interval: 0,
678+
)
679+
680+
# First error
681+
trigger_error!(resource)
682+
683+
assert_circuit_closed(resource)
684+
685+
# Second error should open circuit immediately
686+
trigger_error!(resource)
687+
688+
assert_circuit_opened(resource)
689+
ensure
690+
Semian.destroy(:lumping_zero_test)
691+
end
692+
693+
def test_lumping_interval_cannot_be_greater_than_error_threshold_timeout
694+
error = assert_raises(ArgumentError) do
695+
Semian.register(
696+
:lumping_validation_test,
697+
bulkhead: false,
698+
exceptions: [SomeError],
699+
error_threshold: 2,
700+
error_timeout: 5,
701+
error_threshold_timeout: 3,
702+
success_threshold: 1,
703+
lumping_interval: 4,
704+
)
705+
end
706+
707+
assert_match(/lumping_interval \(4\) must be less than error_threshold_timeout \(3\)/, error.message)
708+
ensure
709+
Semian.destroy(:lumping_validation_test)
710+
end
603711
end

0 commit comments

Comments
 (0)