standard_circuit 0.4.0 → 0.4.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: 60bd3d9dbd17cad5bdf8062be096c8f7b1d8baa00f167e25f1ea96581a078cca
4
- data.tar.gz: 5abdcf7eb5bf00b65bc0b724d3ced093f71f6ee1cb59a8d594c6a8d3944998e1
3
+ metadata.gz: 5772ca0d1cec01fde070aab6ed65f232e52b00f7b2f4fa966348f3866a49dea7
4
+ data.tar.gz: a8ce2582796937fe0816f454c9a2fd56e6a29d85e35850136a0b5f7a0bc7885f
5
5
  SHA512:
6
- metadata.gz: f88dc067ff12e4651a0f6f5317d55f22f5a9b78507d0bc31bf7fc737fa2dc3a7efa09853b64bb89f3fb608660eb4da79b126921d90a87e7ad9396bcb81cb317b
7
- data.tar.gz: 1ed24fba0bdd0e373dfbe4cc4a18cc9e22675db527c109b151395ba2681d403ea64dbf0a12672b53250edfd8886aa66be1cedd42a36099833c23562a828b89c0
6
+ metadata.gz: 1c880e1e97c57f07a0a576a3b2e699583cf7719b1414a4dcc363f55b35109c30367954cea575b163abb8419439a1e9ed264a724110eec85264871e9894b7b930
7
+ data.tar.gz: 0b14981a97b3588b149b41d24c7ac7a554254a449ce40a1773f5921f9d6ca1417e7f5f21116ac239aaefba1993aade11d54a9985026d79adc88aeaaa78555580
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-09-24
11
+
12
+ ### Fixed
13
+ - **`mailer_retry` no longer crashes a lazily-loaded process with `NameError: uninitialized constant ActionMailer::MailDeliveryJob`.** 0.4.0 installed the retry from `ActiveSupport.on_load(:active_job)` and referenced `ActionMailer::MailDeliveryJob` inside the hook. When `class MailDeliveryJob < ActiveJob::Base` was itself what loaded `ActiveJob::Base`, the hook ran while that class was still being autoloaded. In development this broke the first `deliver_later` of a fresh process, and `bin/tapioca dsl` hit it too. Eager-loaded production processes and RSpec runs were not affected. The retry is now installed at whichever of these happens first, and never while `MailDeliveryJob` is still mid-autoload:
14
+ - `ActiveJob::Base` loads when `MailDeliveryJob` can already be referenced safely.
15
+ - `ActionMailer::Base` loads. It references `MailDeliveryJob`, so the class is complete by then. Every delivery goes through a mailer, so this also covers a worker that deserializes the job before any mailer loads.
16
+ - Something subclasses `MailDeliveryJob`. This covers a custom `delivery_job` with its own `rescue_from` that loads before any mailer. The subclass's own handlers keep precedence.
17
+
18
+ A new integration spec boots a Rails app in a subprocess for each load order (mailer first, job first, subclass first, `ActiveJob::Base` first, eager load).
19
+
20
+ **Hosts can delete their workaround.** Any initializer that preloads `ActiveJob::Base` to dodge this bug, such as jumpdrive-web's `config.after_initialize { ActiveJob::Base }`, can be removed.
21
+
10
22
  ## [0.4.0] - 2026-09-24
11
23
 
12
24
  A developer-experience release that moves code copy-pasted across the five consumer apps into the gem. Everything is additive: behaviour is unchanged unless you opt in, and deprecated APIs keep working until 0.5.
@@ -35,19 +35,73 @@ module StandardCircuit
35
35
  EXHAUSTED_MESSAGE = "Email delivery failed: mail circuit breaker retries exhausted".freeze
36
36
  FINGERPRINT = "standard_circuit-mailer-retries-exhausted".freeze
37
37
 
