Module: RactorRailsShim::ActiveRecordStorePatch
- Defined in:
- lib/ractor_rails_shim/patches/active_record_store.rb
Overview
ActiveRecord::Store generates its accessors and store-coder resolution with
un-shareable blocks compiled in the main Ractor. A worker Ractor that writes a
store accessor (e.g. ActiveStorage::Blob#identified=) raises "defined with an
un-shareable Proc in a different Ractor".
We re-implement the two offending methods with block-free equivalents:
* `store_accessor` (ClassMethods) — emits the accessor method bodies via
string-eval `def`s instead of `define_method` blocks.
* `store_accessor_for` (Store module instance method) — resolves the coder
without the `tap do |type| ... end` block.
Both must be installed BEFORE any model that uses store (e.g.
ActiveStorage::Blob) is loaded, so accessors are generated in shareable form
from the start.
Instance Method Summary collapse
-
#store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) ⇒ Object
ActiveRecord::Store::ClassMethods#store_accessorgenerates each store accessor (getter, setter,_changed?,_change,_was,saved_change_to_*,_before_last_save) viadefine_methodinside amodule_evalblock.
Instance Method Details
#store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) ⇒ Object
ActiveRecord::Store::ClassMethods#store_accessor generates each store
accessor (getter, setter, _changed?, _change, _was, saved_change_to_*,
_before_last_save) via define_method inside a module_eval block. Those
define_method blocks are compiled in the main Ractor, so any worker Ractor
that calls a store accessor (e.g. ActiveStorage::Blob#identified=) blows
up with "defined with an un-shareable Proc in a different Ractor".
We re-implement store_accessor to emit the same method bodies with
string-eval defs (no closures / no blocks), which are Ractor-shareable.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/ractor_rails_shim/patches/active_record_store.rb', line 28 def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) keys = keys.flatten accessor_prefix = case prefix when String, Symbol "#{prefix}_" when TrueClass "#{store_attribute}_" else "" end accessor_suffix = case suffix when String, Symbol "_#{suffix}" when TrueClass "_#{store_attribute}" else "" end mod = _store_accessors_module sa = store_attribute.inspect mod.class_eval do keys.each do |key| accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}" key_lit = key.inspect class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{accessor_key}=(value) write_store_attribute(#{sa}, #{key_lit}, value) end def #{accessor_key} read_store_attribute(#{sa}, #{key_lit}) end def #{accessor_key}_changed? return false unless attribute_changed?(#{sa}) prev_store, new_store = changes[#{sa}] accessor = store_accessor_for(#{sa}) accessor.get(prev_store, #{key_lit}) != accessor.get(new_store, #{key_lit}) end def #{accessor_key}_change return unless attribute_changed?(#{sa}) prev_store, new_store = changes[#{sa}] accessor = store_accessor_for(#{sa}) [accessor.get(prev_store, #{key_lit}), accessor.get(new_store, #{key_lit})] end def #{accessor_key}_was return unless attribute_changed?(#{sa}) prev_store, _new_store = changes[#{sa}] accessor = store_accessor_for(#{sa}) accessor.get(prev_store, #{key_lit}) end def saved_change_to_#{accessor_key}? return false unless saved_change_to_attribute?(#{sa}) prev_store, new_store = saved_changes[#{sa}] accessor = store_accessor_for(#{sa}) accessor.get(prev_store, #{key_lit}) != accessor.get(new_store, #{key_lit}) end def saved_change_to_#{accessor_key} return unless saved_change_to_attribute?(#{sa}) prev_store, new_store = saved_changes[#{sa}] accessor = store_accessor_for(#{sa}) [accessor.get(prev_store, #{key_lit}), accessor.get(new_store, #{key_lit})] end def #{accessor_key}_before_last_save return unless saved_change_to_attribute?(#{sa}) prev_store, _new_store = saved_changes[#{sa}] accessor = store_accessor_for(#{sa}) accessor.get(prev_store, #{key_lit}) end RUBY end end self.local_stored_attributes ||= {} self.local_stored_attributes[store_attribute] ||= [] self.local_stored_attributes[store_attribute] |= keys end |