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

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