workarea-circuit_breaker 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/ci.yml +66 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +2 -0
- data/.stylelintrc.json +8 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile +12 -0
- data/LICENSE +52 -0
- data/README.md +103 -0
- data/Rakefile +59 -0
- data/app/assets/stylesheets/circuit_breaker/.keep +0 -0
- data/app/controllers/workarea/admin/circuits_controller.rb +26 -0
- data/app/view_models/workarea/admin/circuit_view_model.rb +33 -0
- data/app/view_models/workarea/admin/circuits_view_model.rb +41 -0
- data/app/views/workarea/admin/circuits/index.html.haml +69 -0
- data/app/views/workarea/admin/shared/_circuit_breaker_link.html.haml +1 -0
- data/bin/rails +25 -0
- data/config/initializers/appends.rb +4 -0
- data/config/initializers/workarea.rb +11 -0
- data/config/locales/en.yml +29 -0
- data/config/routes.rb +10 -0
- data/lib/workarea/circuit_breaker.rb +96 -0
- data/lib/workarea/circuit_breaker/circuit.rb +137 -0
- data/lib/workarea/circuit_breaker/engine.rb +12 -0
- data/lib/workarea/circuit_breaker/failure_message.rb +51 -0
- data/lib/workarea/circuit_breaker/version.rb +5 -0
- data/script/admin_ci +5 -0
- data/script/ci +8 -0
- data/script/core_ci +5 -0
- data/script/plugins_ci +5 -0
- data/script/storefront_ci +5 -0
- data/test/dummy/.ruby-version +1 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +28 -0
- data/test/dummy/bin/update +28 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +33 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +52 -0
- data/test/dummy/config/environments/production.rb +83 -0
- data/test/dummy/config/environments/test.rb +45 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +25 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/workarea.rb +5 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +34 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +33 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/db/seeds.rb +2 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/integration/workarea/admin/circuits_integration_test.rb +27 -0
- data/test/lib/workarea/circuit_breaker/circuit_test.rb +135 -0
- data/test/lib/workarea/circuit_breaker/failure_message_test.rb +41 -0
- data/test/lib/workarea/circuit_breaker_test.rb +42 -0
- data/test/support/circuit_breaker_support.rb +16 -0
- data/test/teaspoon_env.rb +6 -0
- data/test/test_helper.rb +15 -0
- data/workarea-circuit_breaker.gemspec +17 -0
- metadata +151 -0
File without changes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Admin
|
3
|
+
class CircuitsController < Admin::ApplicationController
|
4
|
+
def index
|
5
|
+
@circuits = Admin::CircuitsViewModel.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def disable
|
9
|
+
circuit = CircuitViewModel.new(CircuitBreaker[params[:id]])
|
10
|
+
circuit.add_failure(message: t('workarea.admin.circuit_breaker.manually_broke_circuit', user: current_user.name))
|
11
|
+
circuit.break!
|
12
|
+
|
13
|
+
flash[:success] = t('workarea.admin.circuit_breaker.flash_messages.turned_off', circuit: circuit.name, break_for: circuit.break_for.inspect)
|
14
|
+
redirect_to circuits_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def enable
|
18
|
+
circuit = CircuitViewModel.new(CircuitBreaker[params[:id]])
|
19
|
+
circuit.set_healthy!
|
20
|
+
|
21
|
+
flash[:success] = t('workarea.admin.circuit_breaker.flash_messages.turned_on', circuit: circuit.name)
|
22
|
+
redirect_to circuits_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Admin
|
3
|
+
class CircuitViewModel < ApplicationViewModel
|
4
|
+
def name
|
5
|
+
model.name.to_s.humanize
|
6
|
+
end
|
7
|
+
|
8
|
+
def slug
|
9
|
+
model.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def current_fails
|
13
|
+
timeline.length
|
14
|
+
end
|
15
|
+
|
16
|
+
def broken_until
|
17
|
+
return unless options[:broken_until].present?
|
18
|
+
|
19
|
+
Time.at(options[:broken_until].to_i)
|
20
|
+
end
|
21
|
+
|
22
|
+
def healthy?
|
23
|
+
options[:broken_until].blank?
|
24
|
+
end
|
25
|
+
|
26
|
+
def timeline
|
27
|
+
@timeline ||= options.fetch(:timeline, []).map do |(failure_string, timestamp)|
|
28
|
+
CircuitBreaker::FailureMessage.from_string(failure_string, Time.at(timestamp))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Admin
|
3
|
+
class CircuitsViewModel < ApplicationViewModel
|
4
|
+
delegate :each, to: :all
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def all
|
9
|
+
@all ||= circuits.zip(circuits_broken_until, circuits_timeline).map do |circuit, broken_until, timeline|
|
10
|
+
CircuitViewModel.new(
|
11
|
+
circuit,
|
12
|
+
broken_until: broken_until,
|
13
|
+
timeline: timeline
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def circuits
|
19
|
+
@circuits ||= CircuitBreaker.config.circuits.map do |name, options|
|
20
|
+
CircuitBreaker::Circuit.new(name, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def circuits_timeline
|
25
|
+
@circuits_timeline ||= CircuitBreaker.redis.with do |redis|
|
26
|
+
redis.pipelined do
|
27
|
+
circuits.each do |circuit|
|
28
|
+
redis.zrange(circuit.redis_failure_set_key, 0, -1, with_scores: true)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def circuits_broken_until
|
35
|
+
@circuits_broken_until ||= CircuitBreaker.redis.with do |redis|
|
36
|
+
redis.mget(circuits.map(&:redis_broken_key))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
.view
|
2
|
+
.view__header
|
3
|
+
.view__heading
|
4
|
+
= link_to "↑ #{t('workarea.admin.dashboards.main_dashboard')}", root_path
|
5
|
+
%h1= t('workarea.admin.circuit_breaker.title')
|
6
|
+
|
7
|
+
.view__container
|
8
|
+
%p= t('workarea.admin.circuit_breaker.help_text')
|
9
|
+
|
10
|
+
- if @circuits.each.present?
|
11
|
+
%table
|
12
|
+
%thead
|
13
|
+
%tr
|
14
|
+
%th= t('workarea.admin.circuit_breaker.circuit')
|
15
|
+
%th
|
16
|
+
= t('workarea.admin.circuit_breaker.max_fails')
|
17
|
+
= link_to '#circuit-max-fails', data: { tooltip: '' } do
|
18
|
+
= inline_svg('workarea/admin/icons/help.svg', class: 'svg-icon svg-icon--small svg-icon--blue', title: t('workarea.admin.circuit_breaker.max_fails'))
|
19
|
+
|
20
|
+
#circuit-max-fails.tooltip-content
|
21
|
+
%p= t('workarea.admin.circuit_breaker.help.max_fails')
|
22
|
+
%th
|
23
|
+
= t('workarea.admin.circuit_breaker.window')
|
24
|
+
= link_to '#circuit-window', data: { tooltip: '' } do
|
25
|
+
= inline_svg('workarea/admin/icons/help.svg', class: 'svg-icon svg-icon--small svg-icon--blue', title: t('workarea.admin.circuit_breaker.window'))
|
26
|
+
#circuit-window.tooltip-content
|
27
|
+
%p= t('workarea.admin.circuit_breaker.help.window')
|
28
|
+
%th
|
29
|
+
= t('workarea.admin.circuit_breaker.break_for')
|
30
|
+
= link_to '#circuit-break-for', data: { tooltip: '' } do
|
31
|
+
= inline_svg('workarea/admin/icons/help.svg', class: 'svg-icon svg-icon--small svg-icon--blue', title: t('workarea.admin.circuit_breaker.break_for'))
|
32
|
+
#circuit-break-for.tooltip-content
|
33
|
+
%p= t('workarea.admin.circuit_breaker.help.break_for')
|
34
|
+
%th= t('workarea.admin.circuit_breaker.current_fails')
|
35
|
+
%th= t('workarea.admin.circuit_breaker.timeline')
|
36
|
+
%th= t('workarea.admin.circuit_breaker.broken_until')
|
37
|
+
%th
|
38
|
+
%tbody
|
39
|
+
- @circuits.each do |circuit|
|
40
|
+
%tr
|
41
|
+
%td= circuit.name
|
42
|
+
%td= circuit.max_fails
|
43
|
+
%td= circuit.window.inspect
|
44
|
+
%td= circuit.break_for.inspect
|
45
|
+
%td= circuit.current_fails
|
46
|
+
%td
|
47
|
+
- if circuit.timeline.present?
|
48
|
+
%ul.list-reset
|
49
|
+
- circuit.timeline.each do |failure_message|
|
50
|
+
%li
|
51
|
+
= failure_message.timestamp.to_s(:time_only)
|
52
|
+
- if failure_message.event_id.present?
|
53
|
+
%strong= t('workarea.admin.circuit_breaker.sentry')
|
54
|
+
= failure_message.event_id
|
55
|
+
%strong= t('workarea.admin.circuit_breaker.message')
|
56
|
+
= failure_message.message
|
57
|
+
%td
|
58
|
+
- if circuit.broken_until.present?
|
59
|
+
= circuit.broken_until.to_s(:time_only)
|
60
|
+
- if circuit.healthy?
|
61
|
+
%td
|
62
|
+
= inline_svg('workarea/admin/valid.svg', class: 'svg-icon svg-icon--green')
|
63
|
+
= link_to t('workarea.admin.circuit_breaker.break'), disable_circuit_path(circuit.slug), data: { method: 'post' }
|
64
|
+
- else
|
65
|
+
%td
|
66
|
+
= inline_svg('workarea/admin/invalid.svg', class: 'svg-icon svg-icon--red')
|
67
|
+
= link_to t('workarea.admin.circuit_breaker.enable'), enable_circuit_path(circuit.slug), data: { method: 'post' }
|
68
|
+
- else
|
69
|
+
%p= t('workarea.admin.circuit_breaker.no_circuits')
|
@@ -0,0 +1 @@
|
|
1
|
+
%li{ class: "primary-nav__item" }= link_to t('workarea.admin.shared.primary_nav.circuit_breaker_title'), circuits_path, class: navigation_link_classes(circuits_path)
|
data/bin/rails
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
3
|
+
# installed from the root of your application.
|
4
|
+
|
5
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
6
|
+
ENGINE_PATH = File.expand_path('../lib/workarea/circuit_breaker/engine', __dir__)
|
7
|
+
APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
|
8
|
+
|
9
|
+
# Set up gems listed in the Gemfile.
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
11
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
12
|
+
|
13
|
+
require "rails"
|
14
|
+
# Pick the frameworks you want:
|
15
|
+
require "active_model/railtie"
|
16
|
+
require "active_job/railtie"
|
17
|
+
# require "active_record/railtie"
|
18
|
+
# require "active_storage/engine"
|
19
|
+
require "action_controller/railtie"
|
20
|
+
require "action_mailer/railtie"
|
21
|
+
require "action_view/railtie"
|
22
|
+
# require "action_cable/engine"
|
23
|
+
require "sprockets/railtie"
|
24
|
+
require "rails/test_unit/railtie"
|
25
|
+
require 'rails/engine/commands'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Workarea.configure do |config|
|
2
|
+
config.circuit_breaker = ActiveSupport::Configurable::Configuration.new
|
3
|
+
|
4
|
+
config.circuit_breaker.circuits ||= {}
|
5
|
+
config.circuit_breaker.redis ||= {}
|
6
|
+
config.circuit_breaker.circuit_defaults ||= {
|
7
|
+
max_fails: 3,
|
8
|
+
window: 5.minutes,
|
9
|
+
break_for: 5.minutes
|
10
|
+
}
|
11
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
en:
|
2
|
+
workarea:
|
3
|
+
admin:
|
4
|
+
shared:
|
5
|
+
primary_nav:
|
6
|
+
circuit_breaker_title: Circuit Breakers
|
7
|
+
circuit_breaker:
|
8
|
+
flash_messages:
|
9
|
+
turned_on: "%{circuit} has been enabled"
|
10
|
+
turned_off: "%{circuit} has been disabled for %{break_for}"
|
11
|
+
break: Break
|
12
|
+
break_for: Break For
|
13
|
+
broken_until: Broken Until
|
14
|
+
circuit: Circuit
|
15
|
+
current_fails: Current Fails
|
16
|
+
enable: Enable
|
17
|
+
max_fails: Max Fails
|
18
|
+
message: Message
|
19
|
+
sentry: Sentry
|
20
|
+
timeline: Timeline
|
21
|
+
title: Circuit Breakers
|
22
|
+
window: Window
|
23
|
+
manually_broke_circuit: "%{user} manually broke circuit"
|
24
|
+
no_circuits: There are currently no circuits defined.
|
25
|
+
help_text: Circuits are developer configured code blocks that allow for processes to execute a defined fallback when a service fails within a given set of parameters.
|
26
|
+
help:
|
27
|
+
max_fails: Maximum times this circuit can fail before shutting off.
|
28
|
+
window: Timeframe in which a Max fails would trigger a circuit to shut off. Eg. five failures (max fails) in one minute (window) will trigger the circuit to shut off.
|
29
|
+
break_for: How long to turn the circuit off for when the max failures limit is reached within the window duration.
|
data/config/routes.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'connection_pool'
|
2
|
+
|
3
|
+
require 'workarea'
|
4
|
+
require 'workarea/storefront'
|
5
|
+
require 'workarea/admin'
|
6
|
+
|
7
|
+
require 'workarea/circuit_breaker/engine'
|
8
|
+
require 'workarea/circuit_breaker/version'
|
9
|
+
require 'workarea/circuit_breaker/circuit'
|
10
|
+
require 'workarea/circuit_breaker/failure_message'
|
11
|
+
|
12
|
+
module Workarea
|
13
|
+
module CircuitBreaker
|
14
|
+
class CircuitBreakerError < RuntimeError; end
|
15
|
+
module ConnectionPoolLike
|
16
|
+
def with
|
17
|
+
yield self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
unless ::Redis.instance_methods.include?(:with)
|
22
|
+
::Redis.include(ConnectionPoolLike)
|
23
|
+
end
|
24
|
+
|
25
|
+
CIRCUIT_KEY_BASE = "workarea_circuits"
|
26
|
+
|
27
|
+
def self.config
|
28
|
+
Workarea.config.circuit_breaker
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.circuit_defaults
|
32
|
+
config.circuit_defaults
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.add_service(name, **options)
|
36
|
+
options = options.symbolize_keys
|
37
|
+
overwrite = options.delete(:overwrite) || false
|
38
|
+
|
39
|
+
if !overwrite && config.circuits.has_key?(name)
|
40
|
+
Rails.logger.warn "Overwriting existing service #{name} in CircuitBreaker"
|
41
|
+
end
|
42
|
+
|
43
|
+
config.circuits[name.to_s] = options
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.[](circuit_name)
|
47
|
+
circuit_name = circuit_name.to_s
|
48
|
+
options = config.circuits[circuit_name] || {}
|
49
|
+
Circuit.new(circuit_name, **options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.redis
|
53
|
+
@redis ||= if pool_options.present?
|
54
|
+
ConnectionPool.new(pool_options) do
|
55
|
+
Redis.new(url: Workarea::Configuration::Redis.persistent.to_url)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
Redis.new(url: Workarea::Configuration::Redis.persistent.to_url)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Capture errors thrown by code that is running in the circuit
|
63
|
+
# breaker. When `Sentry::Raven` is installed, send the error over to
|
64
|
+
# Sentry and return the event ID. Otherwise, just return `nil`. This
|
65
|
+
# will also return `nil` when Sentry is not configured to capture
|
66
|
+
# exceptions in the environment, such as when tests are running.
|
67
|
+
#
|
68
|
+
# @param [Exception] exception - Error thrown by the application
|
69
|
+
# @return [String] Event ID or `nil` when the exception was not
|
70
|
+
# captured by Sentry.
|
71
|
+
def self.capture_exception(exception)
|
72
|
+
event = if defined?(::Raven)
|
73
|
+
Raven.capture_exception(exception) || nil
|
74
|
+
else
|
75
|
+
Rails.logger.warn exception
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
return if event.blank?
|
80
|
+
|
81
|
+
event.id
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.freeze_config!
|
85
|
+
Workarea.config.circuit_breaker.freeze
|
86
|
+
Workarea.config.circuit_breaker.circuits.freeze
|
87
|
+
Workarea.config.circuit_breaker.circuit_defaults.freeze
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def self.pool_options
|
93
|
+
config.redis.fetch(:pool, {})
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Workarea
|
2
|
+
module CircuitBreaker
|
3
|
+
class Circuit
|
4
|
+
attr_reader :name, :options
|
5
|
+
|
6
|
+
def initialize(name, **options)
|
7
|
+
@name = name.to_s
|
8
|
+
@options = CircuitBreaker.circuit_defaults.merge(options).symbolize_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
# Checks if this circuit is currently, defaults to true
|
12
|
+
#
|
13
|
+
# @return [Boolean]
|
14
|
+
def healthy?
|
15
|
+
broken = CircuitBreaker.redis.with do |connection|
|
16
|
+
connection.get(redis_broken_key)
|
17
|
+
end
|
18
|
+
|
19
|
+
broken.nil?
|
20
|
+
rescue Timeout::Error => error
|
21
|
+
CircuitBreaker.capture_exception(error)
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set the circuit to be healthy
|
26
|
+
#
|
27
|
+
# @return [nil]
|
28
|
+
def set_healthy!
|
29
|
+
CircuitBreaker.redis.with do |connection|
|
30
|
+
connection.pipelined do
|
31
|
+
connection.del(redis_broken_key)
|
32
|
+
connection.del(redis_failure_set_key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
rescue Timeout::Error => error
|
37
|
+
CircuitBreaker.capture_exception(error)
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Break this circuit for the configured break_for duration of this service
|
42
|
+
#
|
43
|
+
# @return nil
|
44
|
+
def break!
|
45
|
+
CircuitBreaker.redis.with do |connection|
|
46
|
+
connection.setex(redis_broken_key, break_for, break_for.from_now.to_i)
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
rescue Timeout::Error => error
|
50
|
+
CircuitBreaker.capture_exception(error)
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Wraps the black around a circuit. If the circuit is healthy the block
|
55
|
+
# is executed and it's return value is returned. If the black raises an error
|
56
|
+
# an event is added to the circuit timeline. If a fallback is provided the
|
57
|
+
# fallback is invoked otherwise an error is raised
|
58
|
+
#
|
59
|
+
# @param [Symbol, Object] either a symbol of a method defined on the blocks binding receiver or anything that responds to call
|
60
|
+
def wrap(fallback: nil, &block)
|
61
|
+
if healthy?
|
62
|
+
begin
|
63
|
+
block.call
|
64
|
+
rescue => error
|
65
|
+
event_id = CircuitBreaker.capture_exception(error)
|
66
|
+
add_failure(event_id: event_id, message: "#{error.class}: #{error.message}")
|
67
|
+
fallback_or_error(block.binding.receiver, fallback: fallback, error: error)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
fallback_or_error(block.binding.receiver, fallback: fallback, error: CircuitBreakerError)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def window
|
75
|
+
options[:window]
|
76
|
+
end
|
77
|
+
|
78
|
+
def break_for
|
79
|
+
options[:break_for]
|
80
|
+
end
|
81
|
+
|
82
|
+
def max_fails
|
83
|
+
options[:max_fails]
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param [String] event_id Raven::Event#id
|
87
|
+
# @param [String] message - normally an error message
|
88
|
+
#
|
89
|
+
# @return [Int] number of events in the timeline
|
90
|
+
def add_failure(message:, event_id: nil)
|
91
|
+
CircuitBreaker.redis.with do |connection|
|
92
|
+
failure_message = FailureMessage.new(event_id: event_id, message: message)
|
93
|
+
|
94
|
+
_deleted, _added, length, _expiry_set = connection.pipelined do
|
95
|
+
connection.zremrangebyscore(redis_failure_set_key, 0, break_for.ago.to_i)
|
96
|
+
connection.zadd(redis_failure_set_key, Time.current.to_i, failure_message.to_s)
|
97
|
+
connection.zcard(redis_failure_set_key)
|
98
|
+
# expire as the time a circuit could be broken so info is avaiable whole length
|
99
|
+
# of broken time
|
100
|
+
connection.expire(redis_failure_set_key, break_for.to_i)
|
101
|
+
end
|
102
|
+
|
103
|
+
break! if length >= max_fails
|
104
|
+
|
105
|
+
length
|
106
|
+
end
|
107
|
+
rescue Timeout::Error => error
|
108
|
+
CircuitBreaker.capture_exception(error)
|
109
|
+
0
|
110
|
+
end
|
111
|
+
|
112
|
+
def redis_broken_key
|
113
|
+
"#{redis_key_base}:broken"
|
114
|
+
end
|
115
|
+
|
116
|
+
def redis_failure_set_key
|
117
|
+
"#{redis_key_base}:failures"
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def fallback_or_error(receiver, fallback: nil, error: nil)
|
123
|
+
if fallback.blank?
|
124
|
+
raise error
|
125
|
+
elsif fallback.respond_to?(:call)
|
126
|
+
fallback.call
|
127
|
+
elsif receiver.respond_to?(fallback, true)
|
128
|
+
receiver.send(fallback)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def redis_key_base
|
133
|
+
"#{CircuitBreaker::CIRCUIT_KEY_BASE}:#{name.to_s.systemize}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|