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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac355e3881c42900c272ba14d433216f4d8e6019e003e4b5bd52f84545c11c79
4
- data.tar.gz: ffdb732b389d5b752deef02d6cdc82fd148af38e3e861d046876cce329e50d36
3
+ metadata.gz: 335a8efd1db9120747a00cd5b6fc331cb8f31e81b6b391638a6af5877f5c1728
4
+ data.tar.gz: f23e64bf48c6e5ad494674a68e456b5874aef0800c832841c1251e4f20a74e95
5
5
  SHA512:
6
- metadata.gz: e2ffa97cfc68660ca8e32bd606df2251700f619f728969ce0e27ed485b6887eb6d8ecd6f1b641a0cea9d22c4842c3a5041a25b00bac97a84c37ea00ca78adf48
7
- data.tar.gz: 8f245331cafaeb355b33e0ddd515bb23824713826d908544533959cd1f2dcb6c21d0160887370dadeef566a495d0474f66603095c0e1d93d1275f67c35f9f27d
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: opts.fetch(:tracked_errors, NetworkErrors.defaults),
30
- skipped_errors: opts.fetch(: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(error)
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
@@ -1,3 +1,3 @@
1
1
  module StandardCircuit
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim