standard_circuit 0.3.0 → 0.3.1
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 +18 -0
- data/README.md +16 -0
- data/lib/generators/standard_circuit/install/templates/initializer.rb.tt +6 -1
- data/lib/standard_circuit/config.rb +7 -2
- data/lib/standard_circuit/controller_support.rb +3 -11
- data/lib/standard_circuit/error_taxonomies.rb +18 -0
- data/lib/standard_circuit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 335a8efd1db9120747a00cd5b6fc331cb8f31e81b6b391638a6af5877f5c1728
|
|
4
|
+
data.tar.gz: f23e64bf48c6e5ad494674a68e456b5874aef0800c832841c1251e4f20a74e95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec3442f7cf7d4d8efe3ef105fe8f404036691904f95826427fb5838441ff16f4d711e5041e0006f097b3c1c43a8b1426128faae75c4433b483d1a1849103a87f
|
|
7
|
+
data.tar.gz: d3fd9b73de2eea2a8b68f7863aff6328072ec6963e7f5d93a4654d27a82c856f86d99a6da96dd076e65757d4677fc9131d27246d9a39baf0028a62be74ac0530
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
5
8
|
## [Unreleased]
|
|
6
9
|
|
|
10
|
+
## [0.3.1] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
Two behaviour changes — both are bug fixes, but both change numbers or breaker behaviour you may be alerting on.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Circuit-open requests were counted twice in `<metric_prefix>.request`.** `Runner` already emits `<metric_prefix>.request{status: circuit_open}` when a call is rejected by a tripped or forced-open circuit; `ControllerSupport#handle_circuit_open` then emitted the same metric again when the `Stoplight::Error::RedLight` reached a controller. The controller-side emission (and the private `emit_circuit_open_metric` helper) is removed; fallback dispatch is unchanged. **Behaviour change:** `circuit_open` request counts for rejections that reach a controller drop by half, back to the true value — dashboards and alert thresholds tuned against the doubled number should be revisited.
|
|
16
|
+
- **S3 caller errors tripped the S3 breaker.** `ErrorTaxonomies::Aws.tracked` includes `Aws::Errors::ServiceError`, which is the superclass of `Aws::S3::Errors::AccessDenied` and `NoSuchKey` as well as of the dynamically generated 5xx errors, so a burst of missing-key lookups or permission errors opened the circuit. New `ErrorTaxonomies.default_skipped_for(tracked)` returns the AWS caller errors whenever the tracked list covers them, and `register` / `register_prefix` now use it as the default `skipped_errors`. **Behaviour change:** AWS circuits registered without `skipped_errors:` no longer count `AccessDenied` / `NoSuchKey` toward the threshold; 5xx (`ServiceUnavailable` etc.) and `Seahorse::Client::NetworkingError` still trip. An explicit `skipped_errors:` — including `[]` — always wins, and circuits for other adapters keep defaulting to `[]`.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Specs for `NetworkErrors` and the Stripe / AWS / Faraday / SMTP `AdapterErrors` modules.
|
|
20
|
+
- README section on error taxonomies and the AWS `skipped_errors` default; the initializer template notes it too.
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
- **Consumer list corrected in `CLAUDE.md`: this gem has five consumers, not four.** `sidekick-web` was missing. The same entry also claimed the `workspace-os` → `jumpdrive-web` directory rename "was deferred" and pointed at `~/Workspace/rarebit-one/workspace-os`; that rename completed 2026-07-14 and the old husk is gone, so an agent following the note was looking in a directory that no longer exists. Verified against the canonical matrix in the workspace's `rollout-gem/SKILL.md`, which the new advisory `check-gem-family-drift.sh` now diffs this list against on every sweep.
|
|
24
|
+
|
|
7
25
|
## [0.3.0] - 2026-07-30
|
|
8
26
|
|
|
9
27
|
### Added
|
data/README.md
CHANGED
|
@@ -58,6 +58,22 @@ StandardCircuit.run(:stripe) do
|
|
|
58
58
|
end
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
## Error taxonomies
|
|
62
|
+
|
|
63
|
+
`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`, and `Faraday`; `StandardCircuit::AdapterErrors::<Adapter>.caller_errors` lists the adapter's caller-side (4xx-style) errors.
|
|
64
|
+
|
|
65
|
+
When `skipped_errors:` is omitted it defaults to `[]` — except when the tracked list covers the AWS caller errors. AWS 5xx responses are dynamically generated `Aws::Errors::ServiceError` subclasses, so `ErrorTaxonomies::Aws.tracked` has to track `ServiceError` itself, which is also the superclass of `Aws::S3::Errors::AccessDenied` and `NoSuchKey`. So for such circuits `skipped_errors` defaults to `AdapterErrors::Aws.caller_errors` (via `ErrorTaxonomies.default_skipped_for`), and a burst of missing-key lookups or permission errors no longer trips the S3 breaker:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
StandardCircuit.configure do |c|
|
|
69
|
+
# skipped_errors defaults to [Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::AccessDenied]
|
|
70
|
+
c.register_prefix(:s3, tracked_errors: StandardCircuit::ErrorTaxonomies::Aws.tracked)
|
|
71
|
+
|
|
72
|
+
# An explicit skipped_errors — even [] — always wins over the default.
|
|
73
|
+
# c.register(:s3_strict, tracked_errors: StandardCircuit::ErrorTaxonomies::Aws.tracked, skipped_errors: [])
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
61
77
|
## Circuit state storage (`data_store`)
|
|
62
78
|
|
|
63
79
|
Circuit state (failure counts, colors, locks) lives in a Stoplight data store. StandardCircuit defaults to `Stoplight::DataStore::Memory.new`, which is **per-process**: each Puma worker, Sidekiq/SolidQueue worker, and console gets its own independent view of every circuit.
|
|
@@ -54,7 +54,10 @@ StandardCircuit.configure do |config|
|
|
|
54
54
|
# tracked_errors — error classes that count toward the threshold
|
|
55
55
|
# (default: StandardCircuit::NetworkErrors.defaults)
|
|
56
56
|
# skipped_errors — error classes that bypass the circuit (re-raised
|
|
57
|
-
# without counting — typically caller/validation errors
|
|
57
|
+
# without counting — typically caller/validation errors;
|
|
58
|
+
# default: [], or the AWS caller errors (NoSuchKey,
|
|
59
|
+
# AccessDenied) when tracked_errors covers them — see
|
|
60
|
+
# StandardCircuit::ErrorTaxonomies.default_skipped_for)
|
|
58
61
|
# criticality — :critical | :standard | :optional
|
|
59
62
|
# (affects health rollup; default: :standard)
|
|
60
63
|
|
|
@@ -75,6 +78,8 @@ StandardCircuit.configure do |config|
|
|
|
75
78
|
# Use `register_prefix` to dynamically name circuits matching a pattern.
|
|
76
79
|
# The first matching prefix wins; the circuit name is "<prefix>_<suffix>".
|
|
77
80
|
|
|
81
|
+
# AWS caller errors (NoSuchKey, AccessDenied) are skipped by default here,
|
|
82
|
+
# so missing keys and permission errors don't trip the S3 breaker.
|
|
78
83
|
# config.register_prefix(:s3,
|
|
79
84
|
# threshold: 10,
|
|
80
85
|
# tracked_errors: StandardCircuit::ErrorTaxonomies::Aws.tracked,
|
|
@@ -22,12 +22,17 @@ module StandardCircuit
|
|
|
22
22
|
"invalid criticality #{criticality.inspect}; must be one of #{CRITICALITIES.inspect}"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
tracked_errors = opts.fetch(:tracked_errors, NetworkErrors.defaults)
|
|
26
|
+
|
|
25
27
|
new(
|
|
26
28
|
threshold: opts.fetch(:threshold, DEFAULT_THRESHOLD),
|
|
27
29
|
cool_off_time: opts.fetch(:cool_off_time, DEFAULT_COOL_OFF),
|
|
28
30
|
window_size: opts.fetch(:window_size, DEFAULT_WINDOW),
|
|
29
|
-
tracked_errors:
|
|
30
|
-
skipped_errors
|
|
31
|
+
tracked_errors: tracked_errors,
|
|
32
|
+
# An explicit `skipped_errors:` (including `[]`) always wins; the
|
|
33
|
+
# default only kicks in when the key is absent. See
|
|
34
|
+
# ErrorTaxonomies.default_skipped_for for why AWS needs one.
|
|
35
|
+
skipped_errors: opts.fetch(:skipped_errors) { ErrorTaxonomies.default_skipped_for(tracked_errors) },
|
|
31
36
|
criticality: criticality
|
|
32
37
|
)
|
|
33
38
|
end
|
|
@@ -47,9 +47,10 @@ module StandardCircuit
|
|
|
47
47
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
|
-
def handle_circuit_open(
|
|
50
|
+
def handle_circuit_open(_error)
|
|
51
|
+
# No metric here: Runner already counted this rejection as
|
|
52
|
+
# `<metric_prefix>.request{status: circuit_open}` before re-raising.
|
|
51
53
|
fallback = self.class._circuit_open_fallback || {}
|
|
52
|
-
emit_circuit_open_metric(error)
|
|
53
54
|
|
|
54
55
|
return instance_exec(&fallback[:json]) if request.format.json? && fallback[:json]
|
|
55
56
|
return instance_exec(response, &fallback[:stream]) if streaming_controller? && fallback[:stream]
|
|
@@ -61,14 +62,5 @@ module StandardCircuit
|
|
|
61
62
|
def streaming_controller?
|
|
62
63
|
defined?(::ActionController::Live) && self.class.include?(::ActionController::Live)
|
|
63
64
|
end
|
|
64
|
-
|
|
65
|
-
def emit_circuit_open_metric(error)
|
|
66
|
-
prefix = StandardCircuit.config.metric_prefix
|
|
67
|
-
::Sentry::Metrics.count(
|
|
68
|
-
"#{prefix}.request",
|
|
69
|
-
value: 1,
|
|
70
|
-
attributes: { service: error.light_name, status: "circuit_open" }
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
65
|
end
|
|
74
66
|
end
|
|
@@ -16,6 +16,24 @@ module StandardCircuit
|
|
|
16
16
|
# `AdapterErrors::*` because the right `skipped_errors` set is usually
|
|
17
17
|
# app-specific and a shared taxonomy would over-skip.
|
|
18
18
|
module ErrorTaxonomies
|
|
19
|
+
# Default `skipped_errors` for a circuit registered without an explicit
|
|
20
|
+
# `skipped_errors:`. Most adapters default to `[]`, but AWS can't: its 5xx
|
|
21
|
+
# errors are dynamically generated `Aws::Errors::ServiceError` subclasses
|
|
22
|
+
# (e.g. `Aws::S3::Errors::ServiceUnavailable`), so `Aws.tracked` has to
|
|
23
|
+
# track `ServiceError` itself — which is also the superclass of caller
|
|
24
|
+
# errors like `AccessDenied` and `NoSuchKey`. Without a skip list, a burst
|
|
25
|
+
# of missing-key lookups or permission errors would trip the S3 breaker.
|
|
26
|
+
#
|
|
27
|
+
# Returns the AWS caller errors that some entry of +tracked+ covers (is the
|
|
28
|
+
# same class or an ancestor of), or `[]` when none are covered. Returns a
|
|
29
|
+
# fresh array each call.
|
|
30
|
+
def self.default_skipped_for(tracked)
|
|
31
|
+
trackable = Array(tracked).grep(Module)
|
|
32
|
+
AdapterErrors::Aws.caller_errors.select do |caller_error|
|
|
33
|
+
trackable.any? { |klass| caller_error <= klass }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
module Stripe
|
|
20
38
|
def self.tracked
|
|
21
39
|
NetworkErrors.defaults + AdapterErrors::Stripe.server_errors
|