1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| # Logic based on https://martinfowler.com/bliki/CircuitBreaker.html
class CircuitBreaker < ApplicationRecord
ERROR_THRESHOLD = 5
RESET_TIMEOUT = 5.minutes
# name, error_count, last_error_at
def state
if error_count < ERROR_THRESHOLD
:ok
elsif last_error_at < RESET_TIMEOUT.ago
:recoverable
else
:error
end
end
def call(primary, alternate, recoverable_errors = [StandardError])
if state == :error
alternate.call
else # :ok, :recoverable
begin
primary.call.tap { record_success }
rescue *recoverable_errors => exception
Sentry.capture_exception(exception)
record_failure
alternate.call
end
end
end
private
def record_success
update(error_count: 0) if error_count > 0
end
def record_failure
update(error_count: error_count + 1, last_error_at: Time.current)
end
end
|