faulty 0.11.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: 8604d16866bb43e4b58ebd4d9efd9e3ae2a85555bd5e2d92c1f1ec8bfa24fb69
4
- data.tar.gz: a766e4e122b41e4d58cab15fc0c249a254e940e523b54e8d906d00bcefafb5e2
3
+ metadata.gz: 70d795aa07a3ebdb6ab1d59f03b0a93d6c65c6c83c88c4700eb0e553fdf210c6
4
+ data.tar.gz: 25df4a11f04f0a865bf9ea1236d0ad75f4b10b5d810966894c57911126b9dfde
5
5
  SHA512:
6
- metadata.gz: d2bf7c915310ab7eac608fc738f64df191555c7acfe1b6a28eb5a8e0519e42f2cf0512ba5bd7d13d62379783cdbefef69c9fa952d08ff78bf95d973afbd7f15e
7
- data.tar.gz: 56963c2e80e34f2f56ac5a4efa405032f9eb72bd176ee0e7683a2f7dd0a4d26f06535773b9056c84c50d2d5349b2c2eb6d9dcc97fa98f3ef130245e3d65adb2b
6
+ metadata.gz: d778e63ab973c4e31c64f72af87e53d99ce744f39e28edf698d7cf32a53da77c65beccd645e6447ca5360bd821169b2700e16224127a2c287cb9c353d33f133d
7
+ data.tar.gz: 1bb9248883024814297780af0f2417cae07466f246714b238bf2da18f29eb0bea9c6d0f56985b7e72cc7a8b6824756da5945bd7e931addea766d1d09ae597eb3
data/CHANGELOG.md CHANGED
@@ -9,6 +9,47 @@ 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
+
12
53
  [0.11.0] - 2023-04-26
13
54
  ---------------------
14
55
 
data/README.md CHANGED
@@ -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
@@ -1353,12 +1353,15 @@ but there are and have been many other options:
1353
1353
  - [circuitbox](https://github.com/yammer/circuitbox): Also uses a block syntax
1354
1354
  to manually define circuits. It uses Moneta to abstract circuit storage to
1355
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.
1356
1360
 
1357
1361
  ### Previous Work
1358
1362
 
1359
1363
  - [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
1360
1364
  recent activity)
1361
- - [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
1362
1365
  - [circuit_breaker](https://github.com/wsargent/circuit_breaker) (no recent
1363
1366
  activity)
1364
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
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]
@@ -55,7 +55,7 @@ class Faulty
55
55
  #
56
56
  # The call* methods above will then raise that error, so we are able to
57
57
  # capture it with faulty_run.
58
- def io(&block)
58
+ def io(&)
59
59
  return super unless @faulty_circuit
60
60
 
61
61
  reply = super
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
@@ -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
@@ -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
@@ -3,6 +3,6 @@
3
3
  class Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.11.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.11.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-27 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: []
@@ -152,8 +152,8 @@ licenses:
152
152
  metadata:
153
153
  rubygems_mfa_required: 'true'
154
154
  changelog_uri: https://github.com/ParentSquare/faulty/blob/master/CHANGELOG.md
155
- documentation_uri: https://www.rubydoc.info/gems/faulty/0.11.0
156
- post_install_message:
155
+ documentation_uri: https://www.rubydoc.info/gems/faulty/0.12.0
156
+ post_install_message:
157
157
  rdoc_options: []
158
158
  require_paths:
159
159
  - lib
@@ -161,15 +161,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - ">="
163
163
  - !ruby/object:Gem::Version
164
- version: '2.3'
164
+ version: '3.1'
165
165
  required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - ">="
168
168
  - !ruby/object:Gem::Version
169
169
  version: '0'
170
170
  requirements: []
171
- rubygems_version: 3.3.5
172
- signing_key:
171
+ rubygems_version: 3.4.20
172
+ signing_key:
173
173
  specification_version: 4
174
174
  summary: Fault-tolerance tools for ruby based on circuit-breakers
175
175
  test_files: []