38
+ # Prepended onto ActiveJob::Base's singleton class only when the
39
+ # :active_job hook fired mid-way through MailDeliveryJob's own
40
+ # definition. MailDeliveryJob is complete by the time anything
41
+ # subclasses it, and installing on it before the subclass body runs keeps
42
+ # the handler's precedence the same as for any other load order.
43
+ module InstallBeforeSubclassing
44
+ def inherited(subclass)
45
+ StandardCircuit::Mailer::Retry.install_configured if name == "ActionMailer::MailDeliveryJob"
46
+ super
47
+ end
48
+ end
49
+
38
50
  class << self
39
- # Defers installation until ActiveJob::Base has loaded (immediately if
40
- # it already has), so enabling this from an early initializer doesn't
41
- # force ActiveJob to load before its own configuration is applied. The
42
- # hook reads the live config when it runs; it is registered once.
51
+ # Defers installation until MailDeliveryJob can be referenced safely,
52
+ # so enabling this from an early initializer doesn't force ActiveJob
53
+ # or ActionMailer to load before their own configuration is applied.
54
+ # The hooks read the live config when they run; they are registered
55
+ # once.
56
+ #
57
+ # `ActionMailer::MailDeliveryJob` can't simply be referenced from an
58
+ # `on_load(:active_job)` hook: when `class MailDeliveryJob <
59
+ # ActiveJob::Base` is itself what loads ActiveJob::Base (a mailer or
60
+ # `deliver_later` touched first in a lazily-loaded process), the hook
61
+ # fires while that class is still being autoloaded and the reference
62
+ # raises NameError. So each trigger checks first, and together they
63
+ # cover every load order:
64
+ #
65
+ # * :active_job — installs straight away when ActiveJob::Base loads
66
+ # ahead of MailDeliveryJob (eager load, or any ApplicationJob first).
67
+ # * :action_mailer — ActionMailer::Base references MailDeliveryJob, so
68
+ # by the time this runs the class is complete. Every delivery goes
69
+ # through a mailer class, so this runs before the first delivery can
70
+ # raise, including in a worker that deserialized the job first
71
+ # (rescue handlers are read when an error is rescued, not at enqueue).
72
+ # * the first subclass of MailDeliveryJob — covers a custom
73
+ # `delivery_job` loaded ahead of any mailer, which would otherwise
74
+ # take a copy of the parent's rescue handlers without this one.
43
75
  def install_on_load
44
76
  return if @on_load_registered
45
77
 
46
78
  @on_load_registered = true
47
79
  ::ActiveSupport.on_load(:active_job) do
48
- options = StandardCircuit.config.mailer_retry
49
- StandardCircuit::Mailer::Retry.install(::ActionMailer::MailDeliveryJob, options) if options
80
+ if StandardCircuit::Mailer::Retry.mail_delivery_job_referenceable?
81
+ StandardCircuit::Mailer::Retry.install_configured
82
+ else
83
+ singleton_class.prepend(StandardCircuit::Mailer::Retry::InstallBeforeSubclassing)
84
+ end
50
85
  end
86
+ ::ActiveSupport.on_load(:action_mailer) { StandardCircuit::Mailer::Retry.install_configured }
87
+ end
88
+
89
+ # Installs on ActionMailer::MailDeliveryJob with the configured options
90
+ # when mailer_retry is on and the class is safe to reference. Returns
91
+ # true when the handler is in place afterwards.
92
+ def install_configured
93
+ options = StandardCircuit.config.mailer_retry
94
+ return false unless options && mail_delivery_job_referenceable?
95
+
96
+ install(::ActionMailer::MailDeliveryJob, options)
97
+ true
98
+ end
99
+
100
+ # False only while MailDeliveryJob is mid-autoload on this thread (see
101
+ # install_on_load): Ruby reports an in-progress autoload as undefined.
102
+ # When the autoload is merely pending, referencing it loads it normally.
103
+ def mail_delivery_job_referenceable?
104
+ defined?(::ActionMailer) && ::ActionMailer.const_defined?(:MailDeliveryJob, false)
51
105
  end
52
106
 
53
107
  # Adds the retry_on to +job_class+ unless a CircuitOpenError handler is
@@ -1,3 +1,3 @@
1
1
  module StandardCircuit
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim