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

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
# 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
  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.

Returns:

  • (Boolean)


199
200
201
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 199

def self.built?
  @built
end

.class_attr_valuesObject



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_attributesObject



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_constObject



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_mapsObject



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_procsObject



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_configurationObject

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_getObject



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).



313
314
315
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 313

def self.shareable_copy(val)
  ShareableCopy.call(val)
end

.shareable_mattr_defaultsObject



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

.storageObject



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).



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ractor_rails_shim/roles/fallback_builder.rb', line 284

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")

  begin
    replace_unshareable_procs.call(val)
    replace_locks_and_concurrent_maps.call(val)
    Ractor.make_shareable(val)
    val
  rescue StandardError => e
    unless default
      warn "ractor-rails-shim: could not make attribute " \
           "#{owner_name}##{attr_name} shareable (#{e.class}: #{e.message[0,80]}); workers will fall back to default or nil"
    end
    nil
  end
end

.value_lookup_chainObject



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