Module: RactorRailsShim::ConstantShareabilizer

Extended by:
RoleDefaults
Defined in:
lib/ractor_rails_shim/roles/constant_shareabilizer.rb

Class Method Summary collapse

Methods included from RoleDefaults

default_funnel, default_reassign_shareable_const, default_safe_const_get

Class Method Details

.applied?Boolean

Has apply! fully resolved? Lives on ConstantShareabilizer (Issue #24 — own your own state), NOT on the facade singleton.

Returns:

  • (Boolean)


70
71
72
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 70

def self.applied?
  @applied
end

.apply!Object

Run after Rails is fully booted (after Rails.application.initialize!) and BEFORE spawning worker Ractors. Re-attempts to make every registered constant shareable; constants that didn't exist at install time (e.g. Rails::Railtie, loaded after module Rails opens) get fixed here. Safe to call multiple times; already-shareable constants are no-ops. MUST run in the main Ractor (const_set writes the constant table). Public wrapper is prepare_for_ractors! on the facade.



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 126

def self.apply!
  return if @applied
  # Only set the done flag when every registered constant was made
  # shareable (or already was). If any returned false (constant doesn't
  # exist yet), leave the flag unset so a later call (from
  # make_app_shareable! or prepare_for_ractors!) retries the now-loadable
  # constants — otherwise workers hit IsolationError on the unshareable
  # values that were missed on the first pass.
  all_resolved = shareable_constants.map { |path| make_shareable!(path) }.all?
  @applied = true if all_resolved
end

.configure(funnel: nil, register_patch: nil, introspectable: nil, noop_lock_class: nil, shareable_constants_registry: nil) ⇒ Object

Inject the callable/class collaborators. funnel responds to call(label) { block } (runs the block, rescues StandardError — matches _swallow). register_patch responds to call(name, ver). introspectable responds to call(val) returning truthy/nil (matches _introspectable?). noop_lock_class responds to .new (matches NoOpLock). shareable_constants_registry is the array of constant path strings. Passing nil for any (or calling reset_configuration) restores the facade-lookup default.



49
50
51
52
53
54
55
56
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 49

def self.configure(funnel: nil, register_patch: nil, introspectable: nil,
                   noop_lock_class: nil, shareable_constants_registry: nil)
  @funnel = funnel
  @register_patch = register_patch
  @introspectable = introspectable
  @noop_lock_class = noop_lock_class
  @shareable_constants_registry = shareable_constants_registry
end

.funnelObject



79
80
81
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 79

def self.funnel
  @funnel || default_funnel
end

.installObject

Register the patch + apply it now if ActiveSupport is loaded. Called at install time; if ActiveSupport isn't loaded yet, the constants don't exist, so we re-run from patch_rails_module! (which fires once Rails — and thus ActiveSupport — is defined). Guarded by



111
112
113
114
115
116
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 111

def self.install
  register_patch.call(:shareable_constants, "8.1")
  return unless defined?(::ActiveSupport)

  apply!
end

.introspectableObject



87
88
89
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 87

def self.introspectable
  @introspectable || RactorRailsShim::ShareabilityTraversal.method(:introspectable?)
end

.make_shareable!(const_path) ⇒ Object

Resolve a constant path string to a value, and if it exists and is not already shareable, replace it with its shareable (deep-frozen) version. Returns true if the constant was made shareable (or already was); false if it doesn't exist yet (caller may retry).



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
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 142

def self.make_shareable!(const_path)
  owner, name = split_const_path(const_path)
  return false unless owner && name
  return true if owner.const_defined?(name, false) == false

  val = owner.const_get(name, false)
  return true if Ractor.shareable?(val)

  shareable = make_value_shareable(val)
  return true unless shareable

  # Deep-freeze and reassign. Ractor.make_shareable mutates `val` in
  # place (freezing it and its reachable objects) and returns it.
  # const_set warns "already initialized constant" because Rails'
  # environment_inquirer.rb defined the constant first. The reassign is
  # intentional (we're replacing the mutable value with its frozen
  # shareable twin), so silence that one warning.
  verbose, $VERBOSE = $VERBOSE, nil
  begin
    owner.const_set(name, shareable)
  ensure
    $VERBOSE = verbose
  end
  true
end

.make_value_shareable(val) ⇒ Object

Best-effort shareable replacement for a constant value. Any object that responds to :synchronize (the duck type for a Mutex-like lock — covers Monitor, Mutex, and third-party lock classes alike) becomes a NoOpLock (used as sentinel sentinels, e.g. PRIMARY_KEY_NOT_SET) can't be frozen (BasicObject has no #freeze method) — replace with a frozen Symbol. Everything else is deep-frozen via Ractor.make_shareable; if that fails (e.g. a Proc, or a Concurrent::Map / TypeMap holding Procs — both intrinsically unshareable and needing upstream Rails changes), returns nil and the constant is left as-is (the worker will raise a clear IsolationError on read).

Uses the existing _introspectable? helper (make_shareable.rb) instead of ad-hoc rescue false guards. BasicObject subclasses don't define is_a?/respond_to? (Kernel not included); _introspectable? safely detects this via a guarded respond_to?(:is_a?) check.



183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 183

def self.make_value_shareable(val)
  if introspectable.call(val) && val.respond_to?(:synchronize)
    Ractor.make_shareable(noop_lock_class.new)
  elsif !introspectable.call(val) || !val.respond_to?(:freeze)
    # Non-introspectable (BasicObject without is_a?) OR lacks #freeze
    # (BasicObject subclasses). Replace with a frozen Symbol sentinel —
    # it's compared with `equal?`, and a frozen Symbol is always
    # shareable.
    Ractor.make_shareable(:"__shim_unshareable_sentinel__")
  else
    funnel.call("make_value_shareable #{val.class}") { Ractor.make_shareable(val) }
  end
end

.noop_lock_classObject



91
92
93
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 91

def self.noop_lock_class
  @noop_lock_class || RactorRailsShim.singleton_class.const_get(:NoOpLock)
end

.register_patchObject



83
84
85
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 83

def self.register_patch
  @register_patch || RactorRailsShim.method(:_register_patch)
end

.reset_applied!Object

Clear the applied flag. Test seam + reinstall seam.



75
76
77
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 75

def self.reset_applied!
  @applied = false
end

.reset_configurationObject

Restore the default (facade-lookup) collaborators. Test seam.



59
60
61
62
63
64
65
66
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 59

def self.reset_configuration
@applied = false
@funnel = nil
  @register_patch = nil
  @introspectable = nil
  @noop_lock_class = nil
  @shareable_constants_registry = nil
end

.safe_const_get(path, inherit: true) ⇒ Object

Resolve a constant path string (e.g. "A::B::C") to its value, returning nil if any segment isn't defined. When inherit is false, each segment is looked up only in its parent's own constant table (const_get name, false), matching the original no-inherit lookups in _freeze_messages_constants!. Replaces dense rescue/& chains like:

(Object.const_get(:A) rescue nil)&.const_get(:B, false) rescue nil


203
204
205
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 203

def self.safe_const_get(path, inherit: true)
  path.split("::").inject(Object) { |ns, n| ns.const_get(n, inherit) } rescue nil
end

.shareable_constantsObject

The registry of constant path strings whose values are made shareable at boot. Lives on RactorRailsShim (per-concern files concat into it); this reader delegates so call sites don't reach past the role object.



102
103
104
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 102

def self.shareable_constants
  shareable_constants_registry
end

.shareable_constants_registryObject



95
96
97
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 95

def self.shareable_constants_registry
  @shareable_constants_registry || RactorRailsShim::Registry.shareable_constants
end

.split_const_path(path) ⇒ Object

Split "A::B::C" into [A::B (module), :C]. Returns [nil, nil] if the parent isn't defined.



209
210
211
212
213
214
215
# File 'lib/ractor_rails_shim/roles/constant_shareabilizer.rb', line 209

def self.split_const_path(path)
  parts = path.split("::")
  return [Object, parts.first.to_sym] if parts.size == 1
  parent = safe_const_get(parts[0...-1].join("::"))
  return [nil, nil] unless parent
  [parent, parts.last.to_sym]
end