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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +19 -8
- data/app/controllers/standard_circuit/health_controller.rb +2 -2
- data/lib/standard_circuit/active_storage/s3_service.rb +0 -14
- data/lib/standard_circuit/adapter_errors/faraday.rb +0 -14
- data/lib/standard_circuit/deprecator.rb +1 -1
- data/lib/standard_circuit/health.rb +1 -2
- data/lib/standard_circuit/runner.rb +15 -8
- data/lib/standard_circuit/version.rb +1 -1
- data/lib/standard_circuit.rb +0 -18
- metadata +8 -9
- data/lib/standard_circuit/health_controller.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 195e01d912e93d7bf158cafd091aab7ab27571994e9d4c2953d0a39b76c92d71
|
|
4
|
+
data.tar.gz: 880f2b881ada6a92660c8c26c1c2b49abf2ea283411606a7131c9cf1af8bd75f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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
|
-
##
|
|
362
|
+
## Removed in 0.5
|
|
354
363
|
|
|
355
|
-
0.4
|
|
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
|
-
|
|
|
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
|
|
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"`
|
|
16
|
-
#
|
|
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.
|
|
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
|
|
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)
|
data/lib/standard_circuit.rb
CHANGED
|
@@ -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
|
+
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|