ractor-rails-shim 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +101 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +265 -0
  5. data/exe/ractor-rails-check +39 -0
  6. data/lib/ractor_rails_shim/check.rb +190 -0
  7. data/lib/ractor_rails_shim/fallback_ies.rb +33 -0
  8. data/lib/ractor_rails_shim/patches/action_controller.rb +301 -0
  9. data/lib/ractor_rails_shim/patches/action_dispatch.rb +851 -0
  10. data/lib/ractor_rails_shim/patches/action_view.rb +582 -0
  11. data/lib/ractor_rails_shim/patches/active_model_attribute.rb +97 -0
  12. data/lib/ractor_rails_shim/patches/active_record_model_schema.rb +58 -0
  13. data/lib/ractor_rails_shim/patches/active_support.rb +1085 -0
  14. data/lib/ractor_rails_shim/patches/activerecord.rb +1727 -0
  15. data/lib/ractor_rails_shim/patches/class_attribute.rb +194 -0
  16. data/lib/ractor_rails_shim/patches/core.rb +806 -0
  17. data/lib/ractor_rails_shim/patches/devise.rb +172 -0
  18. data/lib/ractor_rails_shim/patches/execution_wrapper.rb +76 -0
  19. data/lib/ractor_rails_shim/patches/kaminari.rb +82 -0
  20. data/lib/ractor_rails_shim/patches/make_shareable.rb +853 -0
  21. data/lib/ractor_rails_shim/patches/mattr_accessor.rb +162 -0
  22. data/lib/ractor_rails_shim/patches/openssl.rb +58 -0
  23. data/lib/ractor_rails_shim/patches/orm_adapter.rb +32 -0
  24. data/lib/ractor_rails_shim/patches/polymorphic_routes.rb +115 -0
  25. data/lib/ractor_rails_shim/patches/propshaft.rb +110 -0
  26. data/lib/ractor_rails_shim/patches/rack.rb +160 -0
  27. data/lib/ractor_rails_shim/patches/rails_module.rb +192 -0
  28. data/lib/ractor_rails_shim/patches/route_helpers.rb +119 -0
  29. data/lib/ractor_rails_shim/patches/rubygems.rb +65 -0
  30. data/lib/ractor_rails_shim/patches/url_helpers.rb +78 -0
  31. data/lib/ractor_rails_shim/patches/warden.rb +195 -0
  32. data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +124 -0
  33. data/lib/ractor_rails_shim/patches.rb +67 -0
  34. data/lib/ractor_rails_shim/version.rb +5 -0
  35. data/lib/ractor_rails_shim/version_check.rb +88 -0
  36. data/lib/ractor_rails_shim.rb +33 -0
  37. metadata +104 -0
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rewrite ActiveSupport::ClassAttribute (used by `class_attribute`) so the
4
+ # reader/writer methods are defined via string eval instead of
5
+ # `define_method` with blocks. Blocks capture the defining Ractor's
6
+ # binding; calling them from a worker Ractor raises
7
+ # "defined with an un-shareable Proc in a different Ractor".
8
+ # `class_attribute` is used for Rails::Application#executor, #reloader,
9
+ # ActiveSupport::Reloader#executor/#check, and many framework globals —
10
+ # all read/written during app boot, which now runs in worker Ractors.
11
+ #
12
+ # Strategy: route the per-attribute storage (`__class_attr_<name>`) through
13
+ # IsolatedExecutionState, mirroring the mattr_accessor rewrite. Defaults
14
+ # are seeded once in the main Ractor at class_attribute-definition time
15
+ # (the original semantics). Worker Ractors get nil from the reader until
16
+ # they set their own value via the writer (which always works — the writer
17
+ # is string-eval'd, no captured binding). In practice workers boot their
18
+ # own app instance and the finisher sets executor/check/etc. during
19
+ # initialize!, so the default is only read as a fallback.
20
+
21
+ module RactorRailsShim
22
+ class << self
23
+ def install_class_attribute
24
+ _register_patch :class_attribute, "8.1"
25
+ return if @class_attr_patched
26
+ @class_attr_patched = true
27
+ if defined?(::ActiveSupport::ClassAttribute)
28
+ patch_class_attribute!
29
+ else
30
+ # Defer until ActiveSupport::ClassAttribute loads. A TracePoint(:class)
31
+ # fires when `module ClassAttribute` opens. One-shot.
32
+ @ca_tp = TracePoint.new(:class) do |trace|
33
+ if defined?(::ActiveSupport::ClassAttribute) && !@ca_patched
34
+ @ca_tp.disable
35
+ patch_class_attribute!
36
+ end
37
+ end
38
+ @ca_tp.enable
39
+ end
40
+ end
41
+
42
+ # The actual patch. Idempotent. Must run in the main Ractor.
43
+ # `redefine` is a singleton method on ClassAttribute (defined in
44
+ # `class << self`), so we prepend onto the singleton class.
45
+ def patch_class_attribute!
46
+ return if @ca_patched
47
+ @ca_patched = true
48
+ ::ActiveSupport::ClassAttribute.singleton_class.prepend(Module.new {
49
+ # redefine is called once per attribute at class_attribute-definition
50
+ # time (in the main Ractor). The original defines methods with blocks;
51
+ # we replace with string-eval'd methods that route through IES so
52
+ # they're callable from any Ractor. The default value is seeded into
53
+ # the main Ractor's IES slot immediately (matching original semantics
54
+ # where the reader returns the default until a subclass overrides).
55
+ def redefine(owner, name, namespaced_name, value)
56
+ key = :"ractor_rails_shim_class_attr_#{owner.object_id}_#{namespaced_name}"
57
+ key_str = key.inspect
58
+
59
+ # Seed the main Ractor's IES slot with the default. Only seed in
60
+ # main — workers start nil and set their own value via the writer.
61
+ ActiveSupport::IsolatedExecutionState[key] = value if Ractor.main?
62
+
63
+ # Also store in CLASS_ATTR_VALUES so the reader can fall back to it
64
+ # in the MAIN ractor on non-boot threads. IES is thread-local: Puma's
65
+ # request threads have empty IES slots, so the reader returns nil
66
+ # without this fallback. This is the bug that breaks normal (non-
67
+ # Ractor) multi-threaded servers — the minimal --minimal app didn't
68
+ # hit it because /up doesn't trigger LogSubscriber.log_levels.
69
+ # CLASS_ATTR_VALUES is NOT shareable (values may be mutable); only
70
+ # safe to read from the main ractor.
71
+ RactorRailsShim::CLASS_ATTR_VALUES[key] = value
72
+
73
+ # Register so _build_shareable_fallback! can capture + make shareable
74
+ # at prepare_for_ractors! time. owner.name may be nil for anonymous
75
+ # classes (e.g. spec fixtures); use a stable label in that case.
76
+ # The default value is stored too so the fallback builder can use it
77
+ # when the live value can't be made shareable (e.g. __callbacks holds
78
+ # self-capturing Procs — workers get the empty default, treating
79
+ # boot-time callbacks as already-run, which is correct for a frozen
80
+ # shared app).
81
+ owner_label = owner.respond_to?(:name) ? owner.name : owner.class.name
82
+ owner_label = owner_label || "anon_#{owner.class.name}_#{owner.object_id}"
83
+ RactorRailsShim::CLASS_ATTRIBUTES << [owner_label, namespaced_name, key, value]
84
+
85
+ # Always define the namespaced reader/writer on owner's singleton
86
+ # class via string eval (no captured binding). The class_attribute
87
+ # macro itself also defines `def #{name}; #{namespaced_name}; end`
88
+ # via class_eval (string-eval'd, safe) on the owner — that calls our
89
+ # IES-routed namespaced reader/writer. We override BOTH the namespaced
90
+ # and (when owner is a module's singleton) the public name.
91
+ #
92
+ # Worker-Ractor fallback: when the worker's own IES slot is empty
93
+ # (which it is by default — the value lives in main's slot), fall
94
+ # back to the frozen shareable table built at prepare_for_ractors!
95
+ # time. This is read-only and shared across all workers; workers that
96
+ # need their own mutable value call the writer, which writes their
97
+ # IES slot and shadows the fallback.
98
+ target = owner.singleton_class? ? owner : owner.singleton_class
99
+ if RactorRailsShim.thread_mode?
100
+ # Thread (Puma/Falcon) mode: route through a SHARED (process-wide)
101
+ # store keyed by the actual class's object_id, walking ancestors
102
+ # for copy-on-write fallback. This restores per-subclass isolation
103
+ # (lost by the IES-routed variant) without thread-local IES, which
104
+ # is empty on Puma's request threads.
105
+ target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
106
+ def #{namespaced_name}
107
+ self.ancestors.each do |anc|
108
+ v = RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"]
109
+ return v unless v.nil?
110
+ end
111
+ #{namespaced_name.inspect} == :__callbacks ? {} : nil
112
+ end
113
+
114
+ def #{namespaced_name}=(new_value)
115
+ RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{self.object_id}_#{namespaced_name}"] = new_value
116
+ new_value
117
+ end
118
+ RUBY
119
+
120
+ # When owner is a module's singleton class, also override the
121
+ # public reader `def #{name}` with the shared-store version.
122
+ if owner.singleton_class? && owner.attached_object.is_a?(Module)
123
+ owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
124
+ def #{name}
125
+ self.ancestors.each do |anc|
126
+ v = RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"]
127
+ return v unless v.nil?
128
+ end
129
+ #{namespaced_name.inspect} == :__callbacks ? {} : nil
130
+ end
131
+
132
+ def #{name}=(new_value)
133
+ RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{self.object_id}_#{namespaced_name}"] = new_value
134
+ new_value
135
+ end
136
+ RUBY
137
+ end
138
+ else
139
+ target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
140
+ def #{namespaced_name}
141
+ self.ancestors.each do |anc|
142
+ k = :"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"
143
+ v = ActiveSupport::IsolatedExecutionState[k]
144
+ return v unless v.nil?
145
+ fb = RactorRailsShim::SHAREABLE_FALLBACK[k]
146
+ return fb unless fb.nil?
147
+ end
148
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
149
+ end
150
+
151
+ def #{namespaced_name}=(new_value)
152
+ ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
153
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
154
+ new_value
155
+ end
156
+ RUBY
157
+
158
+ # When owner is a module's singleton class, the original also
159
+ # defines a public reader `def #{name} { value }` on owner directly
160
+ # (block-based). Override it with the IES-routed version + fallback.
161
+ if owner.singleton_class? && owner.attached_object.is_a?(Module)
162
+ owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
163
+ def #{name}
164
+ self.ancestors.each do |anc|
165
+ k = :"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"
166
+ v = ActiveSupport::IsolatedExecutionState[k]
167
+ return v unless v.nil?
168
+ fb = RactorRailsShim::SHAREABLE_FALLBACK[k]
169
+ return fb unless fb.nil?
170
+ end
171
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
172
+ end
173
+
174
+ def #{name}=(new_value)
175
+ ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
176
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
177
+ new_value
178
+ end
179
+ RUBY
180
+ end
181
+ end
182
+ end
183
+
184
+ # redefine_method is used by `redefine` internally and by other call
185
+ # sites (rare). The class_attribute path goes through our `redefine`
186
+ # above; keep the original block-based behavior for any other callers
187
+ # so we don't break unrelated code.
188
+ def redefine_method(owner, name, private: false, &block)
189
+ super
190
+ end
191
+ })
192
+ end
193
+ end
194
+ end