Module: RactorRailsShim::CallbackCapture
- Extended by:
- RoleDefaults
- Defined in:
- lib/ractor_rails_shim/roles/callback_capture.rb
Class Method Summary collapse
-
.configure(funnel: nil, reassign_shareable_const: nil, register_patch: nil) ⇒ Object
Inject the callable collaborators.
-
.freeze_declared_callbacks! ⇒ Object
Freeze (make Ractor-shareable) the captured declared-callbacks table so worker Ractors can read it via the SHAREABLE_DECLARED_CALLBACKS constant.
-
.funnel ⇒ Object
The active funnel: the injected one if configured, else the facade lookup (
RactorRailsShim::Funnel.method(:swallow)). -
.install_callback_declaration_capture! ⇒ Object
Install an interceptor on ActiveSupport::Callbacks.set_callback that records, per declaring class, every symbolic
:process_actionfilter it declares. -
.installed? ⇒ Boolean
Has install_callback_declaration_capture! run? Lives on CallbackCapture (Issue #24 — own your own state), NOT on the facade singleton.
-
.read_action_filter_constraints(af) ⇒ Object
Read @conditional_key and @actions off an ActionFilter instance (Rails internal ivars).
-
.read_ivar_or_warn(obj, ivar, label) ⇒ Object
Read an ivar; if it's undefined, behavior is gated by the VersionPolicy::Strategy (Issue #37 — the
case policybranch is replaced by a strategy-module message): Strict — raise UnsupportedVersionError (a missing ivar means callbacks run for actions they shouldn't; failing loud pins the security-relevant failure mode instead of silently mis-routing callbacks) Warn — emit a labeled warning via funnel when debug? so a silent Rails internal rename surfaces during diagnosis Off — silent nil Returns the ivar value or nil (under Warn/Off). -
.reassign_shareable_const ⇒ Object
The active reassign callable: the injected one if configured, else the facade lookup (
RactorRailsShim.method(:_reassign_shareable_const)). -
.record_declared_callback(klass_id, chain_kind, phase, filter, only, except, if_cond = nil, unless_cond = nil) ⇒ Object
Record a single declared symbolic filter.
-
.register_patch ⇒ Object
The active register_patch callable: the injected one if configured, else the facade lookup (
RactorRailsShim.method(:_register_patch)). -
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators.
-
.reset_declared_callbacks! ⇒ Object
Clear the declared-callbacks table.
-
.reset_installed! ⇒ Object
Clear the installed flag.
Methods included from RoleDefaults
default_funnel, default_reassign_shareable_const, default_safe_const_get
Class Method Details
.configure(funnel: nil, reassign_shareable_const: nil, register_patch: nil) ⇒ Object
Inject the callable collaborators. funnel responds to
call(label) { block } (runs the block, rescues StandardError —
matches _swallow). reassign_shareable_const responds to
call(sym, value) (reassigns the shareable constant). register_ patch responds to call(name, version) (records the patch tag).
Passing nil for any (or calling reset_configuration) restores
the facade-lookup default for that collaborator.
59 60 61 62 63 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 59 def self.configure(funnel: nil, reassign_shareable_const: nil, register_patch: nil) @funnel = funnel @reassign_shareable_const = reassign_shareable_const @register_patch = register_patch end |
.freeze_declared_callbacks! ⇒ Object
Freeze (make Ractor-shareable) the captured declared-callbacks table so worker Ractors can read it via the SHAREABLE_DECLARED_CALLBACKS constant. Deep-freeze (make shareable) so workers can read the constant. Entries are Hashes of Symbols/booleans/nil/Arrays — all natively shareable. A non-frozen constant raises Ractor::IsolationError when a worker reads it.
108 109 110 111 112 113 114 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 108 def self.freeze_declared_callbacks! table = (@declared_callbacks || {}) funnel.call("freeze declared callbacks") do Ractor.make_shareable(table) reassign_shareable_const.call(:SHAREABLE_DECLARED_CALLBACKS, table) end end |
.funnel ⇒ Object
The active funnel: the injected one if configured, else the
facade lookup (RactorRailsShim::Funnel.method(:swallow)).
86 87 88 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 86 def self.funnel @funnel || default_funnel end |
.install_callback_declaration_capture! ⇒ Object
Install an interceptor on ActiveSupport::Callbacks.set_callback that
records, per declaring class, every symbolic :process_action filter
it declares. This must run BEFORE eager load (so declarations are
captured as they happen) — install wires it via the
ActiveSupport.on_load(:active_support) hook in install.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 145 def self.install_callback_declaration_capture! return if @installed register_patch.call(:action_filter_introspection, "8.1") @installed = true # ActiveSupport::Callbacks may not be loaded yet at # on_load(:active_support) time (it's required lazily). Require it so # the ClassMethods module with set_callback exists before we alias it. require "active_support/callbacks" rescue nil mod = (defined?(::ActiveSupport::Callbacks) && ::ActiveSupport::Callbacks.const_defined?(:ClassMethods)) ? ::ActiveSupport::Callbacks::ClassMethods : nil return unless mod && mod.method_defined?(:set_callback) # Alias the original `set_callback` exactly once. The @callback_ # capture_installed guard above short-circuits a second install, but # specs clear that flag to test the install path in isolation; without # this `unless`, the second alias overwrites `_rrs_orig_set_callback` # with the *interceptor* (which is now `set_callback`), so any later # `set_callback` call recurses infinitely through the interceptor. mod.alias_method(:_rrs_orig_set_callback, :set_callback) unless mod.method_defined?(:_rrs_orig_set_callback) mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def set_callback(name, *filters, &block) # Capture any SYMBOLIC filter (filters[1] is a Symbol) on an app class # — controller (AbstractController::Base) OR ActiveRecord model — for # ANY callback chain kind (:process_action, :save, :create, :destroy, # …). Symbolic filters are shareable; we re-invoke the named method in # worker Ractors. Lambda/block filters are unshareable and are left in # the (empty in workers) chain — they need a dedicated transport (e.g. # the dependent-association transport). Capturing ALL kinds (not just # :process_action) is what generalizes the transport to any callback. if filters.length >= 2 && filters[0].is_a?(Symbol) && self.is_a?(::Class) && ( (self.ancestors.include?(::AbstractController::Base) rescue false) || (defined?(::ActiveRecord::Base) && (self < ::ActiveRecord::Base)) ) kind = filters[0] filter = filters[1] if filter.is_a?(Symbol) opts = filters.find { |f| f.is_a?(::Hash) } only = nil except = nil if_cond = nil unless_cond = nil if opts [opts[:if], opts[:unless]].each do |arr| next unless arr.is_a?(::Array) arr.each do |af| ck, acts = ::RactorRailsShim::CallbackCapture.read_action_filter_constraints(af) next unless ck && acts only = acts if ck == :only except = acts if ck == :except end end # Capture Symbol if:/unless: conditions (e.g. Devise's # `after_update :send_email_changed_notification, # if: :send_email_changed_notification?`). These are # method-name Symbols that the transport can call on the # context to gate the callback — without this, the callback # fires unconditionally in worker Ractors. raw_if = opts[:if] raw_unless = opts[:unless] if_cond = raw_if if raw_if.is_a?(::Symbol) unless_cond = raw_unless if raw_unless.is_a?(::Symbol) end ::RactorRailsShim::CallbackCapture.record_declared_callback( self.object_id, name, kind, filter, only, except, if_cond, unless_cond) end end _rrs_orig_set_callback(name, *filters, &block) end RUBY @installed = true end |
.installed? ⇒ Boolean
Has install_callback_declaration_capture! run? Lives on CallbackCapture (Issue #24 — own your own state), NOT on the facade singleton.
75 76 77 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 75 def self.installed? @installed end |
.read_action_filter_constraints(af) ⇒ Object
Read @conditional_key and @actions off an ActionFilter instance (Rails internal ivars). Returns [conditional_key, actions_as_symbols]. On a Rails version where the ivars are renamed/absent, returns [nil, nil]. A missing ivar means callbacks run for actions they shouldn't (security-relevant). instance_variable_get returns nil for a missing ivar without raising, so we check instance_variable_defined? and emit a labeled warning via _swallow when debug=true so a silent Rails rename is visible during diagnosis.
227 228 229 230 231 232 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 227 def self.read_action_filter_constraints(af) ck = read_ivar_or_warn(af, :@conditional_key, "action filter constraints") acts = read_ivar_or_warn(af, :@actions, "action filter constraints") acts = acts.to_a.map(&:to_sym) if acts && acts.respond_to?(:to_a) [ck, acts] end |
.read_ivar_or_warn(obj, ivar, label) ⇒ Object
Read an ivar; if it's undefined, behavior is gated by the
VersionPolicy::Strategy (Issue #37 — the case policy branch is
replaced by a strategy-module message):
Strict — raise UnsupportedVersionError (a missing ivar means
callbacks run for actions they shouldn't; failing loud
pins the security-relevant failure mode instead of
silently mis-routing callbacks)
Warn — emit a labeled warning via funnel when debug? so a silent
Rails internal rename surfaces during diagnosis
Off — silent nil
Returns the ivar value or nil (under Warn/Off).
245 246 247 248 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 245 def self.read_ivar_or_warn(obj, ivar, label) return obj.instance_variable_get(ivar) if obj.instance_variable_defined?(ivar) RactorRailsShim::VersionPolicy.strategy.missing_ivar(obj, ivar, label, funnel: funnel) end |
.reassign_shareable_const ⇒ Object
The active reassign callable: the injected one if configured, else
the facade lookup (RactorRailsShim.method(:_reassign_shareable_const)).
92 93 94 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 92 def self.reassign_shareable_const @reassign_shareable_const || default_reassign_shareable_const end |
.record_declared_callback(klass_id, chain_kind, phase, filter, only, except, if_cond = nil, unless_cond = nil) ⇒ Object
Record a single declared symbolic filter. Called from the
set_callback interceptor during eager load (main Ractor only). chain_kind
is the ActiveSupport::Callbacks chain name (:process_action, :save,
:create, :destroy, …); phase is :before / :after. Storing chain_kind is
what generalizes replay beyond controllers to model lifecycle callbacks.
121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 121 def self.record_declared_callback(klass_id, chain_kind, phase, filter, only, except, if_cond = nil, unless_cond = nil) @declared_callbacks = {} unless defined?(@declared_callbacks) table = @declared_callbacks (table[klass_id] ||= []) << { chain_kind: chain_kind, phase: phase, filter: filter, only: (only.freeze if only), except: (except.freeze if except), if_cond: (if_cond.freeze if if_cond), unless_cond: (unless_cond.freeze if unless_cond) } end |
.register_patch ⇒ Object
The active register_patch callable: the injected one if configured,
else the facade lookup (RactorRailsShim.method(:_register_patch)).
98 99 100 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 98 def self.register_patch @register_patch || RactorRailsShim.method(:_register_patch) end |
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators. Test seam.
66 67 68 69 70 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 66 def self.reset_configuration @funnel = nil @reassign_shareable_const = nil @register_patch = nil end |
.reset_declared_callbacks! ⇒ Object
Clear the declared-callbacks table. Test seam.
136 137 138 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 136 def self.reset_declared_callbacks! remove_instance_variable(:@declared_callbacks) if instance_variable_defined?(:@declared_callbacks) end |
.reset_installed! ⇒ Object
Clear the installed flag. Test seam + reinstall seam.
80 81 82 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 80 def self.reset_installed! @installed = false end |