Module: RactorRailsShim::StorageStrategy::Ractor

Defined in:
lib/ractor_rails_shim/foundation/storage_strategy.rb

Overview

Rector-mode strategy. Two reader/writer entry points:

* `lookup` / `store`        — EXACT-key 3-tier passthrough. Used by
                           IESAccessor (and anything that hands over
                           a fully-qualified storage key). No ancestor
                           walk; the caller owns the key.
* `lookup_by_attr` / `store_by_attr` — per-receiver class_attribute
                           semantics: derive a storage key from the
                           receiver's class + the attribute name, then
                           walk the ancestor chain so each class keeps
                           its OWN class_attribute value (siblings no
                           longer clobber each other). Backed by the
                           Ractor backends (IES + SHAREABLE_FALLBACK
                           + CLASS_ATTR_VALUES[main]).

Class Method Summary collapse

Class Method Details

.lookup(owner, key, missing_default) ⇒ Object

Exact-key 3-tier passthrough (IES → SHAREABLE_FALLBACK → CLASS_ATTR_VALUES). Contract: key IS the storage key.



78
79
80
81
82
83
84
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 78

def lookup(owner, key, missing_default)
  v = RactorRailsShim.storage[key]
  return v if RactorRailsShim.storage.key?(key)
  fb = RactorRailsShim::Registry.shareable_fallback[key]
  return fb unless fb.nil?
  RactorRailsShim::Registry.class_attr_values[key] if ::Ractor.main?
end

.lookup_by_attr(owner, attr_sym, missing_default) ⇒ Object

Per-receiver class_attribute lookup: derive the receiver's storage key (memoized via StorageStrategy.ca_key) and walk ancestors so subclasses inherit parent values while keeping their own overrides. ancestors (not superclass) is required so module-declared class_attributes (e.g. AbstractController::Callbacks#__callbacks) are reachable — superclass skips included modules.

Per-ancestor tier order:

1. `storage` (IES)        — current execution context's override
                          (workers set their own value here).
2. `shareable_fallback`   — frozen shareable table built at
                          prepare_for_ractors! (worker reads).
3. `class_attr_values`    — the persistent seed registry (main
                          only). IES is execution-context
                          ISOLATED, so a seed written during
                          boot is invisible in a later context;
                          `class_attr_values` is a plain Hash
                          that survives across contexts, so it
                          MUST be consulted per-ancestor (not
                          just for the final receiver) — otherwise
                          inherited values (e.g. a parent's
                          `__callbacks`) are missed.


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 114

def lookup_by_attr(owner, attr_sym, missing_default)
  klass = owner.is_a?(Module) ? owner : owner.class
  klass.ancestors.each do |anc|
    kkey = RactorRailsShim::StorageStrategy.ca_key(anc, attr_sym)
    v = RactorRailsShim.storage[kkey]
    return v if RactorRailsShim.storage.key?(kkey)
    fb = RactorRailsShim::Registry.shareable_fallback[kkey]
    return fb unless fb.nil?
    if ::Ractor.main?
      cv = RactorRailsShim::Registry.class_attr_values[kkey]
      return cv unless cv.nil?
    end
  end
  missing_default
end

.lookup_resolved(owner, attr_sym, missing_default, resolved_key) ⇒ Object

Zero-allocation hot reader. resolved_key is a literal symbol baked into the generated reader method (see _class_attr_methods). The first read for a receiver resolves via lookup_by_attr (ancestor walk) and caches the result in ractor-local IES keyed by receiver object_id; every subsequent read is a literal-key Hash lookup + integer index — no Array/ancestors allocation.



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 143

def lookup_resolved(owner, attr_sym, missing_default, resolved_key)
  klass = owner.is_a?(Module) ? owner : owner.class
  oid = klass.object_id
  cache = RactorRailsShim.storage[resolved_key]
  return cache[oid] if cache && cache.key?(oid)
  v = lookup_by_attr(owner, attr_sym, missing_default)
  cache = RactorRailsShim.storage[resolved_key]
  cache ||= (RactorRailsShim.storage[resolved_key] = {})
  cache[oid] = v
  v
end

.replay_callbacks!(context, kind, &block) ⇒ Object

Shared callback-replay logic used by both the "always" path (Thread mode) and the "on-empty" path (Ractor mode). Walks the SHAREABLE_DECLARED_CALLBACKS table and replays captured symbolic filters (before/after) that apply to the current action. Delegate callback replay to the generalized transport layer (RactorRailsShim::Callbacks::Registry). The registry owns the single yield and dispatches to every applicable transport (the symbolic filter transport for any kind + the dependent-association transport for :destroy). Kept as a thin delegator so the storage-strategy contract (replay_callbacks? / replay_callbacks!) stays intact and the Thread strategy can keep mirroring it. The run_callbacks nil-safe patch calls the registry directly, but external callers/tests may still reach the strategy.



190
191
192
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 190

def replay_callbacks!(context, kind, &block)
  ::RactorRailsShim::Callbacks.registry.replay(context, kind, &block)
end

.replay_callbacks?(callbacks) ⇒ Boolean

Ractor mode: replay captured callbacks only when __callbacks is empty (the worker-Ractor case — workers get the empty default). In the main Ractor, __callbacks is live and replay is skipped.

Returns:

  • (Boolean)


173
174
175
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 173

def replay_callbacks?(callbacks)
  callbacks.nil? || callbacks.empty?
end

.store(owner, key, value) ⇒ Object



86
87
88
89
90
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 86

def store(owner, key, value)
  RactorRailsShim.storage[key] = value
  RactorRailsShim::Registry.class_attr_values[key] = value if ::Ractor.main?
  value
end

.store_by_attr(owner, attr_sym, value) ⇒ Object



130
131
132
133
134
135
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 130

def store_by_attr(owner, attr_sym, value)
  kkey = RactorRailsShim::StorageStrategy.ca_key(owner, attr_sym)
  RactorRailsShim.storage[kkey] = value
  RactorRailsShim::Registry.class_attr_values[kkey] = value if ::Ractor.main?
  value
end

.store_resolved(owner, attr_sym, value, resolved_key) ⇒ Object

Zero-allocation hot writer. Writes the per-receiver slot (so the ancestor-walk lookup_by_attr stays correct) AND updates the resolved cache for this receiver so the next read is hot.



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 158

def store_resolved(owner, attr_sym, value, resolved_key)
  kkey = RactorRailsShim::StorageStrategy.ca_key(owner, attr_sym)
  RactorRailsShim.storage[kkey] = value
  RactorRailsShim::Registry.class_attr_values[kkey] = value if ::Ractor.main?
  klass = owner.is_a?(Module) ? owner : owner.class
  oid = klass.object_id
  cache = RactorRailsShim.storage[resolved_key]
  cache ||= (RactorRailsShim.storage[resolved_key] = {})
  cache[oid] = value
  value
end