standard_circuit 0.4.1 → 0.4.2

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: 5772ca0d1cec01fde070aab6ed65f232e52b00f7b2f4fa966348f3866a49dea7
4
- data.tar.gz: a8ce2582796937fe0816f454c9a2fd56e6a29d85e35850136a0b5f7a0bc7885f
3
+ metadata.gz: 27549729eb52395cef12a0e01e39d48f7218b2cd564f823f2d12d515ffe2fb82
4
+ data.tar.gz: 6ed8b33e22ccd8e655bb75dcb815098bb4a9dd16ed1daef7bd7c9c586eb1a18d
5
5
  SHA512:
6
- metadata.gz: 1c880e1e97c57f07a0a576a3b2e699583cf7719b1414a4dcc363f55b35109c30367954cea575b163abb8419439a1e9ed264a724110eec85264871e9894b7b930
7
- data.tar.gz: 0b14981a97b3588b149b41d24c7ac7a554254a449ce40a1773f5921f9d6ca1417e7f5f21116ac239aaefba1993aade11d54a9985026d79adc88aeaaa78555580
6
+ metadata.gz: e8ebb089c834961f250167f316c9e0648fa51f3600a6a1d6774731acdad3842be7c7fde2afb24806fb4be6af4fb341f0a412090a4100151226c9b3dca58aab46
7
+ data.tar.gz: 3a5f2af8401788aba1f13321a16dd23d7a94f9becf14834e6bc95672a4d21beeaa0e2b87e6cf440ad3417d90ded2c47b30067d4d3cb72634a63b1aca474a0348
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.2] - 2026-09-24
11
+
12
+ ### Fixed
13
+ - **Exhausted mail jobs now fail (dead-letter) instead of being discarded.** `mailer_retry` installs `retry_on CircuitOpenError` with a block, and ActiveJob swallows the error when that block returns normally. So on exhaustion 0.4.0 and 0.4.1 logged, sent the Sentry event, emitted `standard_circuit.mailer.retries_exhausted`, and then let the job complete. The email was silently dropped, and nothing reached Solid Queue's failed executions, where it could have been retried. The handler now reports exactly as before and then re-raises the `CircuitOpenError`, so the job fails. Earlier attempts still re-enqueue with the configured wait. If reporting itself raises, that is logged and the original error is still re-raised. No config change.
14
+
15
+ ### Upgrade notes
16
+ - **Expect one more Sentry event per exhausted email** if your host captures failed jobs (sentry-rails' ActiveJob integration does). It is the re-raised `CircuitOpenError`, grouped by exception class rather than by the `standard_circuit-mailer-retries-exhausted` fingerprint. The fingerprinted event is unchanged and is still the one to alert on. To keep only the fingerprinted event, drop the job-failure event in `before_send`. The gem does not set the fingerprint on the ambient Sentry scope, because outside a job's own scope (inline `perform_now`) it would leak onto unrelated events.
17
+ - Failed mail jobs now accumulate in your queue backend's failed jobs (Solid Queue: `solid_queue_failed_executions`). Retry them once the provider is healthy, or discard them deliberately.
18
+
10
19
  ## [0.4.1] - 2026-09-24
11
20
 
12
21
  ### Fixed
data/README.md CHANGED
@@ -219,7 +219,7 @@ end
219
219
  - **Keep `wait` longer than the mail circuit's `cool_off_time`** (the `:postmark` preset uses 60s), or retries land on a circuit that is still open.
220
220
  - **Reload-safe and idempotent.** Calling `configure` from `to_prepare` on every reload installs the handler once. If a `retry_on CircuitOpenError` is already on the job (for example your old initializer, during migration), the gem leaves it alone. The first install wins, so changing the options later needs a restart.
221
221
  - **Subclasses inherit it.** A custom `self.delivery_job = MyJob < ActionMailer::MailDeliveryJob` gets the handler, and a `retry_on CircuitOpenError` declared on the subclass takes precedence.
222
- - **On exhaustion** it writes one `error` log line, sends a Sentry `:error` event (when `sentry_enabled` and Sentry is initialized, fingerprinted by mailer and action), and emits `standard_circuit.mailer.retries_exhausted`. Every payload holds only `mailer_class`, `mail_action`, `recipient_domains`, `job_id` and `executions`: recipient **domains**, never addresses or subjects.
222
+ - **On exhaustion** it writes one `error` log line, sends a Sentry `:error` event (when `sentry_enabled` and Sentry is initialized, fingerprinted by mailer and action), and emits `standard_circuit.mailer.retries_exhausted`. Every payload holds only `mailer_class`, `mail_action`, `recipient_domains`, `job_id` and `executions`: recipient **domains**, never addresses or subjects. It then **re-raises** the `CircuitOpenError`, so the job fails and lands in your queue backend's failed jobs (Solid Queue's failed executions) where it can be inspected and retried, instead of completing with the email dropped (since 0.4.2). If your Sentry setup also captures job failures (sentry-rails' ActiveJob integration does), that failure arrives as a second event, grouped by the exception rather than by the fingerprint above.
223
223
  - Only `CircuitOpenError` is retried. Errors from the provider while the circuit is closed, such as a SendGrid 429 or a Postmark 422, keep their existing behaviour. Add your own `retry_on` / `discard_on` for those.
224
224
 
225
225
  Replace your host code with:
@@ -29,7 +29,9 @@ module StandardCircuit
29
29
  # On exhaustion: one error log line, a Sentry `:error` event (when
30
30
  # `sentry_enabled` and Sentry is initialized), and a
31
31
  # `standard_circuit.mailer.retries_exhausted` event. PII: recipient
32
- # DOMAINS only — never addresses or subjects.
32
+ # DOMAINS only — never addresses or subjects. Then the CircuitOpenError is
33
+ # re-raised, so the job FAILS (dead letter) rather than completing with
34
+ # the email dropped (0.4.2; 0.4.0/0.4.1 swallowed it).
33
35
  module Retry
34
36
  EXHAUSTED_EVENT = "standard_circuit.mailer.retries_exhausted".freeze
35
37
  EXHAUSTED_MESSAGE = "Email delivery failed: mail circuit breaker retries exhausted".freeze
@@ -115,11 +117,32 @@ module StandardCircuit
115
117
  attempts: options.fetch(:attempts),
116
118
  jitter: options.fetch(:jitter)
117
119
  ) do |job, error|
