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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eda80b5d7aba5948835addf9b8cf418aba0e8ba48453361678a15229ea4481a
4
- data.tar.gz: 53a27be99ae42c157be28df1e887033b86a53ccd232b05bc290bf511ac4e9d0c
3
+ metadata.gz: 70d795aa07a3ebdb6ab1d59f03b0a93d6c65c6c83c88c4700eb0e553fdf210c6
4
+ data.tar.gz: 25df4a11f04f0a865bf9ea1236d0ad75f4b10b5d810966894c57911126b9dfde
5
5
  SHA512:
6
- metadata.gz: 766ab386706d842f4f02faa572d931b702b532071baa221f5c77c1330e3086cab1f2126f548c255254fe3d4f85ca9666698116b808c0b0e29c507ee5f6680c5c
7
- data.tar.gz: c5f04aa84ad450ae186c57236d80bb872c4977e60930a0f99e876bf497aa2473c2753d2307634b1cfe0dc332ce1d3c88382281ce2158c13cf140e432afaeb97e
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.10.0...HEAD
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
  [![CI](https://github.com/ParentSquare/faulty/workflows/CI/badge.svg)](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
5
5
  [![Code Quality](https://app.codacy.com/project/badge/Grade/16bb1df1569a4ddba893a866673dac2a)](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=ParentSquare/faulty&amp;utm_campaign=Badge_Grade)
6
6
  [![Code Coverage](https://codecov.io/gh/ParentSquare/faulty/branch/master/graph/badge.svg?token=1NDT4FW1YJ)](https://codecov.io/gh/ParentSquare/faulty)
7
- [![Inline docs](http://inch-ci.org/github/ParentSquare/faulty.svg?branch=master)](http://inch-ci.org/github/ParentSquare/faulty)
7
+ [![Online docs](https://img.shields.io/badge/docs-✓-green.svg)](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.circuit_open do |payload|
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 4.
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, &block)
41
- options = Options.new(options, &block)
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, &block)
43
+ def initialize(cache, **options, &)
44
44
  @cache = cache
45
- @options = Options.new(options, &block)
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, &block)
33
+ def initialize(cache, **options, &)
34
34
  @cache = cache
35
- @options = Options.new(options, &block)
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, **options, &block)
42
+ def self.wrap(cache, ...)
43
43
  return cache if cache.fault_tolerant?
44
44
 
45
- new(cache, **options, &block)
45
+ new(cache, ...)
46
46
  end
47
47
 
48
48
  # Read from the cache safely
@@ -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, &block)
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, &block)
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(**options, &block)
270
- Result.new(ok: run(**options, &block))
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
- # Excecute a run
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(message)
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(message)
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(message)
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 ||= defined?(Rails) ? Rails.logger : ::Logger.new($stderr)
14
- @logger = logger
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
- begin
26
- listener.handle(event, payload)
27
- rescue StandardError => e
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, &block)
9
- setup(defaults.merge(hash), &block)
8
+ def initialize(hash, &)
9
+ setup(defaults.merge(hash), &)
10
10
  end
11
11
 
12
- def dup_with(hash, &block)
13
- dup.setup(hash, &block)
12
+ def dup_with(hash, &)
13
+ dup.setup(hash, &)
14
14
  end
15
15
 
16
16
  def setup(hash)
@@ -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(&block)
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(&block)
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.each do |_code, klass|
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 = {}, &block)
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
@@ -8,9 +8,12 @@ class Faulty
8
8
  #
9
9
  # This module is not required by default
10
10
  #
11
- # Pass a `:faulty` key into your MySQL connection options to enable
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
- class Redis
95
- class Client
96
- prepend(Faulty::Patch::Redis)
97
- end
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, &block)
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, &block)
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, &block)
56
- options = Options.new(options, &block)
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, &block)
43
+ def initialize(storage, **options, &)
44
44
  @storage = storage
45
- @options = Options.new(options, &block)
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, &block)
45
+ def initialize(storages, **options, &)
46
46
  @storages = storages
47
- @options = Options.new(options, &block)
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
- begin
193
- return s.public_send(method, *args)
194
- rescue StandardError => e
195
- errors << e
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
- begin
215
- s.public_send(method, *args)
216
- rescue StandardError => e
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, &block)
34
+ def initialize(storage, **options, &)
35
35
  @storage = storage
36
- @options = Options.new(options, &block)
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, **options, &block)
44
+ def self.wrap(storage, ...)
45
45
  return storage if storage.fault_tolerant?
46
46
 
47
- new(storage, **options, &block)
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, &block)
74
+ def initialize(**options, &)
75
75
  @circuits = Concurrent::Map.new
76
- @options = Options.new(options, &block)
76
+ @options = Options.new(options, &)
77
77
  end
78
78
 
79
79
  # Get the options stored for circuit
@@ -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. Defaulty `:`.
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, &block)
84
- @options = Options.new(options, &block)
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(list_key, circuit.name)
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(&block)
390
+ def redis(&)
391
391
  if options.client.respond_to?(:with)
392
- options.client.with(&block)
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
- ropts = redis { |r| r.instance_variable_get(:@client).options }
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
- bad_timeouts[time_opt] = ropts[time_opt] if ropts[time_opt] > 2
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
- if ropts[:reconnect_attempts] > 1
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 #{ropts[:reconnect_attempts]}
452
+ prevent cascading failures. Your setting is larger.
447
453
  MSG
448
454
  end
449
455
  end
@@ -3,6 +3,6 @@
3
3
  class Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.10.0')
6
+ Gem::Version.new('0.12.0')
7
7
  end
8
8
  end
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, &block)
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, &block)) unless default_name.nil?
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, &block)
114
- default.circuit(name, **config, &block)
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, &block)
240
- @options = Options.new(options, &block)
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, &block)
255
+ def circuit(name, **options, &)
256
256
  name = name.to_s
257
- @registry.retrieve(name, options, &block)
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
- .select { |k, _v| %i[cache storage notifier].include?(k) }
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.10.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: 2023-04-05 00:00:00.000000000 Z
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.10.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: '2.3'
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.3.5
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: []