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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/exe/ractor-rails-check +39 -0
- data/lib/ractor_rails_shim/check.rb +190 -0
- data/lib/ractor_rails_shim/fallback_ies.rb +33 -0
- data/lib/ractor_rails_shim/patches/action_controller.rb +301 -0
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +851 -0
- data/lib/ractor_rails_shim/patches/action_view.rb +582 -0
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +97 -0
- data/lib/ractor_rails_shim/patches/active_record_model_schema.rb +58 -0
- data/lib/ractor_rails_shim/patches/active_support.rb +1085 -0
- data/lib/ractor_rails_shim/patches/activerecord.rb +1727 -0
- data/lib/ractor_rails_shim/patches/class_attribute.rb +194 -0
- data/lib/ractor_rails_shim/patches/core.rb +806 -0
- data/lib/ractor_rails_shim/patches/devise.rb +172 -0
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +76 -0
- data/lib/ractor_rails_shim/patches/kaminari.rb +82 -0
- data/lib/ractor_rails_shim/patches/make_shareable.rb +853 -0
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +162 -0
- data/lib/ractor_rails_shim/patches/openssl.rb +58 -0
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +32 -0
- data/lib/ractor_rails_shim/patches/polymorphic_routes.rb +115 -0
- data/lib/ractor_rails_shim/patches/propshaft.rb +110 -0
- data/lib/ractor_rails_shim/patches/rack.rb +160 -0
- data/lib/ractor_rails_shim/patches/rails_module.rb +192 -0
- data/lib/ractor_rails_shim/patches/route_helpers.rb +119 -0
- data/lib/ractor_rails_shim/patches/rubygems.rb +65 -0
- data/lib/ractor_rails_shim/patches/url_helpers.rb +78 -0
- data/lib/ractor_rails_shim/patches/warden.rb +195 -0
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +124 -0
- data/lib/ractor_rails_shim/patches.rb +67 -0
- data/lib/ractor_rails_shim/version.rb +5 -0
- data/lib/ractor_rails_shim/version_check.rb +88 -0
- data/lib/ractor_rails_shim.rb +33 -0
- 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
|