118
- StandardCircuit::Mailer::Retry.report_exhausted(job, error)
120
+ StandardCircuit::Mailer::Retry.exhausted!(job, error)
119
121
  end
120
122
  true
121
123
  end
122
124
 
125
+ # Runs when the last attempt still hits an open circuit. Reports first
126
+ # (log, fingerprinted Sentry event, retries_exhausted event), then
127
+ # RE-RAISES so the job fails. ActiveJob's retry_on swallows the error
128
+ # when given a block unless the block raises, and a swallowed error
129
+ # means the job "succeeds" and the email is silently dropped. Raising
130
+ # lands it in the queue backend's failed executions (Solid Queue's
131
+ # dead letter), where it can be inspected and retried.
132
+ #
133
+ # A report that raises must not turn into a dropped email, so it is
134
+ # rescued and logged; the original error is raised regardless.
135
+ #
136
+ # @api private
137
+ def exhausted!(job, error)
138
+ begin
139
+ report_exhausted(job, error)
140
+ rescue StandardError => report_error
141
+ logger&.error("[standard_circuit] failed to report mail retry exhaustion: #{report_error.class}: #{report_error.message}")
142
+ end
143
+ raise error
144
+ end
145
+
123
146
  def installed?(job_class)
124
147
  job_class.rescue_handlers.any? { |(error_name, _handler)| error_name == CircuitOpenError.name }
125
148
  end
@@ -1,3 +1,3 @@
1
1
  module StandardCircuit
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
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.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim