standard_circuit 0.4.2 → 0.6.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: 27549729eb52395cef12a0e01e39d48f7218b2cd564f823f2d12d515ffe2fb82
4
- data.tar.gz: 6ed8b33e22ccd8e655bb75dcb815098bb4a9dd16ed1daef7bd7c9c586eb1a18d
3
+ metadata.gz: 195e01d912e93d7bf158cafd091aab7ab27571994e9d4c2953d0a39b76c92d71
4
+ data.tar.gz: 880f2b881ada6a92660c8c26c1c2b49abf2ea283411606a7131c9cf1af8bd75f
5
5
  SHA512:
6
- metadata.gz: e8ebb089c834961f250167f316c9e0648fa51f3600a6a1d6774731acdad3842be7c7fde2afb24806fb4be6af4fb341f0a412090a4100151226c9b3dca58aab46
7
- data.tar.gz: 3a5f2af8401788aba1f13321a16dd23d7a94f9becf14834e6bc95672a4d21beeaa0e2b87e6cf440ad3417d90ded2c47b30067d4d3cb72634a63b1aca474a0348
6
+ metadata.gz: 4706259801d563a2b60183691ad7c4ae935face858318e358d4e3bd5459c4b2eee097da52c8cb4748c7726883455f96a1416a23cde8b0123d172207d2f64b373
7
+ data.tar.gz: 05f1578d83f4bdbc34d310350570322bf4cd357d24d50e07874bed2e34019d33bb3cf0f396e49cf9a598797700aef34d3182b98c730d4a25e737f3f083ad6e80
data/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-10-22
11
+
12
+ ### Fixed
13
+
14
+ - **Zero-argument fallback lambdas no longer crash when the circuit opens.**
15
+ A fallback is called with the error (or `nil` when forced open), so a
16
+ `fallback: -> { ... }` raised `ArgumentError` at exactly the moment the
17
+ circuit opened, the case the fallback exists for. Such lambdas are now called
18
+ without the argument. Fallbacks that take the error, and procs, are
19
+ unchanged. Zero-arity `Method` fallbacks are accepted the same way.
20
+
21
+ ### Changed
22
+
23
+ - **Requires Rails 8.1** (`railties`, `actionmailer`, `actionpack` `>= 8.1`,
24
+ was `>= 8.0`). Every consumer app runs 8.1; 8.0 was never exercised in CI.
25
+
26
+ ## [0.5.0] - 2026-09-24
27
+
28
+ **Breaking.** Removes everything 0.4 deprecated. Behaviour of everything that remains is unchanged, including 0.4.2's `mailer_retry` (exhaustion reports, then re-raises so the job dead-letters).
29
+
30
+ ### Removed
31
+ - `require "standard_circuit/health_controller"`. The shim file is gone, so the require now raises `LoadError`. `StandardCircuit::HealthController` is autoloaded by the engine (since 0.4); keep the `get "/health", to: "standard_circuit/health#show"` route and delete the require.
32
+ - `StandardCircuit.health_snapshot` and `StandardCircuit.health_overall`. Use `StandardCircuit.health_report[:circuits]` / `[:status]`, which read both atomically. `StandardCircuit::Runner#health_overall`, which only backed the removed reader, is removed too; `Runner#health_snapshot` stays because `health_report` uses it.
33
+ - `StandardCircuit::AdapterErrors::Faraday.caller_errors`. `Faraday::ClientError` is never in `ErrorTaxonomies::Faraday.tracked`, so skipping it was a no-op. `Aws`, `Stripe`, `Smtp` and `Postmark` `caller_errors` are unchanged.
34
+ - `StandardCircuit::ActiveStorage::S3Service`, the unused alias. Use `ActiveStorage::Service::StandardCircuitS3Service`, or `service: StandardCircuitS3` in storage.yml (unchanged).
35
+ - The boot probe's two legacy-require modes; a spec now pins the `LoadError`.
36
+
37
+ ### Changed
38
+ - `StandardCircuit.deprecator`'s horizon is now `0.6`.
39
+
40
+ ### Upgrade notes
41
+ - **No host code change is required in any of the five consumer apps.** Grepped `origin/main` of sidekick-web, jumpdrive-web (control-plane), fundbright-web, luminality-web and nutripod-web on 2026-09-24: none calls `health_snapshot` / `health_overall` / `Faraday.caller_errors`, references `StandardCircuit::ActiveStorage::S3Service`, or requires `standard_circuit/health_controller` (sidekick-web's `health_snapshots` is an unrelated model).
42
+ - Regenerate Sorbet RBIs after bumping (`bin/tapioca gem standard_circuit`): jumpdrive-web's `control-plane/sorbet/rbi/gems/standard_circuit@0.4.1.rbi` still declares the removed readers. `require "standard_circuit/controller_support"` / `"standard_circuit/mailer/delivery_method"` in fundbright-web's and luminality-web's `sorbet/tapioca/require.rb` are still valid paths and can stay.
43
+ - If anything outside these apps still has the legacy require, it now fails at boot with `LoadError` rather than warning. Delete the line.
44
+
10
45
  ## [0.4.2] - 2026-09-24
11
46
 
12
47
  ### Fixed
data/README.md CHANGED
@@ -54,8 +54,17 @@ end
54
54
  StandardCircuit.run(:stripe) do
55
55
  Stripe::PaymentIntent.create(amount:, currency:)
56
56
  end
57
+
58
+ # with a fallback, returned instead of raising while the circuit is open
59
+ StandardCircuit.run(:analytics, fallback: -> { nil }) do
60
+ Analytics.track(event)
61
+ end
57
62
  ```
58
63
 
64
+ A fallback may take the error (`->(error) { ... }`: the error the block raised,
65
+ or `nil` once the circuit is open, since the block then never runs) or take
66
+ nothing (`-> { ... }`, or a zero-arity `method(...)`); all are called correctly.
67
+
59
68
  ## Error taxonomies
60
69
 
61
70
  `tracked_errors` decide what counts toward tripping a circuit; `skipped_errors` are re-raised without counting (and win over `tracked_errors` when a class matches both). `StandardCircuit::ErrorTaxonomies::<Adapter>.tracked` combines `NetworkErrors.defaults` with the adapter's server-side errors for `Stripe`, `Smtp`, `Aws`, `Faraday`, and `Postmark`; `StandardCircuit::AdapterErrors::<Adapter>.caller_errors` lists the adapter's caller-side (4xx-style) errors.
@@ -323,7 +332,7 @@ require "standard_circuit/health_controller"
323
332
  # After — delete the line (and the initializer, if that's all it contained).
324
333
  ```
325
334
 
326
- The old `require` still works in 0.4 but emits a deprecation through `Rails.application.deprecators[:standard_circuit]`. It will be removed in 0.5.
335
+ The old `require` was deprecated in 0.4 and **removed in 0.5**: it now raises `LoadError`, so delete it.
327
336
 
328
337
  The controller inherits from `ActionController::API` to sidestep app-level filters (authentication, bootstrap redirects, etc.) so probes can call it anonymously.
329
338
 
@@ -350,18 +359,20 @@ These are supported public API for host test suites, not internals:
350
359
 
351
360
  All of these are **process-local**. They are test and console tools, not an operational kill switch (see `data_store` above).
352
361
 
353
- ## Deprecations
362
+ ## Removed in 0.5
354
363
 
355
- 0.4 deprecates the following. Each still works and warns through `Rails.application.deprecators[:standard_circuit]`, so your app's `config.active_support.deprecation` setting applies. They will be removed in 0.5.
364
+ 0.4 deprecated these and 0.5 removes them. None of the five consumer apps used any of them at removal time.
356
365
 
357
- | Deprecated | Use instead |
358
- |------------|-------------|
359
- | `require "standard_circuit/health_controller"` | nothing — the controller is autoloaded |
366
+ | Removed | Use instead |
367
+ |---------|-------------|
368
+ | `require "standard_circuit/health_controller"` (now `LoadError`) | nothing — the controller is autoloaded; keep the route |
360
369
  | `StandardCircuit.health_snapshot` | `StandardCircuit.health_report[:circuits]` |
361
- | `StandardCircuit.health_overall` | `StandardCircuit.health_report[:status]` |
362
- | `StandardCircuit::AdapterErrors::Faraday.caller_errors` | drop it — `Faraday::ClientError` is never tracked, so skipping it is a no-op |
370
+ | `StandardCircuit.health_overall` (and `StandardCircuit.runner.health_overall`) | `StandardCircuit.health_report[:status]` |
371
+ | `StandardCircuit::AdapterErrors::Faraday.caller_errors` | drop it — `Faraday::ClientError` is never tracked, so skipping it was a no-op |
363
372
  | `StandardCircuit::ActiveStorage::S3Service` | `ActiveStorage::Service::StandardCircuitS3Service` / `service: StandardCircuitS3` |
364
373
 
374
+ Future deprecations warn through `Rails.application.deprecators[:standard_circuit]`, so your app's `config.active_support.deprecation` setting applies.
375
+
365
376
  ## License
366
377
 
367
378
  MIT
@@ -12,8 +12,8 @@ module StandardCircuit
12
12
  # get "/health", to: "standard_circuit/health#show"
13
13
  # end
14
14
  #
15
- # The pre-0.4 `require "standard_circuit/health_controller"` still works but
16
- # is deprecated (see lib/standard_circuit/health_controller.rb).
15
+ # The pre-0.4 `require "standard_circuit/health_controller"` was removed in
16
+ # 0.5 (it raises LoadError); delete it and keep the route.
17
17
  #
18
18
  # Inherits from +ActionController::API+ to sidestep any ApplicationController
19
19
  # filters (authentication, bootstrap redirects, etc.) — health probes must be
@@ -33,17 +33,3 @@ module ActiveStorage
33
33
  end
34
34
  end
35
35
  end
36
-
37
- module StandardCircuit
38
- # @deprecated The StandardCircuit::ActiveStorage::S3Service alias is unused —
39
- # storage.yml resolves `service: StandardCircuitS3` to
40
- # ActiveStorage::Service::StandardCircuitS3Service — and is removed in 0.5.
41
- module ActiveStorage
42
- include ::ActiveSupport::Deprecation::DeprecatedConstantAccessor
43
-
44
- deprecate_constant "S3Service", "::ActiveStorage::Service::StandardCircuitS3Service",
45
- deprecator: StandardCircuit.deprecator,
46
- message: "StandardCircuit::ActiveStorage::S3Service is deprecated and will be removed in 0.5; " \
47
- "use ActiveStorage::Service::StandardCircuitS3Service (or `service: StandardCircuitS3` in storage.yml)."
48
- end
49
- end
@@ -9,20 +9,6 @@ module StandardCircuit
9
9
  errors << ::Faraday::ServerError if defined?(::Faraday::ServerError)
10
10
  errors.select { |klass| klass.is_a?(Class) }
11
11
  end
12
-
13
- # @deprecated Removed in 0.5. Faraday::ClientError (4xx) is not in
14
- # +server_errors+, so it never counts toward a Faraday circuit —
15
- # listing it in +skipped_errors+ is a no-op. Unused by every consumer.
16
- def caller_errors
17
- StandardCircuit.deprecator.warn(
18
- "StandardCircuit::AdapterErrors::Faraday.caller_errors is deprecated and will be removed in 0.5: " \
19
- "Faraday::ClientError is never tracked by ErrorTaxonomies::Faraday.tracked, so skipping it is a no-op. " \
20
- "Drop it from skipped_errors."
21
- )
22
- return [] unless defined?(::Faraday::ClientError)
23
-
24
- [ ::Faraday::ClientError ]
25
- end
26
12
  end
27
13
  end
28
14
  end
@@ -7,6 +7,6 @@ module StandardCircuit
7
7
  # host's `config.active_support.deprecation` behaviour (log / raise /
8
8
  # silence / report) applies to StandardCircuit deprecations too.
9
9
  def self.deprecator
10
- @deprecator ||= ActiveSupport::Deprecation.new("0.5", "StandardCircuit")
10
+ @deprecator ||= ActiveSupport::Deprecation.new("0.6", "StandardCircuit")
11
11
  end
12
12
  end
@@ -5,8 +5,7 @@ module StandardCircuit
5
5
  #
6
6
  # Intended for mounting in a Rails HealthController. Use +health_report+ —
7
7
  # it takes a single atomic snapshot, so the rendered status and circuits
8
- # always describe the same moment (the separate module-level
9
- # +health_snapshot+ / +health_overall+ readers are deprecated):
8
+ # always describe the same moment:
10
9
  #
11
10
  # report = StandardCircuit.health_report
12
11
  # render json: report, status: (report[:status] == :critical ? 503 : 200)
@@ -12,6 +12,7 @@ module StandardCircuit
12
12
  end
13
13
 
14
14
  def run(name, fallback: nil, &block)
15
+ fallback = accept_error_argument(fallback)
15
16
  forced = @forced_states[name.to_sym]
16
17
  return run_forced_open(name, fallback) if forced == :open
17
18
  return yield if forced == :closed
@@ -63,14 +64,6 @@ module StandardCircuit
63
64
  Health.snapshot(self, @config)
64
65
  end
65
66
 
66
- # Accepts an optional pre-computed snapshot so callers that also need the
67
- # raw circuits list don't read the data store twice (which could yield
68
- # an inconsistent status/circuits pair). Prefer +health_report+ when you
69
- # need both.
70
- def health_overall(snapshot = nil)
71
- Health.overall(snapshot || health_snapshot)
72
- end
73
-
74
67
  # Atomic health report — takes a single snapshot and returns both the
75
68
  # rolled-up status and the per-circuit list. Preferred for rendering a
76
69
  # health-check endpoint so status and circuits describe the same moment.
@@ -81,6 +74,20 @@ module StandardCircuit
81
74
 
82
75
  private
83
76
 
77
+ # A fallback is called with one argument: the error, or nil when the
78
+ # circuit is forced open. A zero-argument lambda (`-> { ... }`, the
79
+ # natural way to write "return this instead") raises ArgumentError on that
80
+ # call, so the fallback would crash at exactly the moment the circuit
81
+ # opens. Accept it, and a zero-arity Method (`fallback: method(:degraded)`),
82
+ # by calling it without the argument. Procs, and callables that take the
83
+ # error, are passed through unchanged.
84
+ def accept_error_argument(fallback)
85
+ strict = fallback.is_a?(Method) || (fallback.is_a?(Proc) && fallback.lambda?)
86
+ return fallback unless strict && fallback.arity.zero?
87
+
88
+ ->(_error) { fallback.call }
89
+ end
90
+
84
91
  def execute(name, fallback, &block)
85
92
  started_at = monotonic_now
86
93
  light = light_for(name)
@@ -1,3 +1,3 @@
1
1
  module StandardCircuit
2
- VERSION = "0.4.2"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -72,24 +72,6 @@ module StandardCircuit
72
72
  runner.reset!
73
73
  end
74
74
 
75
- # @deprecated Use +health_report[:circuits]+. Removed in 0.5.
76
- def health_snapshot
77
- deprecator.warn(
78
- "StandardCircuit.health_snapshot is deprecated and will be removed in 0.5; " \
79
- "use StandardCircuit.health_report[:circuits] (one atomic read of status + circuits)."
80
- )
81
- runner.health_snapshot
82
- end
83
-
84
- # @deprecated Use +health_report[:status]+. Removed in 0.5.
85
- def health_overall(snapshot = nil)
86
- deprecator.warn(
87
- "StandardCircuit.health_overall is deprecated and will be removed in 0.5; " \
88
- "use StandardCircuit.health_report[:status] (one atomic read of status + circuits)."
89
- )
90
- runner.health_overall(snapshot)
91
- end
92
-
93
75
  def health_report
94
76
  runner.health_report
95
77
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_circuit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -57,42 +57,42 @@ dependencies:
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '8.0'
60
+ version: '8.1'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '8.0'
67
+ version: '8.1'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: actionmailer
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: '8.0'
74
+ version: '8.1'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: '8.0'
81
+ version: '8.1'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: actionpack
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '8.0'
88
+ version: '8.1'
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '8.0'
95
+ version: '8.1'
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: brakeman
98
98
  requirement: !ruby/object:Gem::Requirement
@@ -166,7 +166,6 @@ files:
166
166
  - lib/standard_circuit/error_taxonomies.rb
167
167
  - lib/standard_circuit/event_emitter.rb
168
168
  - lib/standard_circuit/health.rb
169
- - lib/standard_circuit/health_controller.rb
170
169
  - lib/standard_circuit/mailer/circuit_open_error.rb
171
170
  - lib/standard_circuit/mailer/delivery_method.rb
172
171
  - lib/standard_circuit/mailer/retry.rb
@@ -202,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
201
  - !ruby/object:Gem::Version
203
202
  version: '0'
204
203
  requirements: []
205
- rubygems_version: 4.0.3
204
+ rubygems_version: 4.0.10
206
205
  specification_version: 4
207
206
  summary: Circuit breaker primitives for Rails apps, built on stoplight.
208
207
  test_files: []
@@ -1,23 +0,0 @@
1
- # Deprecated load path, kept so `require "standard_circuit/health_controller"`
2
- # at the top of a host's config/routes.rb (the pre-0.4 opt-in) keeps working.
3
- #
4
- # The controller now lives at app/controllers/standard_circuit/health_controller.rb
5
- # and the engine autoloads it — delete the require and keep the route.
6
- require "action_controller"
7
- require "standard_circuit"
8
-
9
- StandardCircuit.deprecator.warn(
10
- 'require "standard_circuit/health_controller" is no longer needed: the engine ' \
11
- "autoloads StandardCircuit::HealthController. Remove the require and keep the " \
12
- '`get "/health", to: "standard_circuit/health#show"` route.'
13
- )
14
-
15
- # Booted Rails app (e.g. required from config/routes.rb): the engine's
16
- # autoloader already has an autoload set for the constant, so there is nothing
17
- # to do — loading the file here would race Zeitwerk for the same path.
18
- # Otherwise (required from an initializer before autoloaders are set up, or
19
- # outside Rails entirely) load it directly; Zeitwerk later sees the constant
20
- # is defined and leaves it alone.
21
- unless StandardCircuit.autoload?(:HealthController) || StandardCircuit.const_defined?(:HealthController, false)
22
- require_relative "../../app/controllers/standard_circuit/health_controller"
23
- end