faulty 0.8.0 → 0.8.5
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 +4 -4
- data/.github/workflows/ci.yml +8 -1
- data/.yardopts +1 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +2 -0
- data/README.md +42 -6
- data/bin/benchmark +30 -7
- data/lib/faulty/circuit.rb +63 -12
- data/lib/faulty/deprecation.rb +37 -0
- data/lib/faulty/error.rb +7 -3
- data/lib/faulty/patch/elasticsearch.rb +95 -0
- data/lib/faulty/patch/mysql2.rb +9 -2
- data/lib/faulty/patch/redis.rb +9 -2
- data/lib/faulty/patch.rb +6 -5
- data/lib/faulty/storage/auto_wire.rb +3 -3
- data/lib/faulty/storage/fallback_chain.rb +2 -2
- data/lib/faulty/storage/fault_tolerant_proxy.rb +3 -3
- data/lib/faulty/storage/interface.rb +5 -4
- data/lib/faulty/storage/memory.rb +3 -2
- data/lib/faulty/storage/null.rb +12 -6
- data/lib/faulty/storage/redis.rb +50 -31
- data/lib/faulty/version.rb +1 -1
- data/lib/faulty.rb +4 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54ef7d6bc1b23815e02563c80e4c17fef2735812a8c424882e8283996506edcc
|
4
|
+
data.tar.gz: c4c2d074b118f3077a3cddb862fddc5c3ace18c62a206e4523cfb830b7b6e35b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75fa840ff865e39e381894bdce59ab13f67ac001cbae57696360692c495b8d0ca2f643261a7a4ea083fdb1964d8e14ad0237815f90e9b436d17eff04a0abe0f5
|
7
|
+
data.tar.gz: dfcaede2d29e34bd466a21ad5037ddaeebfaa947bb44f09ef9c64b34ac0f207dd2a8e4d038067b47ce3ac76ccbba744721dc1fb4337854e7e9af0df739798169
|
data/.github/workflows/ci.yml
CHANGED
@@ -22,6 +22,11 @@ jobs:
|
|
22
22
|
image: redis
|
23
23
|
ports:
|
24
24
|
- 6379:6379
|
25
|
+
elasticsearch:
|
26
|
+
image: elasticsearch:7.13.4
|
27
|
+
ports:
|
28
|
+
- 9200:9200
|
29
|
+
options: -e="discovery.type=single-node" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=3s --health-timeout=5s --health-retries=20
|
25
30
|
steps:
|
26
31
|
- uses: actions/checkout@v2
|
27
32
|
- uses: ruby/setup-ruby@v1
|
@@ -32,6 +37,9 @@ jobs:
|
|
32
37
|
bundler-cache: true
|
33
38
|
- run: bundle exec rubocop
|
34
39
|
if: matrix.ruby == '2.7'
|
40
|
+
- run: bin/yardoc --fail-on-warning
|
41
|
+
if: matrix.ruby == '2.7'
|
42
|
+
- run: bin/check-version
|
35
43
|
- name: start MySQL
|
36
44
|
run: sudo /etc/init.d/mysql start
|
37
45
|
- run: bundle exec rspec --format doc
|
@@ -43,7 +51,6 @@ jobs:
|
|
43
51
|
with:
|
44
52
|
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
45
53
|
coverage-reports: coverage/lcov/faulty.lcov
|
46
|
-
- run: bin/check-version
|
47
54
|
|
48
55
|
release:
|
49
56
|
needs: test
|
data/.yardopts
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## Release v0.8.5
|
2
|
+
|
3
|
+
* Fix yard warnings #49 justinhoward
|
4
|
+
* Fix crash in Redis storage backend if opened_at was missing #46 justinhoward
|
5
|
+
* Add granular errors for Elasticsearch patch #48 justinhoward
|
6
|
+
* Return status conditionally for Storage::Interface#entry #45 justinhoward
|
7
|
+
|
8
|
+
## Release v0.8.4
|
9
|
+
|
10
|
+
* Add Elasticsearch client patch #44 justinhoward
|
11
|
+
|
12
|
+
## Release v0.8.2
|
13
|
+
|
14
|
+
* Fix crash for older versions of concurrent-ruby #42 justinhoward
|
15
|
+
|
16
|
+
## Release v0.8.1
|
17
|
+
|
18
|
+
* Add cause message to CircuitTrippedError #40 justinhoward
|
19
|
+
* Record failures for cache hits #41 justinhoward
|
20
|
+
|
21
|
+
|
1
22
|
## Release v0.8.0
|
2
23
|
|
3
24
|
* Store circuit options in the backend when run #34 justinhoward
|
data/Gemfile
CHANGED
@@ -13,6 +13,8 @@ not_jruby = %i[ruby mingw x64_mingw].freeze
|
|
13
13
|
gem 'activesupport', '>= 4.2'
|
14
14
|
gem 'bundler', '>= 1.17', '< 3'
|
15
15
|
gem 'byebug', platforms: not_jruby
|
16
|
+
# Open source licensed elasticsearch
|
17
|
+
gem 'elasticsearch', '> 7', '< 7.14'
|
16
18
|
gem 'honeybadger', '>= 2.0'
|
17
19
|
gem 'irb', '~> 1.0'
|
18
20
|
# Minimum of 0.5.0 for specific error classes
|
data/README.md
CHANGED
@@ -84,6 +84,7 @@ Also see "Release It!: Design and Deploy Production-Ready Software" by
|
|
84
84
|
* [Patches](#patches)
|
85
85
|
+ [Patch::Redis](#patchredis)
|
86
86
|
+ [Patch::Mysql2](#patchmysql2)
|
87
|
+
+ [Patch::Elasticsearch](#patchelasticsearch)
|
87
88
|
* [Event Handling](#event-handling)
|
88
89
|
+ [CallbackListener](#callbacklistener)
|
89
90
|
+ [Other Built-in Listeners](#other-built-in-listeners)
|
@@ -1046,6 +1047,38 @@ mysql = Mysql2::Client.new(host: '127.0.0.1')
|
|
1046
1047
|
mysql.query('SELECT * FROM users') # not protected by a circuit
|
1047
1048
|
```
|
1048
1049
|
|
1050
|
+
### Patch::Elasticsearch
|
1051
|
+
|
1052
|
+
[`Faulty::Patch::Elasticsearch`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Elasticsearch)
|
1053
|
+
protects a `Elasticsearch::Client` with an internal circuit. Pass a `:faulty` key along
|
1054
|
+
with your client options to enable the circuit breaker.
|
1055
|
+
|
1056
|
+
```ruby
|
1057
|
+
require 'faulty/patch/elasticsearch'
|
1058
|
+
|
1059
|
+
es = Elasticsearch::Client.new(url: 'localhost:9200', faulty: {
|
1060
|
+
# The name for the Elasticsearch::Client circuit
|
1061
|
+
name: 'elasticsearch'
|
1062
|
+
|
1063
|
+
# The faulty instance to use
|
1064
|
+
# This can also be a registered faulty instance or a constant name. See API
|
1065
|
+
# docs for more details
|
1066
|
+
instance: Faulty.default
|
1067
|
+
|
1068
|
+
# By default, circuit errors will be subclasses of
|
1069
|
+
# Elasticsearch::Transport::Transport::Error
|
1070
|
+
# To disable this behavior, set patch_errors to false and Faulty
|
1071
|
+
# will raise its default errors
|
1072
|
+
patch_errors: true
|
1073
|
+
})
|
1074
|
+
```
|
1075
|
+
|
1076
|
+
If you're using Searchkick, you can configure Faulty with `client_options`.
|
1077
|
+
|
1078
|
+
```ruby
|
1079
|
+
Searchkick.client_options[:faulty] = { name: 'searchkick' }
|
1080
|
+
```
|
1081
|
+
|
1049
1082
|
## Event Handling
|
1050
1083
|
|
1051
1084
|
Faulty uses an event-dispatching model to deliver notifications of internal
|
@@ -1288,18 +1321,20 @@ but there are and have been many other options:
|
|
1288
1321
|
### Currently Active
|
1289
1322
|
|
1290
1323
|
- [semian](https://github.com/Shopify/semian): A resiliency toolkit that
|
1291
|
-
includes circuit breakers. It
|
1292
|
-
only in-memory storage by design.
|
1293
|
-
|
1294
|
-
|
1295
|
-
to
|
1324
|
+
includes circuit breakers. It auto-wires circuits for MySQL, Net::HTTP, and
|
1325
|
+
Redis. It has only in-memory storage by design. Its core components are
|
1326
|
+
written in C, which allows it to be faster than pure ruby.
|
1327
|
+
- [circuitbox](https://github.com/yammer/circuitbox): Also uses a block syntax
|
1328
|
+
to manually define circuits. It uses Moneta to abstract circuit storage to
|
1329
|
+
allow any key-value store.
|
1296
1330
|
|
1297
1331
|
### Previous Work
|
1298
1332
|
|
1299
1333
|
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
|
1300
1334
|
recent activity)
|
1301
1335
|
- [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
|
1302
|
-
- [circuit_breaker](https://github.com/
|
1336
|
+
- [circuit_breaker](https://github.com/wsargent/circuit_breaker) (no recent
|
1337
|
+
activity)
|
1303
1338
|
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
1304
1339
|
(unmaintained)
|
1305
1340
|
- [breaker](https://github.com/ahawkins/breaker) (unmaintained)
|
@@ -1309,6 +1344,7 @@ but there are and have been many other options:
|
|
1309
1344
|
|
1310
1345
|
- Simple API but configurable for advanced users
|
1311
1346
|
- Pluggable storage backends (circuitbox also has this)
|
1347
|
+
- Patches for common core dependencies (semian also has this)
|
1312
1348
|
- Protected storage access with fallback to safe storage
|
1313
1349
|
- Global, or object-oriented configuration with multiple instances
|
1314
1350
|
- Integrated caching support tailored for fault-tolerance
|
data/bin/benchmark
CHANGED
@@ -4,12 +4,13 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'benchmark'
|
6
6
|
require 'faulty'
|
7
|
+
require 'redis'
|
8
|
+
require 'json'
|
7
9
|
|
8
10
|
n = 100_000
|
9
|
-
|
10
|
-
puts "
|
11
|
-
|
12
|
-
Benchmark.bm(25) do |b|
|
11
|
+
width = 25
|
12
|
+
puts "In memory circuits x#{n}"
|
13
|
+
Benchmark.bm(width) do |b|
|
13
14
|
in_memory = Faulty.new(listeners: [])
|
14
15
|
b.report('memory storage') do
|
15
16
|
n.times { in_memory.circuit(:memory).run { true } }
|
@@ -31,11 +32,33 @@ Benchmark.bm(25) do |b|
|
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
n =
|
35
|
+
n = 1000
|
36
|
+
puts "\n\Redis circuits x#{n}"
|
37
|
+
Benchmark.bm(width) do |b|
|
38
|
+
redis = Faulty.new(listeners: [], storage: Faulty::Storage::Redis.new)
|
39
|
+
b.report('redis storage') do
|
40
|
+
n.times { redis.circuit(:memory).run { true } }
|
41
|
+
end
|
35
42
|
|
36
|
-
|
43
|
+
b.report('redis storage failures') do
|
44
|
+
n.times do
|
45
|
+
begin
|
46
|
+
redis.circuit(:memory_fail, sample_threshold: n + 1).run { raise 'fail' }
|
47
|
+
rescue StandardError
|
48
|
+
# Expected to raise here
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
37
52
|
|
38
|
-
|
53
|
+
redis_large = Faulty.new(listeners: [], storage: Faulty::Storage::Redis.new(max_sample_size: 1000))
|
54
|
+
b.report('large redis storage') do
|
55
|
+
n.times { redis_large.circuit(:memory).run { true } }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
n = 1_000_000
|
60
|
+
puts "\n\nExtra x#{n}"
|
61
|
+
Benchmark.bm(width) do |b|
|
39
62
|
in_memory = Faulty.new(listeners: [])
|
40
63
|
|
41
64
|
log_listener = Faulty::Events::LogListener.new(Logger.new(File::NULL))
|
data/lib/faulty/circuit.rb
CHANGED
@@ -49,9 +49,13 @@ class Faulty
|
|
49
49
|
# @!attribute [r] cool_down
|
50
50
|
# @return [Integer] The number of seconds the circuit will
|
51
51
|
# stay open after it is tripped. Default 300.
|
52
|
-
# @!attribute [r]
|
53
|
-
# @return [Module] Used by patches to set the namespace module for
|
54
|
-
# the faulty errors that will be raised.
|
52
|
+
# @!attribute [r] error_mapper
|
53
|
+
# @return [Module, #call] Used by patches to set the namespace module for
|
54
|
+
# the faulty errors that will be raised. Should be a module or a callable.
|
55
|
+
# If given a module, the circuit assumes the module has error classes
|
56
|
+
# in that module. If given an object that responds to `#call` (a proc
|
57
|
+
# or lambda), the return value of the callable will be used. The callable
|
58
|
+
# is called with (`error_name`, `cause_error`, `circuit`). Default `Faulty`
|
55
59
|
# @!attribute [r] evaluation_window
|
56
60
|
# @return [Integer] The number of seconds of history that
|
57
61
|
# will be evaluated to determine the failure rate for a circuit.
|
@@ -93,6 +97,7 @@ class Faulty
|
|
93
97
|
:rate_threshold,
|
94
98
|
:sample_threshold,
|
95
99
|
:errors,
|
100
|
+
:error_mapper,
|
96
101
|
:error_module,
|
97
102
|
:exclude,
|
98
103
|
:cache,
|
@@ -120,7 +125,7 @@ class Faulty
|
|
120
125
|
cache_refreshes_after: 900,
|
121
126
|
cool_down: 300,
|
122
127
|
errors: [StandardError],
|
123
|
-
|
128
|
+
error_mapper: Faulty,
|
124
129
|
exclude: [],
|
125
130
|
evaluation_window: 60,
|
126
131
|
rate_threshold: 0.5,
|
@@ -133,7 +138,7 @@ class Faulty
|
|
133
138
|
cache
|
134
139
|
cool_down
|
135
140
|
errors
|
136
|
-
|
141
|
+
error_mapper
|
137
142
|
exclude
|
138
143
|
evaluation_window
|
139
144
|
rate_threshold
|
@@ -153,6 +158,17 @@ class Faulty
|
|
153
158
|
unless cache_refreshes_after.nil?
|
154
159
|
self.cache_refresh_jitter = 0.2 * cache_refreshes_after
|
155
160
|
end
|
161
|
+
|
162
|
+
deprecated_error_module
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def deprecated_error_module
|
168
|
+
return unless error_module
|
169
|
+
|
170
|
+
Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
|
171
|
+
self.error_mapper = error_module
|
156
172
|
end
|
157
173
|
end
|
158
174
|
|
@@ -379,7 +395,7 @@ class Faulty
|
|
379
395
|
# @return The result from cache if available
|
380
396
|
def run_skipped(cached_value)
|
381
397
|
skipped!
|
382
|
-
raise
|
398
|
+
raise map_error(:OpenCircuitError) if cached_value.nil?
|
383
399
|
|
384
400
|
cached_value
|
385
401
|
end
|
@@ -397,10 +413,13 @@ class Faulty
|
|
397
413
|
rescue *options.errors => e
|
398
414
|
raise if options.exclude.any? { |ex| e.is_a?(ex) }
|
399
415
|
|
416
|
+
opened = failure!(status, e)
|
400
417
|
if cached_value.nil?
|
401
|
-
|
402
|
-
|
403
|
-
|
418
|
+
if opened
|
419
|
+
raise map_error(:CircuitTrippedError, e)
|
420
|
+
else
|
421
|
+
raise map_error(:CircuitFailureError, e)
|
422
|
+
end
|
404
423
|
else
|
405
424
|
cached_value
|
406
425
|
end
|
@@ -408,7 +427,11 @@ class Faulty
|
|
408
427
|
|
409
428
|
# @return [Boolean] True if the circuit transitioned to closed
|
410
429
|
def success!(status)
|
411
|
-
|
430
|
+
if deprecated_entry?
|
431
|
+
storage.entry(self, Faulty.current_time, true, nil)
|
432
|
+
else
|
433
|
+
storage.entry(self, Faulty.current_time, true)
|
434
|
+
end
|
412
435
|
closed = close! if status.half_open?
|
413
436
|
|
414
437
|
options.notifier.notify(:circuit_success, circuit: self)
|
@@ -417,8 +440,11 @@ class Faulty
|
|
417
440
|
|
418
441
|
# @return [Boolean] True if the circuit transitioned to open
|
419
442
|
def failure!(status, error)
|
420
|
-
|
421
|
-
|
443
|
+
status = if deprecated_entry?
|
444
|
+
storage.entry(self, Faulty.current_time, false, status)
|
445
|
+
else
|
446
|
+
deprecated_entry(status)
|
447
|
+
end
|
422
448
|
options.notifier.notify(:circuit_failure, circuit: self, status: status, error: error)
|
423
449
|
|
424
450
|
opened = if status.half_open?
|
@@ -432,6 +458,23 @@ class Faulty
|
|
432
458
|
opened
|
433
459
|
end
|
434
460
|
|
461
|
+
def deprecated_entry?
|
462
|
+
return @deprecated_entry unless @deprecated_entry.nil?
|
463
|
+
|
464
|
+
@deprecated_entry = storage.method(:entry).arity == 4
|
465
|
+
end
|
466
|
+
|
467
|
+
def deprecated_entry(status)
|
468
|
+
Faulty::Deprecation.deprecate(
|
469
|
+
'Returning entries array from entry',
|
470
|
+
note: 'see Storate::Interface#entry',
|
471
|
+
sunset: '0.9'
|
472
|
+
)
|
473
|
+
|
474
|
+
entries = storage.entry(self, Faulty.current_time, false)
|
475
|
+
Status.from_entries(entries, **status.to_h)
|
476
|
+
end
|
477
|
+
|
435
478
|
def skipped!
|
436
479
|
options.notifier.notify(:circuit_skipped, circuit: self)
|
437
480
|
end
|
@@ -528,5 +571,13 @@ class Faulty
|
|
528
571
|
|
529
572
|
@given_options.storage
|
530
573
|
end
|
574
|
+
|
575
|
+
def map_error(error_name, cause = nil)
|
576
|
+
if options.error_mapper.respond_to?(:call)
|
577
|
+
options.error_mapper.call(error_name, cause, self)
|
578
|
+
else
|
579
|
+
options.error_mapper.const_get(error_name).new(cause&.message, self)
|
580
|
+
end
|
581
|
+
end
|
531
582
|
end
|
532
583
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
# Support deprecating Faulty features
|
5
|
+
module Deprecation
|
6
|
+
class << self
|
7
|
+
# Call to raise errors instead of logging warnings for Faulty deprecations
|
8
|
+
def raise_errors!(enabled = true)
|
9
|
+
@raise_errors = (enabled == true)
|
10
|
+
end
|
11
|
+
|
12
|
+
def silenced
|
13
|
+
@silence = true
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
@silence = false
|
17
|
+
end
|
18
|
+
|
19
|
+
# @private
|
20
|
+
def method(klass, name, note: nil, sunset: nil)
|
21
|
+
deprecate("#{klass}##{name}", note: note, sunset: sunset)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @private
|
25
|
+
def deprecate(subject, note: nil, sunset: nil)
|
26
|
+
return if @silence
|
27
|
+
|
28
|
+
message = "#{subject} is deprecated"
|
29
|
+
message += " and will be removed in #{sunset}" if sunset
|
30
|
+
message += " (#{note})" if note
|
31
|
+
raise DeprecationError, message if @raise_errors
|
32
|
+
|
33
|
+
Kernel.warn("DEPRECATION: #{message}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/faulty/error.rb
CHANGED
@@ -28,6 +28,9 @@ class Faulty
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
class DeprecationError < FaultyError
|
32
|
+
end
|
33
|
+
|
31
34
|
# Included in faulty circuit errors to provide common features for
|
32
35
|
# native and patched errors
|
33
36
|
module CircuitErrorBase
|
@@ -36,10 +39,11 @@ class Faulty
|
|
36
39
|
# @param message [String]
|
37
40
|
# @param circuit [Circuit] The circuit that raised the error
|
38
41
|
def initialize(message, circuit)
|
39
|
-
|
40
|
-
|
42
|
+
full_message = %(circuit error for "#{circuit.name}")
|
43
|
+
full_message = %(#{full_message}: #{message}) if message
|
41
44
|
|
42
|
-
|
45
|
+
@circuit = circuit
|
46
|
+
super(full_message)
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'elasticsearch'
|
4
|
+
|
5
|
+
class Faulty
|
6
|
+
module Patch
|
7
|
+
# Patch Elasticsearch to run requests in a circuit
|
8
|
+
#
|
9
|
+
# This module is not required by default
|
10
|
+
#
|
11
|
+
# Pass a `:faulty` key into your Elasticsearch client options to enable
|
12
|
+
# circuit protection. See {Patch.circuit_from_hash} for the available
|
13
|
+
# options.
|
14
|
+
#
|
15
|
+
# By default, all circuit errors raised by this patch inherit from
|
16
|
+
# `::Elasticsearch::Transport::Transport::Error`. One side effect of the way
|
17
|
+
# this patch wraps errors is that `host_unreachable_exceptions` raised by
|
18
|
+
# the inner transport adapters are converted into
|
19
|
+
# `Elasticsearch::Transport::Transport::Error` instead of the transport
|
20
|
+
# error type such as `Faraday::ConnectionFailed`.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# require 'faulty/patch/elasticsearch'
|
24
|
+
#
|
25
|
+
# es = Elasticsearch::Client.new(url: 'http://localhost:9200', faulty: {})
|
26
|
+
# es.search(q: 'test') # raises Faulty::CircuitError if connection fails
|
27
|
+
#
|
28
|
+
# # If the faulty key is not given, no circuit is used
|
29
|
+
# es = Elasticsearch::Client.new(url: 'http://localhost:9200', faulty: {})
|
30
|
+
# es.search(q: 'test') # not protected by a circuit
|
31
|
+
#
|
32
|
+
# # With Searchkick
|
33
|
+
# Searchkick.client_options[:faulty] = {}
|
34
|
+
#
|
35
|
+
# @see Patch.circuit_from_hash
|
36
|
+
module Elasticsearch
|
37
|
+
include Base
|
38
|
+
|
39
|
+
module Error; end
|
40
|
+
module SnifferTimeoutError; end
|
41
|
+
module ServerError; end
|
42
|
+
|
43
|
+
# We will freeze this after adding the dynamic error classes
|
44
|
+
MAPPED_ERRORS = { # rubocop:disable Style/MutableConstant
|
45
|
+
::Elasticsearch::Transport::Transport::Error => Error,
|
46
|
+
::Elasticsearch::Transport::Transport::SnifferTimeoutError => SnifferTimeoutError,
|
47
|
+
::Elasticsearch::Transport::Transport::ServerError => ServerError
|
48
|
+
}
|
49
|
+
|
50
|
+
module Errors
|
51
|
+
::Elasticsearch::Transport::Transport::ERRORS.each do |_code, klass|
|
52
|
+
MAPPED_ERRORS[klass] = const_set(klass.name.split('::').last, Module.new)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
MAPPED_ERRORS.freeze
|
57
|
+
MAPPED_ERRORS.each do |klass, mod|
|
58
|
+
Patch.define_circuit_errors(mod, klass)
|
59
|
+
end
|
60
|
+
|
61
|
+
ERROR_MAPPER = lambda do |error_name, cause, circuit|
|
62
|
+
MAPPED_ERRORS.fetch(cause&.class, Error).const_get(error_name).new(cause&.message, circuit)
|
63
|
+
end
|
64
|
+
private_constant :ERROR_MAPPER, :MAPPED_ERRORS
|
65
|
+
|
66
|
+
def initialize(arguments = {}, &block)
|
67
|
+
super
|
68
|
+
|
69
|
+
errors = [::Elasticsearch::Transport::Transport::Error]
|
70
|
+
errors.concat(@transport.host_unreachable_exceptions)
|
71
|
+
|
72
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
73
|
+
'elasticsearch',
|
74
|
+
arguments[:faulty],
|
75
|
+
errors: errors,
|
76
|
+
exclude: ::Elasticsearch::Transport::Transport::Errors::NotFound,
|
77
|
+
patched_error_mapper: ERROR_MAPPER
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Protect all elasticsearch requests
|
82
|
+
def perform_request(*args)
|
83
|
+
faulty_run { super }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module Elasticsearch
|
90
|
+
module Transport
|
91
|
+
class Client
|
92
|
+
prepend(Faulty::Patch::Elasticsearch)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/faulty/patch/mysql2.rb
CHANGED
@@ -20,6 +20,9 @@ class Faulty
|
|
20
20
|
# protected by the circuit. This is to allow open transactions to be closed
|
21
21
|
# if possible.
|
22
22
|
#
|
23
|
+
# By default, all circuit errors raised by this patch inherit from
|
24
|
+
# `::Mysql2::Error::ConnectionError`
|
25
|
+
#
|
23
26
|
# @example
|
24
27
|
# require 'faulty/patch/mysql2'
|
25
28
|
#
|
@@ -50,7 +53,7 @@ class Faulty
|
|
50
53
|
::Mysql2::Error::ConnectionError,
|
51
54
|
::Mysql2::Error::TimeoutError
|
52
55
|
],
|
53
|
-
|
56
|
+
patched_error_mapper: Faulty::Patch::Mysql2
|
54
57
|
)
|
55
58
|
|
56
59
|
super
|
@@ -78,4 +81,8 @@ class Faulty
|
|
78
81
|
end
|
79
82
|
end
|
80
83
|
|
81
|
-
|
84
|
+
module Mysql2
|
85
|
+
class Client
|
86
|
+
prepend(Faulty::Patch::Mysql2)
|
87
|
+
end
|
88
|
+
end
|
data/lib/faulty/patch/redis.rb
CHANGED
@@ -12,6 +12,9 @@ class Faulty
|
|
12
12
|
# circuit protection. See {Patch.circuit_from_hash} for the available
|
13
13
|
# options.
|
14
14
|
#
|
15
|
+
# By default, all circuit errors raised by this patch inherit from
|
16
|
+
# `::Redis::BaseConnectionError`
|
17
|
+
#
|
15
18
|
# @example
|
16
19
|
# require 'faulty/patch/redis'
|
17
20
|
#
|
@@ -40,7 +43,7 @@ class Faulty
|
|
40
43
|
::Redis::BaseConnectionError,
|
41
44
|
BusyError
|
42
45
|
],
|
43
|
-
|
46
|
+
patched_error_mapper: Faulty::Patch::Redis
|
44
47
|
)
|
45
48
|
|
46
49
|
super
|
@@ -90,4 +93,8 @@ class Faulty
|
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
93
|
-
|
96
|
+
class Redis
|
97
|
+
class Client
|
98
|
+
prepend(Faulty::Patch::Redis)
|
99
|
+
end
|
100
|
+
end
|
data/lib/faulty/patch.rb
CHANGED
@@ -64,14 +64,15 @@ class Faulty
|
|
64
64
|
# option and these additional options
|
65
65
|
# @option hash [String] :name The circuit name. Defaults to `default_name`
|
66
66
|
# @option hash [Boolean] :patch_errors By default, circuit errors will be
|
67
|
-
# subclasses of `options[:
|
67
|
+
# subclasses of `options[:patched_error_mapper]`. The user can disable
|
68
68
|
# this by setting this option to false.
|
69
69
|
# @option hash [Faulty, String, Symbol, Hash{ constant: String }] :instance
|
70
70
|
# A reference to a faulty instance. See examples.
|
71
71
|
# @param options [Hash] Additional override options. Supports any circuit
|
72
72
|
# option and these additional ones.
|
73
|
-
# @option options [Module] :
|
74
|
-
# for patched errors
|
73
|
+
# @option options [Module] :patched_error_mapper The namespace module
|
74
|
+
# for patched errors or a mapping proc. See {Faulty::Circuit::Options}
|
75
|
+
# `:error_mapper`
|
75
76
|
# @yield [Circuit::Options] For setting override options in a block
|
76
77
|
# @return [Circuit, nil] The circuit if one was created
|
77
78
|
def circuit_from_hash(default_name, hash, **options, &block)
|
@@ -80,8 +81,8 @@ class Faulty
|
|
80
81
|
hash = symbolize_keys(hash)
|
81
82
|
name = hash.delete(:name) || default_name
|
82
83
|
patch_errors = hash.delete(:patch_errors) != false
|
83
|
-
|
84
|
-
hash[:
|
84
|
+
error_mapper = options.delete(:patched_error_mapper)
|
85
|
+
hash[:error_mapper] ||= error_mapper if error_mapper && patch_errors
|
85
86
|
faulty = resolve_instance(hash.delete(:instance))
|
86
87
|
faulty.circuit(name, **hash, **options, &block)
|
87
88
|
end
|
@@ -69,7 +69,7 @@ class Faulty
|
|
69
69
|
|
70
70
|
# Wrap an array of storage backends in a fault-tolerant FallbackChain
|
71
71
|
#
|
72
|
-
# @param [Array<Storage::Interface>] The array to wrap
|
72
|
+
# @param array [Array<Storage::Interface>] The array to wrap
|
73
73
|
# @param options [Options]
|
74
74
|
# @return [Storage::Interface] A fault-tolerant fallback chain
|
75
75
|
def wrap_array(array, options)
|
@@ -81,7 +81,7 @@ class Faulty
|
|
81
81
|
|
82
82
|
# Wrap one storage backend in fault-tolerant backends
|
83
83
|
#
|
84
|
-
# @param [Storage::Interface] The storage to wrap
|
84
|
+
# @param storage [Storage::Interface] The storage to wrap
|
85
85
|
# @param options [Options]
|
86
86
|
# @return [Storage::Interface] A fault-tolerant storage backend
|
87
87
|
def wrap_one(storage, options)
|
@@ -93,7 +93,7 @@ class Faulty
|
|
93
93
|
|
94
94
|
# Wrap storage in a CircuitProxy
|
95
95
|
#
|
96
|
-
# @param [Storage::Interface] The storage to wrap
|
96
|
+
# @param storage [Storage::Interface] The storage to wrap
|
97
97
|
# @param options [Options]
|
98
98
|
# @return [CircuitProxy]
|
99
99
|
def circuit_proxy(storage, options)
|
@@ -69,8 +69,8 @@ class Faulty
|
|
69
69
|
#
|
70
70
|
# @param (see Interface#entry)
|
71
71
|
# @return (see Interface#entry)
|
72
|
-
def entry(circuit, time, success)
|
73
|
-
send_chain(:entry, circuit, time, success) do |e|
|
72
|
+
def entry(circuit, time, success, status)
|
73
|
+
send_chain(:entry, circuit, time, success, status) do |e|
|
74
74
|
options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
|
75
75
|
end
|
76
76
|
end
|
@@ -112,11 +112,11 @@ class Faulty
|
|
112
112
|
# @see Interface#entry
|
113
113
|
# @param (see Interface#entry)
|
114
114
|
# @return (see Interface#entry)
|
115
|
-
def entry(circuit, time, success)
|
116
|
-
@storage.entry(circuit, time, success)
|
115
|
+
def entry(circuit, time, success, status)
|
116
|
+
@storage.entry(circuit, time, success, status)
|
117
117
|
rescue StandardError => e
|
118
118
|
options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
|
119
|
-
|
119
|
+
stub_status(circuit) if status
|
120
120
|
end
|
121
121
|
|
122
122
|
# Safely mark a circuit as open
|
@@ -21,7 +21,7 @@ class Faulty
|
|
21
21
|
# They should be returned exactly as given by {#set_options}
|
22
22
|
#
|
23
23
|
# @param circuit [Circuit] The circuit to set options for
|
24
|
-
# @param
|
24
|
+
# @param stored_options [Hash<Symbol, Object>] A hash of symbol option names to
|
25
25
|
# circuit options. These option values are guranteed to be primive
|
26
26
|
# values.
|
27
27
|
# @return [void]
|
@@ -37,9 +37,10 @@ class Faulty
|
|
37
37
|
# @param circuit [Circuit] The circuit that ran
|
38
38
|
# @param time [Integer] The unix timestamp for the run
|
39
39
|
# @param success [Boolean] True if the run succeeded
|
40
|
-
# @
|
41
|
-
# the new entry
|
42
|
-
|
40
|
+
# @param status [Status, nil] The previous status. If given, this method must
|
41
|
+
# return an updated status object from the new entry data.
|
42
|
+
# @return [Status, nil] If `status` is not nil, the updated status object.
|
43
|
+
def entry(circuit, time, success, status)
|
43
44
|
raise NotImplementedError
|
44
45
|
end
|
45
46
|
|
@@ -99,13 +99,14 @@ class Faulty
|
|
99
99
|
# @see Interface#entry
|
100
100
|
# @param (see Interface#entry)
|
101
101
|
# @return (see Interface#entry)
|
102
|
-
def entry(circuit, time, success)
|
102
|
+
def entry(circuit, time, success, status)
|
103
103
|
memory = fetch(circuit)
|
104
104
|
memory.runs.borrow do |runs|
|
105
105
|
runs.push([time, success])
|
106
106
|
runs.shift if runs.size > options.max_sample_size
|
107
107
|
end
|
108
|
-
|
108
|
+
|
109
|
+
Status.from_entries(memory.runs.value, **status.to_h) if status
|
109
110
|
end
|
110
111
|
|
111
112
|
# Mark a circuit as open
|
data/lib/faulty/storage/null.rb
CHANGED
@@ -24,8 +24,8 @@ class Faulty
|
|
24
24
|
|
25
25
|
# @param (see Interface#entry)
|
26
26
|
# @return (see Interface#entry)
|
27
|
-
def entry(
|
28
|
-
|
27
|
+
def entry(circuit, _time, _success, status)
|
28
|
+
stub_status(circuit) if status
|
29
29
|
end
|
30
30
|
|
31
31
|
# @param (see Interface#open)
|
@@ -64,10 +64,7 @@ class Faulty
|
|
64
64
|
# @param (see Interface#status)
|
65
65
|
# @return (see Interface#status)
|
66
66
|
def status(circuit)
|
67
|
-
|
68
|
-
options: circuit.options,
|
69
|
-
stub: true
|
70
|
-
)
|
67
|
+
stub_status(circuit)
|
71
68
|
end
|
72
69
|
|
73
70
|
# @param (see Interface#history)
|
@@ -89,6 +86,15 @@ class Faulty
|
|
89
86
|
def fault_tolerant?
|
90
87
|
true
|
91
88
|
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def stub_status(circuit)
|
93
|
+
Faulty::Status.new(
|
94
|
+
options: circuit.options,
|
95
|
+
stub: true
|
96
|
+
)
|
97
|
+
end
|
92
98
|
end
|
93
99
|
end
|
94
100
|
end
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -119,7 +119,7 @@ class Faulty
|
|
119
119
|
# @see Interface#entry
|
120
120
|
# @param (see Interface#entry)
|
121
121
|
# @return (see Interface#entry)
|
122
|
-
def entry(circuit, time, success)
|
122
|
+
def entry(circuit, time, success, status)
|
123
123
|
key = entries_key(circuit)
|
124
124
|
result = pipe do |r|
|
125
125
|
r.sadd(list_key, circuit.name)
|
@@ -127,9 +127,10 @@ class Faulty
|
|
127
127
|
r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
|
128
128
|
r.ltrim(key, 0, options.max_sample_size - 1)
|
129
129
|
r.expire(key, options.sample_ttl) if options.sample_ttl
|
130
|
-
r.lrange(key, 0, -1)
|
130
|
+
r.lrange(key, 0, -1) if status
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
|
+
Status.from_entries(map_entries(result.last), **status.to_h) if status
|
133
134
|
end
|
134
135
|
|
135
136
|
# Mark a circuit as open
|
@@ -138,11 +139,14 @@ class Faulty
|
|
138
139
|
# @param (see Interface#open)
|
139
140
|
# @return (see Interface#open)
|
140
141
|
def open(circuit, opened_at)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
142
|
+
key = state_key(circuit)
|
143
|
+
ex = options.circuit_ttl
|
144
|
+
result = watch_exec(key, ['closed', nil]) do |m|
|
145
|
+
m.set(key, 'open', ex: ex)
|
146
|
+
m.set(opened_at_key(circuit), opened_at, ex: ex)
|
145
147
|
end
|
148
|
+
|
149
|
+
result && result[0] == 'OK'
|
146
150
|
end
|
147
151
|
|
148
152
|
# Mark a circuit as reopened
|
@@ -151,9 +155,12 @@ class Faulty
|
|
151
155
|
# @param (see Interface#reopen)
|
152
156
|
# @return (see Interface#reopen)
|
153
157
|
def reopen(circuit, opened_at, previous_opened_at)
|
154
|
-
|
155
|
-
|
158
|
+
key = opened_at_key(circuit)
|
159
|
+
result = watch_exec(key, [previous_opened_at.to_s]) do |m|
|
160
|
+
m.set(key, opened_at, ex: options.circuit_ttl)
|
156
161
|
end
|
162
|
+
|
163
|
+
result && result[0] == 'OK'
|
157
164
|
end
|
158
165
|
|
159
166
|
# Mark a circuit as closed
|
@@ -162,11 +169,14 @@ class Faulty
|
|
162
169
|
# @param (see Interface#close)
|
163
170
|
# @return (see Interface#close)
|
164
171
|
def close(circuit)
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
closed
|
172
|
+
key = state_key(circuit)
|
173
|
+
ex = options.circuit_ttl
|
174
|
+
result = watch_exec(key, ['open']) do |m|
|
175
|
+
m.set(key, 'closed', ex: ex)
|
176
|
+
m.del(entries_key(circuit))
|
169
177
|
end
|
178
|
+
|
179
|
+
result && result[0] == 'OK'
|
170
180
|
end
|
171
181
|
|
172
182
|
# Lock a circuit open or closed
|
@@ -220,11 +230,15 @@ class Faulty
|
|
220
230
|
futures[:entries] = r.lrange(entries_key(circuit), 0, -1)
|
221
231
|
end
|
222
232
|
|
233
|
+
state = futures[:state].value&.to_sym || :closed
|
234
|
+
opened_at = futures[:opened_at].value ? futures[:opened_at].value.to_i : nil
|
235
|
+
opened_at = Faulty.current_time - options.circuit_ttl if state == :open && opened_at.nil?
|
236
|
+
|
223
237
|
Faulty::Status.from_entries(
|
224
238
|
map_entries(futures[:entries].value),
|
225
|
-
state:
|
239
|
+
state: state,
|
226
240
|
lock: futures[:lock].value&.to_sym,
|
227
|
-
opened_at:
|
241
|
+
opened_at: opened_at,
|
228
242
|
options: circuit.options
|
229
243
|
)
|
230
244
|
end
|
@@ -329,23 +343,28 @@ class Faulty
|
|
329
343
|
(Faulty.current_time.to_f / options.list_granularity).floor
|
330
344
|
end
|
331
345
|
|
332
|
-
#
|
346
|
+
# Watch a Redis key and exec commands only if the key matches the expected
|
347
|
+
# value. Internally this uses Redis transactions with WATCH/MULTI/EXEC.
|
333
348
|
#
|
334
|
-
# @param
|
335
|
-
# @param
|
336
|
-
#
|
337
|
-
#
|
338
|
-
#
|
339
|
-
#
|
340
|
-
#
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
+
# @param key [String] The redis key to watch
|
350
|
+
# @param old [Array<String>] A list of previous values. The block will be
|
351
|
+
# run only if key is one of these values.
|
352
|
+
# @yield [Redis] A redis client. Commands executed using this client
|
353
|
+
# will be executed inside the MULTI context and will only be run if
|
354
|
+
# the watch succeeds and the comparison passes
|
355
|
+
# @return [Array] An array of Redis results from the commands executed
|
356
|
+
# inside the block
|
357
|
+
def watch_exec(key, old)
|
358
|
+
redis do |r|
|
359
|
+
r.watch(key) do
|
360
|
+
if old.include?(r.get(key))
|
361
|
+
r.multi do |m|
|
362
|
+
yield m
|
363
|
+
end
|
364
|
+
else
|
365
|
+
r.unwatch
|
366
|
+
nil
|
367
|
+
end
|
349
368
|
end
|
350
369
|
end
|
351
370
|
end
|
data/lib/faulty/version.rb
CHANGED
data/lib/faulty.rb
CHANGED
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
require 'securerandom'
|
4
4
|
require 'forwardable'
|
5
|
-
require 'concurrent
|
5
|
+
require 'concurrent'
|
6
6
|
|
7
|
+
require 'faulty/deprecation'
|
7
8
|
require 'faulty/immutable_options'
|
8
9
|
require 'faulty/cache'
|
9
10
|
require 'faulty/circuit'
|
@@ -143,14 +144,14 @@ class Faulty
|
|
143
144
|
@disabled = true
|
144
145
|
end
|
145
146
|
|
146
|
-
# Re-enable Faulty if disabled with {
|
147
|
+
# Re-enable Faulty if disabled with {.disable!}
|
147
148
|
#
|
148
149
|
# @return [void]
|
149
150
|
def enable!
|
150
151
|
@disabled = false
|
151
152
|
end
|
152
153
|
|
153
|
-
# Check whether Faulty was disabled with {
|
154
|
+
# Check whether Faulty was disabled with {.disable!}
|
154
155
|
#
|
155
156
|
# @return [Boolean] True if disabled
|
156
157
|
def disabled?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faulty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Howard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -159,6 +159,7 @@ files:
|
|
159
159
|
- lib/faulty/cache/rails.rb
|
160
160
|
- lib/faulty/circuit.rb
|
161
161
|
- lib/faulty/circuit_registry.rb
|
162
|
+
- lib/faulty/deprecation.rb
|
162
163
|
- lib/faulty/error.rb
|
163
164
|
- lib/faulty/events.rb
|
164
165
|
- lib/faulty/events/callback_listener.rb
|
@@ -170,6 +171,7 @@ files:
|
|
170
171
|
- lib/faulty/immutable_options.rb
|
171
172
|
- lib/faulty/patch.rb
|
172
173
|
- lib/faulty/patch/base.rb
|
174
|
+
- lib/faulty/patch/elasticsearch.rb
|
173
175
|
- lib/faulty/patch/mysql2.rb
|
174
176
|
- lib/faulty/patch/redis.rb
|
175
177
|
- lib/faulty/result.rb
|