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, kind, filter, only, except) ⇒ 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.
139 140 141 142 143 144 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 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 139 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) if name == :process_action && filters.length >= 2 && filters[0].is_a?(Symbol) kind = filters[0] filter = filters[1] if filter.is_a?(Symbol) && self.is_a?(::Class) && self.ancestors.include?(::AbstractController::Base) # Rails converts `only:`/`except:` into an ActionFilter object # stored in the callback's `:if`/`:unless` options (NOT a bare # `:only` key). Read the constraint back from the ActionFilter's # @conditional_key (:only/:except) and @actions (a Set of action # name Strings) via the extracted, version-gated helper. opts = filters.find { |f| f.is_a?(Hash) } only = nil except = 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 end ::RactorRailsShim::CallbackCapture.record_declared_callback( self.object_id, kind, filter, only, except) 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.
203 204 205 206 207 208 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 203 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).
221 222 223 224 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 221 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, kind, filter, only, except) ⇒ Object
Record a single declared symbolic filter. Called from the set_callback interceptor during eager load (main Ractor only).
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 118 def self.record_declared_callback(klass_id, kind, filter, only, except) @declared_callbacks = {} unless defined?(@declared_callbacks) table = @declared_callbacks (table[klass_id] ||= []) << { kind: kind, filter: filter, only: (only.freeze if only), except: (except.freeze if except) } 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.
130 131 132 |
# File 'lib/ractor_rails_shim/roles/callback_capture.rb', line 130 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 |