Module: RactorRailsShim::FallbackBuilder
- Extended by:
- RoleDefaults
- Defined in:
- lib/ractor_rails_shim/roles/fallback_builder.rb
Defined Under Namespace
Modules: ShareableCopy, ValueLookup
Class Method Summary collapse
-
.build! ⇒ Object
Build the shareable fallback for every class_attribute / mattr_accessor value the shim has rerouted.
-
.built? ⇒ Boolean
Has build! run? Lives on FallbackBuilder (Issue #24 — own your own state), NOT on the facade singleton.
- .class_attr_values ⇒ Object
- .class_attributes ⇒ Object
-
.configure(safe_const_get: nil, replace_unshareable_procs: nil, replace_locks_and_concurrent_maps: nil, reassign_shareable_const: nil, class_attributes: nil, class_attr_values: nil, shareable_mattr_defaults: nil, storage: nil, value_lookup_chain: nil) ⇒ Object
Inject the callable + registry collaborators.
- .reassign_shareable_const ⇒ Object
- .replace_locks_and_concurrent_maps ⇒ Object
- .replace_unshareable_procs ⇒ Object
-
.reset_built! ⇒ Object
Clear the built flag.
-
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators.
- .safe_const_get ⇒ Object
-
.shareable_copy(val) ⇒ Object
Return a fresh copy of a mutable default container (Hash/Array) so the fallback entry is independent.
- .shareable_mattr_defaults ⇒ Object
- .storage ⇒ Object
-
.try_make_shareable(val, owner_name, attr_name, default: false) ⇒ Object
Best-effort attempt to make
valshareable (callable-replacement for Procs + lock-replacement + make_shareable). - .value_lookup_chain ⇒ Object
Methods included from RoleDefaults
default_funnel, default_reassign_shareable_const, default_safe_const_get
Class Method Details
.build! ⇒ Object
Build the shareable fallback for every class_attribute / mattr_accessor value the shim has rerouted. For each registered attribute we:
1. Read the main-ractor value (from its IES slot, which `redefine`
seeded at class_attribute-definition time).
2. Make it shareable (deep-freeze + callable-replacement for any Procs
it holds — same technique as make_app_shareable!, applied to the
config sub-graph).
3. Store under the IES key in a frozen Hash on RactorRailsShim, which
is readable from every Ractor (it's a constant).
Workers' class_attribute readers fall back to this when their own IES slot is nil. Must run in the main Ractor. Idempotent (guarded by
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 220 def self.build! return nil if @built @built = true fallback = {} chain = value_lookup_chain lookup_ctx = { storage: storage, class_attr_values: class_attr_values, main: ::Ractor.main?, safe_const_get: safe_const_get, } class_attributes.each do |(owner_name, attr_name, ies_key, default_val)| # Skip the Rails logger — it's intrinsically unshareable (IO + Mutex + # mutable formatter) and workers build their own per-Ractor logger # via the patched reader. Trying to make it shareable would freeze the # IO, breaking logging in main too. next if owner_name == "Rails" && attr_name == :logger # Walk the ValueLookup chain to resolve the value. The first # non-nil result wins; if all links return nil, val stays nil. val = nil chain.each do |link| val = link.lookup(owner_name, attr_name, ies_key, **lookup_ctx) break unless val.nil? end shareable_val = nil # Try the live value first. if !val.nil? shareable_val = try_make_shareable(val, owner_name, attr_name) end # If the live value couldn't be shared (e.g. __callbacks holds # self-capturing Procs), fall back to the definition-time default. # For a frozen shared app this is correct: boot-time callbacks # already ran in main; workers treat them as already-run # (empty/no-op). The default is dup'd if it's a mutable container # (Hash/Array) so each entry in the fallback is an independent # shareable copy. if shareable_val.nil? && !default_val.nil? shareable_val = try_make_shareable(shareable_copy(default_val), owner_name, attr_name, default: true) end fallback[ies_key] = shareable_val if shareable_val end # The class_attributes loop above only seeds the declaring-module's # value (keyed by its per-owner key). Per-subclass overrides written # through the class_attribute writer land in CLASS_ATTR_VALUES under # their OWN per-owner keys (e.g. Api::PostsController's # `mimes_for_respond_to = {json: {}}`). Workers resolve values via an # ancestor walk keyed by each class's per-owner key, so the frozen # SHAREABLE_FALLBACK must contain those per-subclass entries too. class_attr_values.each do |kkey, val| next if kkey.to_s.include?("logger") attr_name = kkey.to_s.sub(/\Aractor_rails_shim_class_attr_\d+_/, "").to_sym shareable_val = try_make_shareable(val, nil, attr_name) fallback[kkey] = shareable_val if shareable_val end fallback.freeze Ractor.make_shareable(fallback) # Make the shareable mattr-defaults subset shareable too (workers # read it via the constant). Frozen + reassigned via the centralized # helper. shareable_mattr_defaults.freeze Ractor.make_shareable(shareable_mattr_defaults) reassign_shareable_const.call(:SHAREABLE_FALLBACK, fallback) reassign_shareable_const.call(:SHAREABLE_MATTR_DEFAULTS, shareable_mattr_defaults) fallback end |
.built? ⇒ Boolean
Has build! run? Lives on FallbackBuilder (Issue #24 — own your own state), NOT on the facade singleton.
199 200 201 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 199 def self.built? @built end |
.class_attr_values ⇒ Object
106 107 108 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 106 def self.class_attr_values @class_attr_values || RactorRailsShim::Registry.class_attr_values end |
.class_attributes ⇒ Object
102 103 104 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 102 def self.class_attributes @class_attributes || RactorRailsShim::Registry.class_attributes end |
.configure(safe_const_get: nil, replace_unshareable_procs: nil, replace_locks_and_concurrent_maps: nil, reassign_shareable_const: nil, class_attributes: nil, class_attr_values: nil, shareable_mattr_defaults: nil, storage: nil, value_lookup_chain: nil) ⇒ Object
Inject the callable + registry collaborators. The callables:
safe_const_get(path) → value|nil; replace_unshareable_procs(val)
and replace_locks_and_concurrent_maps(val) mutate val in place;
reassign_shareable_const(sym, value) reassigns the shareable
constant. The registries: class_attributes (CLASS_ATTRIBUTES
array), class_attr_values (CLASS_ATTR_VALUES store),
shareable_mattr_defaults (SHAREABLE_MATTR_DEFAULTS array),
storage (IES storage, hash-like: storage). The lookup
chain: value_lookup_chain (Array of lookup modules, each
responding to .lookup(owner, attr_name, ies_key, **ctx) -> val|nil).
Passing nil for any (or calling reset_configuration) restores
the facade-lookup default for that collaborator.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 57 def self.configure(safe_const_get: nil, replace_unshareable_procs: nil, replace_locks_and_concurrent_maps: nil, reassign_shareable_const: nil, class_attributes: nil, class_attr_values: nil, shareable_mattr_defaults: nil, storage: nil, value_lookup_chain: nil) @safe_const_get = safe_const_get @replace_unshareable_procs = replace_unshareable_procs @replace_locks_and_concurrent_maps = replace_locks_and_concurrent_maps @reassign_shareable_const = reassign_shareable_const @class_attributes = class_attributes @class_attr_values = class_attr_values @shareable_mattr_defaults = shareable_mattr_defaults @storage = storage @value_lookup_chain = value_lookup_chain end |
.reassign_shareable_const ⇒ Object
98 99 100 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 98 def self.reassign_shareable_const @reassign_shareable_const || RactorRailsShim.method(:_reassign_shareable_const) end |
.replace_locks_and_concurrent_maps ⇒ Object
94 95 96 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 94 def self.replace_locks_and_concurrent_maps @replace_locks_and_concurrent_maps || RactorRailsShim::ShareabilityTraversal.method(:replace_locks_and_concurrent_maps!) end |
.replace_unshareable_procs ⇒ Object
90 91 92 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 90 def self.replace_unshareable_procs @replace_unshareable_procs || RactorRailsShim::ShareabilityTraversal.method(:replace_unshareable_procs!) end |
.reset_built! ⇒ Object
Clear the built flag. Test seam + reinstall seam.
204 205 206 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 204 def self.reset_built! @built = false end |
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators. Test seam.
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 74 def self.reset_configuration @safe_const_get = nil @replace_unshareable_procs = nil @replace_locks_and_concurrent_maps = nil @reassign_shareable_const = nil @class_attributes = nil @class_attr_values = nil @shareable_mattr_defaults = nil @storage = nil @value_lookup_chain = nil end |
.safe_const_get ⇒ Object
86 87 88 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 86 def self.safe_const_get @safe_const_get || default_safe_const_get end |
.shareable_copy(val) ⇒ Object
Return a fresh copy of a mutable default container (Hash/Array) so the fallback entry is independent. Frozen/shareable defaults pass through. Uses the ShareableCopy dispatch table (Issue #40, POODR §5b).
347 348 349 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 347 def self.shareable_copy(val) ShareableCopy.call(val) end |
.shareable_mattr_defaults ⇒ Object
110 111 112 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 110 def self.shareable_mattr_defaults @shareable_mattr_defaults || RactorRailsShim::Registry.shareable_mattr_defaults end |
.storage ⇒ Object
114 115 116 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 114 def self.storage @storage || RactorRailsShim.storage end |
.try_make_shareable(val, owner_name, attr_name, default: false) ⇒ Object
Best-effort attempt to make val shareable (callable-replacement for
Procs + lock-replacement + make_shareable). Returns the shareable val,
or nil if it can't be made shareable. On failure, emits a warning
(unless default: — defaults are expected to sometimes be unshareable,
so we skip the noise).
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 299 def self.try_make_shareable(val, owner_name, attr_name, default: false) # __callbacks and validators hold callback chains / validator # instances with self-capturing Procs that can NEVER be made # shareable. This is expected: workers correctly treat callbacks as # already-run (the nil-safe run_callbacks patch yields the block # directly). Skip the attempt entirely — don't waste cycles traversing # the graph, and don't emit warnings for known-expected failures. attr_sym = attr_name.to_s return nil if attr_sym.end_with?("__callbacks") || attr_sym.end_with?("__validators") || attr_sym.end_with?("default_connection_handler") || attr_sym.include?("default_connection_handler") || attr_sym.end_with?("__class_attr__queue_adapter") # Try a plain deep-freeze FIRST. Several class attributes (notably # ActiveRecord's `_reflections`, a Hash of Reflection objects) ARE # genuinely shareable as-is — their Procs are self-contained — but the # proc/lock-replacement traversal that runs first in the old order # mutates the value in a way that then defeats Ractor.make_shareable, # so the attribute fell back to nil in workers and broke associations # (`_reflect_on_association` -> `nil[]`). A plain make_shareable # succeeds for these. Only fall back to the (slower) replacement # traversal when the plain attempt raises — that path is still required # for values whose Procs capture unshareable state (it swaps them for # shareable callables). Either order produces a correct shareable # value; plain-first just avoids the spurious failure. begin Ractor.make_shareable(val) val rescue StandardError => e begin replace_unshareable_procs.call(val) replace_locks_and_concurrent_maps.call(val) Ractor.make_shareable(val) val rescue StandardError => e2 unless default warn "ractor-rails-shim: could not make attribute " \ "#{owner_name}##{attr_name} shareable (#{e2.class}: #{e2.[0,80]}); workers will fall back to default or nil" end nil end end end |
.value_lookup_chain ⇒ Object
118 119 120 |
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 118 def self.value_lookup_chain @value_lookup_chain || ValueLookup::DEFAULT_CHAIN end |