faulty 0.10.0 → 0.12.0
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/CHANGELOG.md +51 -1
- data/README.md +14 -4
- data/lib/faulty/cache/auto_wire.rb +2 -2
- data/lib/faulty/cache/circuit_proxy.rb +2 -2
- data/lib/faulty/cache/fault_tolerant_proxy.rb +4 -4
- data/lib/faulty/circuit.rb +5 -5
- data/lib/faulty/error.rb +3 -3
- data/lib/faulty/events/log_listener.rb +11 -2
- data/lib/faulty/events/notifier.rb +3 -5
- data/lib/faulty/immutable_options.rb +4 -4
- data/lib/faulty/patch/base.rb +2 -2
- data/lib/faulty/patch/elasticsearch.rb +2 -2
- data/lib/faulty/patch/redis/middleware.rb +54 -0
- data/lib/faulty/patch/redis/patch.rb +76 -0
- data/lib/faulty/patch/redis.rb +13 -66
- data/lib/faulty/patch.rb +2 -2
- data/lib/faulty/storage/auto_wire.rb +2 -2
- data/lib/faulty/storage/circuit_proxy.rb +2 -2
- data/lib/faulty/storage/fallback_chain.rb +9 -13
- data/lib/faulty/storage/fault_tolerant_proxy.rb +4 -4
- data/lib/faulty/storage/memory.rb +2 -2
- data/lib/faulty/storage/redis.rb +16 -10
- data/lib/faulty/version.rb +1 -1
- data/lib/faulty.rb +9 -9
- metadata +11 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70d795aa07a3ebdb6ab1d59f03b0a93d6c65c6c83c88c4700eb0e553fdf210c6
|
|
4
|
+
data.tar.gz: 25df4a11f04f0a865bf9ea1236d0ad75f4b10b5d810966894c57911126b9dfde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d778e63ab973c4e31c64f72af87e53d99ce744f39e28edf698d7cf32a53da77c65beccd645e6447ca5360bd821169b2700e16224127a2c287cb9c353d33f133d
|
|
7
|
+
data.tar.gz: 1bb9248883024814297780af0f2417cae07466f246714b238bf2da18f29eb0bea9c6d0f56985b7e72cc7a8b6824756da5945bd7e931addea766d1d09ae597eb3
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
[Unreleased]
|
|
10
10
|
-------------------
|
|
11
11
|
|
|
12
|
+
[0.12.0] - 2026-05-13
|
|
13
|
+
---------------------
|
|
14
|
+
|
|
15
|
+
Runtime behavior is unchanged from `0.11.0` — the version bump reflects
|
|
16
|
+
support-policy and toolchain breaking changes only. Upgrading should be
|
|
17
|
+
a drop-in replacement on any supported Ruby.
|
|
18
|
+
|
|
19
|
+
### Breaking
|
|
20
|
+
|
|
21
|
+
* Drop support for Ruby < 3.1. Ruby 2.3 – 3.0 are EOL upstream and are no
|
|
22
|
+
longer covered by CI. Faulty now requires Ruby 3.1 or newer.
|
|
23
|
+
* `faulty.gemspec` declares `required_ruby_version = '>= 3.1'`, so older
|
|
24
|
+
Rubies will refuse to install the gem.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
* Modernize the CI matrix: Ruby `3.1`, `3.2`, `3.3`, `3.4`, `jruby-head`,
|
|
29
|
+
and `truffleruby-head`; Redis 4 and 5; OpenSearch `2.19.5` (default) and
|
|
30
|
+
`3.0.0`; Elasticsearch `7.17.x` for back-compat coverage. The
|
|
31
|
+
Elasticsearch 7.17 row runs the `elasticsearch:7.17.28` Docker image
|
|
32
|
+
(which ships a JDK that handles cgroupv2 on current runners) against
|
|
33
|
+
the `elasticsearch ~> 7.17.11` gem.
|
|
34
|
+
* Pass `DISABLE_INSTALL_DEMO_CONFIG=true` to the OpenSearch service
|
|
35
|
+
container so OpenSearch 2.12+ doesn't require an admin-password env
|
|
36
|
+
var just to boot up with the security plugin disabled.
|
|
37
|
+
* Replace the deprecated `:mingw` / `:x64_mingw` platform symbols in the
|
|
38
|
+
`Gemfile` with the unified `:windows` symbol Bundler now expects.
|
|
39
|
+
* Upgrade development dependencies: `rubocop ~> 1.84`, `rubocop-rspec ~> 3.9`,
|
|
40
|
+
`simplecov-cobertura ~> 3.1`, `opensearch-ruby ~> 3.4`.
|
|
41
|
+
* `LogListener` now lazy-requires `logger` only when constructing the
|
|
42
|
+
default `Logger.new($stderr)`. Consumers that pass their own logger or
|
|
43
|
+
run under Rails do not need the stdlib `logger` gem on the load path.
|
|
44
|
+
This keeps Faulty boot-clean on Ruby 3.5+ where `logger` is no longer a
|
|
45
|
+
default gem.
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
* Drop Redis 3 from the test matrix.
|
|
50
|
+
* Drop the Ruby 2.3 carve-out in `spec/spec_helper.rb` that conditionally
|
|
51
|
+
loaded the `mysql2` patch.
|
|
52
|
+
|
|
53
|
+
[0.11.0] - 2023-04-26
|
|
54
|
+
---------------------
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
* Add storage support for redis gem v5 #63 justinhoward
|
|
59
|
+
* Add Redis 5 support for patch #67 justinhoward
|
|
60
|
+
|
|
12
61
|
[0.10.0] - 2023-04-05
|
|
13
62
|
---------------------
|
|
14
63
|
|
|
@@ -300,7 +349,8 @@ of AutoWire.
|
|
|
300
349
|
|
|
301
350
|
Initial public release
|
|
302
351
|
|
|
303
|
-
[Unreleased]: https://github.com/ParentSquare/faulty/compare/v0.
|
|
352
|
+
[Unreleased]: https://github.com/ParentSquare/faulty/compare/v0.11.0...HEAD
|
|
353
|
+
[0.11.0]: https://github.com/ParentSquare/faulty/compare/v0.10.0...v0.11.0
|
|
304
354
|
[0.10.0]: https://github.com/ParentSquare/faulty/compare/v0.9.0...v0.10.0
|
|
305
355
|
[0.9.0]: https://github.com/ParentSquare/faulty/compare/v0.8.7...v0.9.0
|
|
306
356
|
[0.8.7]: https://github.com/ParentSquare/faulty/compare/v0.8.6...v0.8.7
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
|
|
5
5
|
[](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ParentSquare/faulty&utm_campaign=Badge_Grade)
|
|
6
6
|
[](https://codecov.io/gh/ParentSquare/faulty)
|
|
7
|
-
[](https://www.rubydoc.info/github/ParentSquare/faulty)
|
|
8
8
|
|
|
9
9
|
Fault-tolerance tools for ruby based on [circuit-breakers][martin fowler].
|
|
10
10
|
|
|
@@ -146,7 +146,7 @@ Faulty.init do |config|
|
|
|
146
146
|
config.storage = Faulty::Storage::Redis.new
|
|
147
147
|
|
|
148
148
|
config.listeners << Faulty::Events::CallbackListener.new do |events|
|
|
149
|
-
events.
|
|
149
|
+
events.circuit_opened do |payload|
|
|
150
150
|
puts 'Circuit was opened'
|
|
151
151
|
end
|
|
152
152
|
end
|
|
@@ -985,11 +985,12 @@ Mysql2::Client.new(host: '127.0.0.1', faulty: { instance: 'mysql2' })
|
|
|
985
985
|
protects a Redis client with an internal circuit. Pass a `:faulty` key along
|
|
986
986
|
with your connection options to enable the circuit breaker.
|
|
987
987
|
|
|
988
|
-
The Redis patch supports the Redis gem versions 3 and
|
|
988
|
+
The Redis patch supports the Redis gem versions 3 and up
|
|
989
989
|
|
|
990
990
|
```ruby
|
|
991
991
|
require 'faulty/patch/redis'
|
|
992
992
|
|
|
993
|
+
# For Redis <= 4, pass faulty into the top-level connection options
|
|
993
994
|
redis = Redis.new(url: 'redis://localhost:6379', faulty: {
|
|
994
995
|
# The name for the redis circuit
|
|
995
996
|
name: 'redis'
|
|
@@ -1004,6 +1005,12 @@ redis = Redis.new(url: 'redis://localhost:6379', faulty: {
|
|
|
1004
1005
|
# will raise its default errors
|
|
1005
1006
|
patch_errors: true
|
|
1006
1007
|
})
|
|
1008
|
+
|
|
1009
|
+
# Or for Redis 5+, pass faulty into the custom connection options
|
|
1010
|
+
redis = Redis.new(url: 'redis://localhost:6379', custom: { faulty: {
|
|
1011
|
+
# ...
|
|
1012
|
+
}})
|
|
1013
|
+
|
|
1007
1014
|
redis.connect # raises Faulty::CircuitError if connection fails
|
|
1008
1015
|
|
|
1009
1016
|
# If the faulty key is not given, no circuit is used
|
|
@@ -1346,12 +1353,15 @@ but there are and have been many other options:
|
|
|
1346
1353
|
- [circuitbox](https://github.com/yammer/circuitbox): Also uses a block syntax
|
|
1347
1354
|
to manually define circuits. It uses Moneta to abstract circuit storage to
|
|
1348
1355
|
allow any key-value store.
|
|
1356
|
+
- [stoplight](https://github.com/bolshakov/stoplight): Stoplight uses Redis for
|
|
1357
|
+
leader-less coordination in distributed environments to ensure coordinated states
|
|
1358
|
+
transitions and recovery and offers a built-in Admin Panel while focusing on performance
|
|
1359
|
+
by utilizing Lua scripts for the Redis data store which minimizes operational overhead.
|
|
1349
1360
|
|
|
1350
1361
|
### Previous Work
|
|
1351
1362
|
|
|
1352
1363
|
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
|
|
1353
1364
|
recent activity)
|
|
1354
|
-
- [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
|
|
1355
1365
|
- [circuit_breaker](https://github.com/wsargent/circuit_breaker) (no recent
|
|
1356
1366
|
activity)
|
|
1357
1367
|
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
|
@@ -37,8 +37,8 @@ class Faulty
|
|
|
37
37
|
# @param cache [Interface] A cache backend
|
|
38
38
|
# @param options [Hash] Attributes for {Options}
|
|
39
39
|
# @yield [Options] For setting options in a block
|
|
40
|
-
def wrap(cache, **options, &
|
|
41
|
-
options = Options.new(options, &
|
|
40
|
+
def wrap(cache, **options, &)
|
|
41
|
+
options = Options.new(options, &)
|
|
42
42
|
if cache.nil?
|
|
43
43
|
Cache::Default.new
|
|
44
44
|
elsif cache.fault_tolerant?
|
|
@@ -40,9 +40,9 @@ class Faulty
|
|
|
40
40
|
# @param cache [Cache::Interface] The cache backend to wrap
|
|
41
41
|
# @param options [Hash] Attributes for {Options}
|
|
42
42
|
# @yield [Options] For setting options in a block
|
|
43
|
-
def initialize(cache, **options, &
|
|
43
|
+
def initialize(cache, **options, &)
|
|
44
44
|
@cache = cache
|
|
45
|
-
@options = Options.new(options, &
|
|
45
|
+
@options = Options.new(options, &)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
%i[read write].each do |method|
|
|
@@ -30,19 +30,19 @@ class Faulty
|
|
|
30
30
|
# @param cache [Cache::Interface] The cache backend to wrap
|
|
31
31
|
# @param options [Hash] Attributes for {Options}
|
|
32
32
|
# @yield [Options] For setting options in a block
|
|
33
|
-
def initialize(cache, **options, &
|
|
33
|
+
def initialize(cache, **options, &)
|
|
34
34
|
@cache = cache
|
|
35
|
-
@options = Options.new(options, &
|
|
35
|
+
@options = Options.new(options, &)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Wrap a cache in a FaultTolerantProxy unless it's already fault tolerant
|
|
39
39
|
#
|
|
40
40
|
# @param cache [Cache::Interface] The cache to maybe wrap
|
|
41
41
|
# @return [Cache::Interface] The original cache or a {FaultTolerantProxy}
|
|
42
|
-
def self.wrap(cache,
|
|
42
|
+
def self.wrap(cache, ...)
|
|
43
43
|
return cache if cache.fault_tolerant?
|
|
44
44
|
|
|
45
|
-
new(cache,
|
|
45
|
+
new(cache, ...)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Read from the cache safely
|
data/lib/faulty/circuit.rb
CHANGED
|
@@ -178,11 +178,11 @@ class Faulty
|
|
|
178
178
|
# @param name [String] The name of the circuit
|
|
179
179
|
# @param options [Hash] Attributes for {Options}
|
|
180
180
|
# @yield [Options] For setting options in a block
|
|
181
|
-
def initialize(name, **options, &
|
|
181
|
+
def initialize(name, **options, &)
|
|
182
182
|
raise ArgumentError, 'name must be a String' unless name.is_a?(String)
|
|
183
183
|
|
|
184
184
|
@name = name
|
|
185
|
-
@given_options = Options.new(options, &
|
|
185
|
+
@given_options = Options.new(options, &)
|
|
186
186
|
@pulled_options = nil
|
|
187
187
|
@options_pushed = false
|
|
188
188
|
end
|
|
@@ -266,8 +266,8 @@ class Faulty
|
|
|
266
266
|
# @return [Result<Object, Error>] A result where the ok value is the return
|
|
267
267
|
# value of the block, or the error value is an error captured by the
|
|
268
268
|
# circuit.
|
|
269
|
-
def try_run(
|
|
270
|
-
Result.new(ok: run(
|
|
269
|
+
def try_run(...)
|
|
270
|
+
Result.new(ok: run(...))
|
|
271
271
|
rescue FaultyError => e
|
|
272
272
|
Result.new(error: e)
|
|
273
273
|
end
|
|
@@ -403,7 +403,7 @@ class Faulty
|
|
|
403
403
|
cached_value
|
|
404
404
|
end
|
|
405
405
|
|
|
406
|
-
#
|
|
406
|
+
# Execute a run
|
|
407
407
|
#
|
|
408
408
|
# @param cached_value The cached value if one is available
|
|
409
409
|
# @param cache_key [String, nil] The cache key if one is given
|
data/lib/faulty/error.rb
CHANGED
|
@@ -8,7 +8,7 @@ class Faulty
|
|
|
8
8
|
class UninitializedError < FaultyError
|
|
9
9
|
def initialize(message = nil)
|
|
10
10
|
message ||= 'Faulty is not initialized'
|
|
11
|
-
super
|
|
11
|
+
super
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ class Faulty
|
|
|
16
16
|
class AlreadyInitializedError < FaultyError
|
|
17
17
|
def initialize(message = nil)
|
|
18
18
|
message ||= 'Faulty is already initialized'
|
|
19
|
-
super
|
|
19
|
+
super
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ class Faulty
|
|
|
24
24
|
class MissingDefaultInstanceError < FaultyError
|
|
25
25
|
def initialize(message = nil)
|
|
26
26
|
message ||= 'No default instance. Create one with init or get your instance with Faulty[:name]'
|
|
27
|
-
super
|
|
27
|
+
super
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -10,8 +10,17 @@ class Faulty
|
|
|
10
10
|
# by default if available, otherwise it creates a new `Logger` to
|
|
11
11
|
# stderr.
|
|
12
12
|
def initialize(logger = nil)
|
|
13
|
-
logger
|
|
14
|
-
|
|
13
|
+
@logger = if logger
|
|
14
|
+
logger
|
|
15
|
+
elsif defined?(Rails)
|
|
16
|
+
Rails.logger
|
|
17
|
+
else
|
|
18
|
+
# Lazy-require so consumers who pass their own logger or use Rails
|
|
19
|
+
# don't need the stdlib `logger` gem on the load path. Required for
|
|
20
|
+
# Ruby >= 3.5 where `logger` was extracted from the default gems.
|
|
21
|
+
require 'logger'
|
|
22
|
+
::Logger.new($stderr)
|
|
23
|
+
end
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
# (see ListenerInterface#handle)
|
|
@@ -22,11 +22,9 @@ class Faulty
|
|
|
22
22
|
raise ArgumentError, "Unknown event #{event}" unless EVENTS.include?(event)
|
|
23
23
|
|
|
24
24
|
@listeners.each do |listener|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
warn "Faulty listener #{listener.class.name} crashed: #{e.message}"
|
|
29
|
-
end
|
|
25
|
+
listener.handle(event, payload)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
warn "Faulty listener #{listener.class.name} crashed: #{e.message}"
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
end
|
|
@@ -5,12 +5,12 @@ class Faulty
|
|
|
5
5
|
module ImmutableOptions
|
|
6
6
|
# @param hash [Hash] A hash of attributes to initialize with
|
|
7
7
|
# @yield [self] Yields itself to the block to set options before freezing
|
|
8
|
-
def initialize(hash, &
|
|
9
|
-
setup(defaults.merge(hash), &
|
|
8
|
+
def initialize(hash, &)
|
|
9
|
+
setup(defaults.merge(hash), &)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def dup_with(hash, &
|
|
13
|
-
dup.setup(hash, &
|
|
12
|
+
def dup_with(hash, &)
|
|
13
|
+
dup.setup(hash, &)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def setup(hash)
|
data/lib/faulty/patch/base.rb
CHANGED
|
@@ -31,13 +31,13 @@ class Faulty
|
|
|
31
31
|
#
|
|
32
32
|
# @yield A block to run inside the circuit
|
|
33
33
|
# @return The block return value
|
|
34
|
-
def faulty_run(&
|
|
34
|
+
def faulty_run(&)
|
|
35
35
|
faulty_running_key = "faulty_running_#{object_id}"
|
|
36
36
|
return yield unless @faulty_circuit
|
|
37
37
|
return yield if Thread.current[faulty_running_key]
|
|
38
38
|
|
|
39
39
|
Thread.current[faulty_running_key] = true
|
|
40
|
-
@faulty_circuit.run(&
|
|
40
|
+
@faulty_circuit.run(&)
|
|
41
41
|
ensure
|
|
42
42
|
Thread.current[faulty_running_key] = nil
|
|
43
43
|
end
|
|
@@ -54,7 +54,7 @@ class Faulty
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
module Errors
|
|
57
|
-
PATCHED_MODULE::Transport::Transport::ERRORS.
|
|
57
|
+
PATCHED_MODULE::Transport::Transport::ERRORS.each_value do |klass|
|
|
58
58
|
MAPPED_ERRORS[klass] = const_set(klass.name.split('::').last, Module.new)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -69,7 +69,7 @@ class Faulty
|
|
|
69
69
|
end
|
|
70
70
|
private_constant :ERROR_MAPPER, :MAPPED_ERRORS
|
|
71
71
|
|
|
72
|
-
def initialize(arguments = {}, &
|
|
72
|
+
def initialize(arguments = {}, &)
|
|
73
73
|
super
|
|
74
74
|
|
|
75
75
|
errors = [PATCHED_MODULE::Transport::Transport::Error]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Faulty
|
|
4
|
+
module Patch
|
|
5
|
+
module Redis
|
|
6
|
+
Patch.define_circuit_errors(self, ::RedisClient::ConnectionError)
|
|
7
|
+
|
|
8
|
+
class BusyError < ::RedisClient::CommandError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Middleware
|
|
12
|
+
include Base
|
|
13
|
+
|
|
14
|
+
def initialize(client)
|
|
15
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
|
16
|
+
'redis',
|
|
17
|
+
client.config.custom[:faulty],
|
|
18
|
+
errors: [
|
|
19
|
+
::RedisClient::ConnectionError,
|
|
20
|
+
BusyError
|
|
21
|
+
],
|
|
22
|
+
patched_error_mapper: Faulty::Patch::Redis
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def connect(redis_config)
|
|
29
|
+
faulty_run { super }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(commands, redis_config)
|
|
33
|
+
faulty_run { wrap_command { super } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call_pipelined(commands, redis_config)
|
|
37
|
+
faulty_run { wrap_command { super } }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def wrap_command
|
|
43
|
+
yield
|
|
44
|
+
rescue ::RedisClient::CommandError => e
|
|
45
|
+
raise BusyError, e.message if e.message.start_with?('BUSY')
|
|
46
|
+
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
::RedisClient.register(Middleware)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis'
|
|
4
|
+
|
|
5
|
+
class Faulty
|
|
6
|
+
module Patch
|
|
7
|
+
module Redis
|
|
8
|
+
include Base
|
|
9
|
+
|
|
10
|
+
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
|
11
|
+
|
|
12
|
+
class BusyError < ::Redis::CommandError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Patches Redis to add the `:faulty` key
|
|
16
|
+
def initialize(options = {})
|
|
17
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
|
18
|
+
'redis',
|
|
19
|
+
options[:faulty],
|
|
20
|
+
errors: [
|
|
21
|
+
::Redis::BaseConnectionError,
|
|
22
|
+
BusyError
|
|
23
|
+
],
|
|
24
|
+
patched_error_mapper: Faulty::Patch::Redis
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The initial connection is protected by a circuit
|
|
31
|
+
def connect
|
|
32
|
+
faulty_run { super }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Protect command calls
|
|
36
|
+
def call(command)
|
|
37
|
+
faulty_run { super }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Protect command_loop calls
|
|
41
|
+
def call_loop(command, timeout = 0)
|
|
42
|
+
faulty_run { super }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Protect pipelined commands
|
|
46
|
+
def call_pipelined(commands)
|
|
47
|
+
faulty_run { super }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Inject specific error classes if client is patched
|
|
51
|
+
#
|
|
52
|
+
# This method does not raise errors, it returns them
|
|
53
|
+
# as exception objects, so we simply modify that error if necessary and
|
|
54
|
+
# return it.
|
|
55
|
+
#
|
|
56
|
+
# The call* methods above will then raise that error, so we are able to
|
|
57
|
+
# capture it with faulty_run.
|
|
58
|
+
def io(&)
|
|
59
|
+
return super unless @faulty_circuit
|
|
60
|
+
|
|
61
|
+
reply = super
|
|
62
|
+
if reply.is_a?(::Redis::CommandError) && reply.message.start_with?('BUSY')
|
|
63
|
+
reply = BusyError.new(reply.message)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
reply
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class Redis
|
|
73
|
+
class Client
|
|
74
|
+
prepend(Faulty::Patch::Redis)
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/faulty/patch/redis.rb
CHANGED
|
@@ -8,9 +8,12 @@ class Faulty
|
|
|
8
8
|
#
|
|
9
9
|
# This module is not required by default
|
|
10
10
|
#
|
|
11
|
-
#
|
|
11
|
+
# Redis <= 4
|
|
12
|
+
# ---------------------
|
|
13
|
+
# Pass a `:faulty` key into your Redis connection options to enable
|
|
12
14
|
# circuit protection. See {Patch.circuit_from_hash} for the available
|
|
13
|
-
# options.
|
|
15
|
+
# options. On Redis 5+, the faulty key should be passed in the `:custom` hash
|
|
16
|
+
# instead of the top-level options. See example.
|
|
14
17
|
#
|
|
15
18
|
# By default, all circuit errors raised by this patch inherit from
|
|
16
19
|
# `::Redis::BaseConnectionError`
|
|
@@ -18,7 +21,11 @@ class Faulty
|
|
|
18
21
|
# @example
|
|
19
22
|
# require 'faulty/patch/redis'
|
|
20
23
|
#
|
|
24
|
+
# # Redis <= 4
|
|
21
25
|
# redis = Redis.new(url: 'redis://localhost:6379', faulty: {})
|
|
26
|
+
# # Or for Redis 5+
|
|
27
|
+
# redis = Redis.new(url: 'redis://localhost:6379', custom: { faulty: {} })
|
|
28
|
+
#
|
|
22
29
|
# redis.connect # raises Faulty::CircuitError if connection fails
|
|
23
30
|
#
|
|
24
31
|
# # If the faulty key is not given, no circuit is used
|
|
@@ -27,72 +34,12 @@ class Faulty
|
|
|
27
34
|
#
|
|
28
35
|
# @see Patch.circuit_from_hash
|
|
29
36
|
module Redis
|
|
30
|
-
include Base
|
|
31
|
-
|
|
32
|
-
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
|
33
|
-
|
|
34
|
-
class BusyError < ::Redis::CommandError
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Patches Redis to add the `:faulty` key
|
|
38
|
-
def initialize(options = {})
|
|
39
|
-
@faulty_circuit = Patch.circuit_from_hash(
|
|
40
|
-
'redis',
|
|
41
|
-
options[:faulty],
|
|
42
|
-
errors: [
|
|
43
|
-
::Redis::BaseConnectionError,
|
|
44
|
-
BusyError
|
|
45
|
-
],
|
|
46
|
-
patched_error_mapper: Faulty::Patch::Redis
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
super
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# The initial connection is protected by a circuit
|
|
53
|
-
def connect
|
|
54
|
-
faulty_run { super }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Protect command calls
|
|
58
|
-
def call(command)
|
|
59
|
-
faulty_run { super }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Protect command_loop calls
|
|
63
|
-
def call_loop(command, timeout = 0)
|
|
64
|
-
faulty_run { super }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Protect pipelined commands
|
|
68
|
-
def call_pipelined(commands)
|
|
69
|
-
faulty_run { super }
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Inject specific error classes if client is patched
|
|
73
|
-
#
|
|
74
|
-
# This method does not raise errors, it returns them
|
|
75
|
-
# as exception objects, so we simply modify that error if necessary and
|
|
76
|
-
# return it.
|
|
77
|
-
#
|
|
78
|
-
# The call* methods above will then raise that error, so we are able to
|
|
79
|
-
# capture it with faulty_run.
|
|
80
|
-
def io(&block)
|
|
81
|
-
return super unless @faulty_circuit
|
|
82
|
-
|
|
83
|
-
reply = super
|
|
84
|
-
if reply.is_a?(::Redis::CommandError) && reply.message.start_with?('BUSY')
|
|
85
|
-
reply = BusyError.new(reply.message)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
reply
|
|
89
|
-
end
|
|
90
37
|
end
|
|
91
38
|
end
|
|
92
39
|
end
|
|
93
40
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
41
|
+
if Redis::VERSION.to_f < 5
|
|
42
|
+
require 'faulty/patch/redis/patch'
|
|
43
|
+
else
|
|
44
|
+
require 'faulty/patch/redis/middleware'
|
|
98
45
|
end
|
data/lib/faulty/patch.rb
CHANGED
|
@@ -75,7 +75,7 @@ class Faulty
|
|
|
75
75
|
# `:error_mapper`
|
|
76
76
|
# @yield [Circuit::Options] For setting override options in a block
|
|
77
77
|
# @return [Circuit, nil] The circuit if one was created
|
|
78
|
-
def circuit_from_hash(default_name, hash, **options, &
|
|
78
|
+
def circuit_from_hash(default_name, hash, **options, &)
|
|
79
79
|
return unless hash
|
|
80
80
|
|
|
81
81
|
hash = symbolize_keys(hash)
|
|
@@ -84,7 +84,7 @@ class Faulty
|
|
|
84
84
|
error_mapper = options.delete(:patched_error_mapper)
|
|
85
85
|
hash[:error_mapper] ||= error_mapper if error_mapper && patch_errors
|
|
86
86
|
faulty = resolve_instance(hash.delete(:instance))
|
|
87
|
-
faulty.circuit(name, **hash, **options, &
|
|
87
|
+
faulty.circuit(name, **hash, **options, &)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# Create a full set of {CircuitError}s with a given base error class
|
|
@@ -52,8 +52,8 @@ class Faulty
|
|
|
52
52
|
# of storage backends to setup.
|
|
53
53
|
# @param options [Hash] Attributes for {Options}
|
|
54
54
|
# @yield [Options] For setting options in a block
|
|
55
|
-
def wrap(storage, **options, &
|
|
56
|
-
options = Options.new(options, &
|
|
55
|
+
def wrap(storage, **options, &)
|
|
56
|
+
options = Options.new(options, &)
|
|
57
57
|
if storage.nil?
|
|
58
58
|
Memory.new
|
|
59
59
|
elsif storage.is_a?(Array)
|
|
@@ -40,9 +40,9 @@ class Faulty
|
|
|
40
40
|
# @param storage [Storage::Interface] The storage backend to wrap
|
|
41
41
|
# @param options [Hash] Attributes for {Options}
|
|
42
42
|
# @yield [Options] For setting options in a block
|
|
43
|
-
def initialize(storage, **options, &
|
|
43
|
+
def initialize(storage, **options, &)
|
|
44
44
|
@storage = storage
|
|
45
|
-
@options = Options.new(options, &
|
|
45
|
+
@options = Options.new(options, &)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
%i[
|
|
@@ -42,9 +42,9 @@ class Faulty
|
|
|
42
42
|
# additional entries will be tried in sequence until one succeeds.
|
|
43
43
|
# @param options [Hash] Attributes for {Options}
|
|
44
44
|
# @yield [Options] For setting options in a block
|
|
45
|
-
def initialize(storages, **options, &
|
|
45
|
+
def initialize(storages, **options, &)
|
|
46
46
|
@storages = storages
|
|
47
|
-
@options = Options.new(options, &
|
|
47
|
+
@options = Options.new(options, &)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Get options from the first available storage backend
|
|
@@ -189,12 +189,10 @@ class Faulty
|
|
|
189
189
|
def send_chain(method, *args)
|
|
190
190
|
errors = []
|
|
191
191
|
@storages.each do |s|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
yield e
|
|
197
|
-
end
|
|
192
|
+
return s.public_send(method, *args)
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
errors << e
|
|
195
|
+
yield e
|
|
198
196
|
end
|
|
199
197
|
|
|
200
198
|
raise AllFailedError.new("#{self.class}##{method} failed for all storage backends", errors)
|
|
@@ -211,11 +209,9 @@ class Faulty
|
|
|
211
209
|
def send_all(method, *args)
|
|
212
210
|
errors = []
|
|
213
211
|
@storages.each do |s|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
errors << e
|
|
218
|
-
end
|
|
212
|
+
s.public_send(method, *args)
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
errors << e
|
|
219
215
|
end
|
|
220
216
|
|
|
221
217
|
if errors.empty?
|
|
@@ -31,9 +31,9 @@ class Faulty
|
|
|
31
31
|
# @param storage [Storage::Interface] The storage backend to wrap
|
|
32
32
|
# @param options [Hash] Attributes for {Options}
|
|
33
33
|
# @yield [Options] For setting options in a block
|
|
34
|
-
def initialize(storage, **options, &
|
|
34
|
+
def initialize(storage, **options, &)
|
|
35
35
|
@storage = storage
|
|
36
|
-
@options = Options.new(options, &
|
|
36
|
+
@options = Options.new(options, &)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Wrap a storage backend in a FaultTolerantProxy unless it's already
|
|
@@ -41,10 +41,10 @@ class Faulty
|
|
|
41
41
|
#
|
|
42
42
|
# @param storage [Storage::Interface] The storage to maybe wrap
|
|
43
43
|
# @return [Storage::Interface] The original storage or a {FaultTolerantProxy}
|
|
44
|
-
def self.wrap(storage,
|
|
44
|
+
def self.wrap(storage, ...)
|
|
45
45
|
return storage if storage.fault_tolerant?
|
|
46
46
|
|
|
47
|
-
new(storage,
|
|
47
|
+
new(storage, ...)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# @!method lock(circuit, state)
|
|
@@ -71,9 +71,9 @@ class Faulty
|
|
|
71
71
|
|
|
72
72
|
# @param options [Hash] Attributes for {Options}
|
|
73
73
|
# @yield [Options] For setting options in a block
|
|
74
|
-
def initialize(**options, &
|
|
74
|
+
def initialize(**options, &)
|
|
75
75
|
@circuits = Concurrent::Map.new
|
|
76
|
-
@options = Options.new(options, &
|
|
76
|
+
@options = Options.new(options, &)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# Get the options stored for circuit
|
data/lib/faulty/storage/redis.rb
CHANGED
|
@@ -25,7 +25,7 @@ class Faulty
|
|
|
25
25
|
# circuit state. Default `faulty`.
|
|
26
26
|
# @!attribute [r] key_separator
|
|
27
27
|
# @return [String] A string used to separate the parts of the Redis keys
|
|
28
|
-
# used to store circuit state.
|
|
28
|
+
# used to store circuit state. Default `:`.
|
|
29
29
|
# @!attribute [r] max_sample_size
|
|
30
30
|
# @return [Integer] The number of cache run entries to keep in memory
|
|
31
31
|
# for each circuit. Default `100`.
|
|
@@ -80,8 +80,8 @@ class Faulty
|
|
|
80
80
|
|
|
81
81
|
# @param options [Hash] Attributes for {Options}
|
|
82
82
|
# @yield [Options] For setting options in a block
|
|
83
|
-
def initialize(**options, &
|
|
84
|
-
@options = Options.new(options, &
|
|
83
|
+
def initialize(**options, &)
|
|
84
|
+
@options = Options.new(options, &)
|
|
85
85
|
|
|
86
86
|
# Ensure JSON is available since we don't explicitly require it
|
|
87
87
|
JSON # rubocop:disable Lint/Void
|
|
@@ -122,7 +122,7 @@ class Faulty
|
|
|
122
122
|
def entry(circuit, time, success, status)
|
|
123
123
|
key = entries_key(circuit.name)
|
|
124
124
|
result = pipe do |r|
|
|
125
|
-
r.sadd
|
|
125
|
+
r.call([:sadd, list_key, circuit.name])
|
|
126
126
|
r.expire(list_key, options.circuit_ttl + options.list_granularity) if options.circuit_ttl
|
|
127
127
|
r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
|
|
128
128
|
r.ltrim(key, 0, options.max_sample_size - 1)
|
|
@@ -387,9 +387,9 @@ class Faulty
|
|
|
387
387
|
#
|
|
388
388
|
# @yield [Redis] Yields the connection to the block
|
|
389
389
|
# @return The value returned from the block
|
|
390
|
-
def redis(&
|
|
390
|
+
def redis(&)
|
|
391
391
|
if options.client.respond_to?(:with)
|
|
392
|
-
options.client.with(&
|
|
392
|
+
options.client.with(&)
|
|
393
393
|
else
|
|
394
394
|
yield options.client
|
|
395
395
|
end
|
|
@@ -425,11 +425,16 @@ class Faulty
|
|
|
425
425
|
end
|
|
426
426
|
|
|
427
427
|
def check_redis_options!
|
|
428
|
-
|
|
428
|
+
gte5 = ::Redis::VERSION.to_f >= 5
|
|
429
|
+
method = gte5 ? :config : :options
|
|
430
|
+
ropts = redis do |r|
|
|
431
|
+
r.instance_variable_get(:@client).public_send(method)
|
|
432
|
+
end
|
|
429
433
|
|
|
430
434
|
bad_timeouts = {}
|
|
431
435
|
%i[connect_timeout read_timeout write_timeout].each do |time_opt|
|
|
432
|
-
|
|
436
|
+
value = gte5 ? ropts.public_send(time_opt) : ropts[time_opt]
|
|
437
|
+
bad_timeouts[time_opt] = value if value > 2
|
|
433
438
|
end
|
|
434
439
|
|
|
435
440
|
unless bad_timeouts.empty?
|
|
@@ -440,10 +445,11 @@ class Faulty
|
|
|
440
445
|
MSG
|
|
441
446
|
end
|
|
442
447
|
|
|
443
|
-
|
|
448
|
+
gt1_retry = gte5 ? ropts.retry_connecting?(1, nil) : ropts[:reconnect_attempts] > 1
|
|
449
|
+
if gt1_retry
|
|
444
450
|
warn <<~MSG
|
|
445
451
|
Faulty recommends setting Redis reconnect_attempts to <= 1 to
|
|
446
|
-
prevent cascading failures. Your setting is
|
|
452
|
+
prevent cascading failures. Your setting is larger.
|
|
447
453
|
MSG
|
|
448
454
|
end
|
|
449
455
|
end
|
data/lib/faulty/version.rb
CHANGED
data/lib/faulty.rb
CHANGED
|
@@ -41,12 +41,12 @@ class Faulty
|
|
|
41
41
|
# @param config [Hash] Attributes for {Faulty::Options}
|
|
42
42
|
# @yield [Faulty::Options] For setting options in a block
|
|
43
43
|
# @return [self]
|
|
44
|
-
def init(default_name = :default, **config, &
|
|
44
|
+
def init(default_name = :default, **config, &)
|
|
45
45
|
raise AlreadyInitializedError if @instances
|
|
46
46
|
|
|
47
47
|
@default_instance = default_name
|
|
48
48
|
@instances = Concurrent::Map.new
|
|
49
|
-
register(default_name, new(**config, &
|
|
49
|
+
register(default_name, new(**config, &)) unless default_name.nil?
|
|
50
50
|
self
|
|
51
51
|
rescue StandardError
|
|
52
52
|
@instances = nil
|
|
@@ -110,8 +110,8 @@ class Faulty
|
|
|
110
110
|
# @param (see Faulty#circuit)
|
|
111
111
|
# @yield (see Faulty#circuit)
|
|
112
112
|
# @return (see Faulty#circuit)
|
|
113
|
-
def circuit(name, **config, &
|
|
114
|
-
default.circuit(name, **config, &
|
|
113
|
+
def circuit(name, **config, &)
|
|
114
|
+
default.circuit(name, **config, &)
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
# Get a list of all circuit names for the default instance
|
|
@@ -236,8 +236,8 @@ class Faulty
|
|
|
236
236
|
# @see Options
|
|
237
237
|
# @param options [Hash] Attributes for {Options}
|
|
238
238
|
# @yield [Options] For setting options in a block
|
|
239
|
-
def initialize(**options, &
|
|
240
|
-
@options = Options.new(options, &
|
|
239
|
+
def initialize(**options, &)
|
|
240
|
+
@options = Options.new(options, &)
|
|
241
241
|
@registry = CircuitRegistry.new(circuit_options)
|
|
242
242
|
end
|
|
243
243
|
|
|
@@ -252,9 +252,9 @@ class Faulty
|
|
|
252
252
|
# @param options [Hash] Attributes for {Circuit::Options}
|
|
253
253
|
# @yield [Circuit::Options] For setting options in a block
|
|
254
254
|
# @return [Circuit] The new circuit or the existing circuit if it already exists
|
|
255
|
-
def circuit(name, **options, &
|
|
255
|
+
def circuit(name, **options, &)
|
|
256
256
|
name = name.to_s
|
|
257
|
-
@registry.retrieve(name, options, &
|
|
257
|
+
@registry.retrieve(name, options, &)
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
# Get a list of all circuit names
|
|
@@ -285,7 +285,7 @@ class Faulty
|
|
|
285
285
|
# @return [Hash] The circuit options
|
|
286
286
|
def circuit_options
|
|
287
287
|
@options.to_h
|
|
288
|
-
.
|
|
288
|
+
.slice(:cache, :storage, :notifier)
|
|
289
289
|
.merge(options.circuit_defaults)
|
|
290
290
|
end
|
|
291
291
|
end
|
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.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Howard
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -94,7 +94,7 @@ dependencies:
|
|
|
94
94
|
- - ">="
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0.9'
|
|
97
|
-
description:
|
|
97
|
+
description:
|
|
98
98
|
email:
|
|
99
99
|
- jmhoward0@gmail.com
|
|
100
100
|
executables: []
|
|
@@ -132,6 +132,8 @@ files:
|
|
|
132
132
|
- lib/faulty/patch/elasticsearch.rb
|
|
133
133
|
- lib/faulty/patch/mysql2.rb
|
|
134
134
|
- lib/faulty/patch/redis.rb
|
|
135
|
+
- lib/faulty/patch/redis/middleware.rb
|
|
136
|
+
- lib/faulty/patch/redis/patch.rb
|
|
135
137
|
- lib/faulty/result.rb
|
|
136
138
|
- lib/faulty/status.rb
|
|
137
139
|
- lib/faulty/storage.rb
|
|
@@ -150,8 +152,8 @@ licenses:
|
|
|
150
152
|
metadata:
|
|
151
153
|
rubygems_mfa_required: 'true'
|
|
152
154
|
changelog_uri: https://github.com/ParentSquare/faulty/blob/master/CHANGELOG.md
|
|
153
|
-
documentation_uri: https://www.rubydoc.info/gems/faulty/0.
|
|
154
|
-
post_install_message:
|
|
155
|
+
documentation_uri: https://www.rubydoc.info/gems/faulty/0.12.0
|
|
156
|
+
post_install_message:
|
|
155
157
|
rdoc_options: []
|
|
156
158
|
require_paths:
|
|
157
159
|
- lib
|
|
@@ -159,15 +161,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
159
161
|
requirements:
|
|
160
162
|
- - ">="
|
|
161
163
|
- !ruby/object:Gem::Version
|
|
162
|
-
version: '
|
|
164
|
+
version: '3.1'
|
|
163
165
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
166
|
requirements:
|
|
165
167
|
- - ">="
|
|
166
168
|
- !ruby/object:Gem::Version
|
|
167
169
|
version: '0'
|
|
168
170
|
requirements: []
|
|
169
|
-
rubygems_version: 3.
|
|
170
|
-
signing_key:
|
|
171
|
+
rubygems_version: 3.4.20
|
|
172
|
+
signing_key:
|
|
171
173
|
specification_version: 4
|
|
172
174
|
summary: Fault-tolerance tools for ruby based on circuit-breakers
|
|
173
175
|
test_files: []
|