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,582 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patches for ActionView: LookupContext (default_formats defined via
|
|
4
|
+
# define_method(&block)), Template::Handlers, and PathRegistry.
|
|
5
|
+
|
|
6
|
+
module RactorRailsShim
|
|
7
|
+
# Shareable, mutable module that holds compiled template methods
|
|
8
|
+
# (e.g. `_app_views_...`). ActionView attaches compiled template methods to
|
|
9
|
+
# `compiled_method_container`; the default returns a per-class container,
|
|
10
|
+
# which isolates the shared application layout's compiled method to whichever
|
|
11
|
+
# controller first rendered it (so other controllers raise NoMethodError on
|
|
12
|
+
# the layout). Routing every view_context_class to this ONE shared module
|
|
13
|
+
# makes compiled methods available to all controllers/workers. It is a plain
|
|
14
|
+
# Module (shareable without freezing, so workers can still define methods on
|
|
15
|
+
# it).
|
|
16
|
+
SHAREABLE_COMPILED_MODULE = Module.new unless defined?(::RactorRailsShim::SHAREABLE_COMPILED_MODULE)
|
|
17
|
+
|
|
18
|
+
# ActionView constants that need to be made shareable.
|
|
19
|
+
SHAREABLE_CONSTANTS.concat([
|
|
20
|
+
"ActionView::LookupContext::Accessors::DEFAULT_PROCS",
|
|
21
|
+
"ActionView::Template::NONE",
|
|
22
|
+
"ActionView::Template::Handlers::ERB::ENCODING_TAG",
|
|
23
|
+
"ActionView::AbstractRenderer::RenderedTemplate::EMPTY_SPACER",
|
|
24
|
+
"ActionView::Helpers::TagHelper::PRE_CONTENT_STRINGS",
|
|
25
|
+
"ActionView::Helpers::AssetUrlHelper::ASSET_EXTENSIONS",
|
|
26
|
+
"ActionView::Helpers::UrlHelper::BUTTON_TAG_METHOD_VERBS",
|
|
27
|
+
"ActionView::Helpers::UrlHelper::STRINGIFIED_COMMON_METHODS",
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Patch `ActionView::Base.with_empty_template_cache` (action_view/base.rb:204)
|
|
32
|
+
# to a block-free `def`. The original defines `compiled_method_container`
|
|
33
|
+
# (instance + singleton) via `define_method(&block)` — an un-shareable Proc
|
|
34
|
+
# compiled in the main Ractor that raises "defined with an un-shareable Proc
|
|
35
|
+
# in a different Ractor" when a worker calls it. We also route compiled
|
|
36
|
+
# template methods through ONE shared `SHAREABLE_COMPILED_MODULE` so the
|
|
37
|
+
# `application` layout (and Devise shared partials) compile once and are
|
|
38
|
+
# visible to every controller / worker Ractor.
|
|
39
|
+
#
|
|
40
|
+
# Installed EARLY via ActiveSupport.on_load(:action_view) (see core.rb
|
|
41
|
+
# `install`) so it is in place before production eager load calls
|
|
42
|
+
# `DetailsKey.view_context_class` -> `with_empty_template_cache`. Idempotent.
|
|
43
|
+
def _install_with_empty_template_cache_patch
|
|
44
|
+
return if @with_empty_template_cache_patched
|
|
45
|
+
return unless defined?(::ActionView::Base) && Ractor.main?
|
|
46
|
+
@with_empty_template_cache_patched = true
|
|
47
|
+
_register_patch :with_empty_template_cache, "8.1"
|
|
48
|
+
::ActionView::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
49
|
+
def with_empty_template_cache
|
|
50
|
+
subclass = Class.new(self) do
|
|
51
|
+
include RactorRailsShim::SHAREABLE_COMPILED_MODULE
|
|
52
|
+
def compiled_method_container
|
|
53
|
+
RactorRailsShim::SHAREABLE_COMPILED_MODULE
|
|
54
|
+
end
|
|
55
|
+
def self.compiled_method_container
|
|
56
|
+
RactorRailsShim::SHAREABLE_COMPILED_MODULE
|
|
57
|
+
end
|
|
58
|
+
def inspect
|
|
59
|
+
"#<ActionView::Base:\#{'%#016x' % (object_id << 1)}>"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
subclass
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _install_lookup_context_patch
|
|
68
|
+
return if @lookup_context_patched
|
|
69
|
+
@lookup_context_patched = true
|
|
70
|
+
_register_patch :lookup_context, "8.1"
|
|
71
|
+
return unless defined?(::ActionView::LookupContext)
|
|
72
|
+
# Force autoload of the nested ActionView::Template constants that the
|
|
73
|
+
# patched details_cache_key references (Template::Types,
|
|
74
|
+
# TemplateDetails::Requested). Constants are global, so defining them
|
|
75
|
+
# here (main ractor) makes them visible to worker ractors, which cannot
|
|
76
|
+
# autoload. Without this, the first template render in a worker dies on
|
|
77
|
+
# `NameError: uninitialized constant ActionView::Template::TemplateDetails`.
|
|
78
|
+
if Ractor.main? && defined?(::ActionView::Template)
|
|
79
|
+
::ActionView::Template::Types rescue nil
|
|
80
|
+
::ActionView::TemplateDetails rescue nil
|
|
81
|
+
end
|
|
82
|
+
lc = ::ActionView::LookupContext
|
|
83
|
+
key = :ractor_rails_shim_lookup_context_registered_details
|
|
84
|
+
key_str = key.inspect
|
|
85
|
+
lc.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
86
|
+
def registered_details
|
|
87
|
+
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
88
|
+
return v unless v.nil?
|
|
89
|
+
if Ractor.main? && instance_variable_defined?(:@registered_details)
|
|
90
|
+
@registered_details
|
|
91
|
+
else
|
|
92
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}] || []
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
def registered_details=(val)
|
|
96
|
+
ActiveSupport::IsolatedExecutionState[#{key_str}] = val
|
|
97
|
+
@registered_details = val if Ractor.main?
|
|
98
|
+
val
|
|
99
|
+
end
|
|
100
|
+
RUBY
|
|
101
|
+
CLASS_ATTRIBUTES << ["ActionView::LookupContext", :registered_details, key, []]
|
|
102
|
+
|
|
103
|
+
# Redefine default_#{name} methods. register_detail (line 25 of
|
|
104
|
+
# lookup_context.rb) defines these via Accessors.define_method(:"default_#{name}", &block)
|
|
105
|
+
# — a Proc from the main ractor. Calling them from a worker raises
|
|
106
|
+
# "defined with an un-shareable Proc in a different Ractor".
|
|
107
|
+
# Trigger: when Accept: */* is sent, request.formats returns [Mime::ALL],
|
|
108
|
+
# and LookupContext#formats= (the override at line 263) does
|
|
109
|
+
# `values.concat(default_formats) if values.delete "*/*"` — Mime::ALL
|
|
110
|
+
# compares == to "*/*", so delete removes it and default_formats is called.
|
|
111
|
+
# Fix: call each block once in main, make the result shareable, and
|
|
112
|
+
# redefine the method via string eval (no captured binding).
|
|
113
|
+
accessors = ::ActionView::LookupContext::Accessors
|
|
114
|
+
::ActionView::LookupContext.registered_details.each do |name|
|
|
115
|
+
block = accessors::DEFAULT_PROCS[name]
|
|
116
|
+
next unless block
|
|
117
|
+
begin
|
|
118
|
+
value = block.call
|
|
119
|
+
rescue
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
begin
|
|
123
|
+
value = Ractor.make_shareable(value)
|
|
124
|
+
rescue
|
|
125
|
+
value = value.dup.freeze rescue value
|
|
126
|
+
end
|
|
127
|
+
const_name = "SHIM_DEFAULT_#{name.upcase}_VALUE"
|
|
128
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
129
|
+
accessors.const_set(const_name, value)
|
|
130
|
+
$VERBOSE = verbose
|
|
131
|
+
accessors.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
132
|
+
def default_#{name}
|
|
133
|
+
::ActionView::LookupContext::Accessors::#{const_name}
|
|
134
|
+
end
|
|
135
|
+
RUBY
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Patch ActionView::Rendering::ClassMethods#view_context_class to read
|
|
139
|
+
# from a shareable registry (populated in main) instead of building via
|
|
140
|
+
# Class.new{...} blocks (un-shareable Proc from a worker). The built
|
|
141
|
+
# class is made shareable; workers read it via the registry.
|
|
142
|
+
if defined?(::ActionView::Rendering::ClassMethods)
|
|
143
|
+
rcm = ::ActionView::Rendering::ClassMethods
|
|
144
|
+
|
|
145
|
+
# `ActionView::Base.with_empty_template_cache` is patched separately and
|
|
146
|
+
# EARLY (see _install_with_empty_template_cache_patch, installed via
|
|
147
|
+
# ActiveSupport.on_load(:action_view) before eager load) because the
|
|
148
|
+
# framework's original uses `define_method(:compiled_method_container)
|
|
149
|
+
# { subclass }` — a block/Proc captured in the main Ractor that raises
|
|
150
|
+
# "defined with an un-shareable Proc in a different Ractor" when a
|
|
151
|
+
# worker calls it. In production `DetailsKey.view_context_class` calls
|
|
152
|
+
# with_empty_template_cache during eager load, so the patch MUST be in
|
|
153
|
+
# place before then.
|
|
154
|
+
_install_with_empty_template_cache_patch if defined?(::ActionView::Base)
|
|
155
|
+
|
|
156
|
+
# Build the per-controller view_context_class registry in main. We call
|
|
157
|
+
# the ORIGINAL build_view_context_class directly (bypassing Rails'
|
|
158
|
+
# inherit_view_context_class? short-circuit, which otherwise makes
|
|
159
|
+
# subclasses reuse ActionController::Base's class built with a nil
|
|
160
|
+
# `_routes` and thus NO route url_helpers). `routes` is forced to the
|
|
161
|
+
# shareable Rails.application.routes when the controller's own `_routes`
|
|
162
|
+
# is nil, so named helpers (new_post_path, etc.) are always present.
|
|
163
|
+
if Ractor.main?
|
|
164
|
+
registry = {}
|
|
165
|
+
::AbstractController::Base.descendants.each do |ctrl|
|
|
166
|
+
begin
|
|
167
|
+
routes = ctrl.respond_to?(:_routes) ? ctrl._routes : nil
|
|
168
|
+
routes = ::Rails.application.routes if routes.nil? && ::Rails.respond_to?(:application) && ::Rails.application
|
|
169
|
+
vcc = rcm.instance_method(:build_view_context_class).bind(ctrl).call(
|
|
170
|
+
::ActionView::LookupContext::DetailsKey.view_context_class,
|
|
171
|
+
ctrl.respond_to?(:supports_path?) ? ctrl.supports_path? : true,
|
|
172
|
+
routes,
|
|
173
|
+
ctrl.respond_to?(:_helpers) ? ctrl._helpers : nil
|
|
174
|
+
)
|
|
175
|
+
registry[ctrl] = vcc if vcc
|
|
176
|
+
rescue => e
|
|
177
|
+
# skip controllers that can't build (e.g. abstract)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
registry.delete_if { |_, v| v.nil? }
|
|
181
|
+
registry.freeze
|
|
182
|
+
begin
|
|
183
|
+
Ractor.make_shareable(registry)
|
|
184
|
+
self._view_context_registry = registry
|
|
185
|
+
rescue => e
|
|
186
|
+
# If the registry can't be made shareable, leave it — workers fall
|
|
187
|
+
# back to the empty-cache base.
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
rcm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
192
|
+
def view_context_class
|
|
193
|
+
return @view_context_class if Ractor.main? && instance_variable_defined?(:@view_context_class) && @view_context_class
|
|
194
|
+
|
|
195
|
+
if Ractor.main?
|
|
196
|
+
@view_context_class ||= build_view_context_class(
|
|
197
|
+
ActionView::LookupContext::DetailsKey.view_context_class,
|
|
198
|
+
supports_path?,
|
|
199
|
+
_routes,
|
|
200
|
+
_helpers
|
|
201
|
+
)
|
|
202
|
+
return @view_context_class
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Worker Ractor. The frozen shared controller class carries a
|
|
206
|
+
# memoized @view_context_class built in main (in the registry) with
|
|
207
|
+
# its proper route url_helpers + controller helpers. Look it up by
|
|
208
|
+
# controller class; fall back to the shareable fallback class (built
|
|
209
|
+
# in main with Rails.application.routes) for any controller not
|
|
210
|
+
# present in the registry at prepare time.
|
|
211
|
+
vcc = RactorRailsShim._view_context_registry[self]
|
|
212
|
+
return vcc if vcc
|
|
213
|
+
RactorRailsShim._view_context_fallback
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# `inherit_view_context_class?` (action_view/rendering.rb:52) compares
|
|
217
|
+
# `superclass._helpers` — and `_helpers` is defined via a block
|
|
218
|
+
# (redefine_singleton_method) that cannot run in a worker Ractor
|
|
219
|
+
# ("defined with an un-shareable Proc in a different Ractor"). Return
|
|
220
|
+
# false so each controller builds its own view_context_class (the
|
|
221
|
+
# normal Rails behaviour whenever _routes/_helpers differ), avoiding
|
|
222
|
+
# the `_helpers` comparison entirely. Behaviour is identical in the
|
|
223
|
+
# main Ractor (the inherited class would be equivalent).
|
|
224
|
+
def inherit_view_context_class?
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
RUBY
|
|
228
|
+
end
|
|
229
|
+
vcc_key = :ractor_rails_shim_lookup_context_view_context_class
|
|
230
|
+
vcc_key_str = vcc_key.inspect
|
|
231
|
+
CLASS_ATTRIBUTES << ["ActionView::LookupContext::DetailsKey", :view_context_class, vcc_key, nil]
|
|
232
|
+
# Build it now in main and stash in IES so the fallback builder picks it up.
|
|
233
|
+
if Ractor.main? && defined?(::ActionView::Base)
|
|
234
|
+
ActiveSupport::IsolatedExecutionState[vcc_key] = ::ActionView::LookupContext::DetailsKey.view_context_class
|
|
235
|
+
# Shareable fallback view_context_class for any controller not present in
|
|
236
|
+
# the registry at prepare time. Subclasses ActionView::Base, so it
|
|
237
|
+
# inherits the per-class compiled_method_container (self.class) and the
|
|
238
|
+
# route url_helpers. build_view_context_class is a ClassMethods method on
|
|
239
|
+
# controllers, so invoke it via the unbound method bound to
|
|
240
|
+
# ActionController::Base.
|
|
241
|
+
fallback = rcm.instance_method(:build_view_context_class).bind(::ActionController::Base).call(
|
|
242
|
+
::ActionView::LookupContext::DetailsKey.view_context_class,
|
|
243
|
+
true,
|
|
244
|
+
(defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application) ? ::Rails.application.routes : nil,
|
|
245
|
+
nil
|
|
246
|
+
)
|
|
247
|
+
Ractor.make_shareable(fallback) rescue nil
|
|
248
|
+
self._view_context_fallback = fallback
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
dk = ::ActionView::LookupContext::DetailsKey
|
|
252
|
+
dk_key = :ractor_rails_shim_lookup_context_details_keys
|
|
253
|
+
dc_key = :ractor_rails_shim_lookup_context_digest_cache
|
|
254
|
+
dk_key_str = dk_key.inspect
|
|
255
|
+
dc_key_str = dc_key.inspect
|
|
256
|
+
vcc_key_str2 = vcc_key.inspect
|
|
257
|
+
dk.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
258
|
+
def view_context_class
|
|
259
|
+
v = ActiveSupport::IsolatedExecutionState[#{vcc_key_str2}]
|
|
260
|
+
return v unless v.nil?
|
|
261
|
+
if Ractor.main? && instance_variable_defined?(:@view_context_class)
|
|
262
|
+
v = @view_context_class
|
|
263
|
+
ActiveSupport::IsolatedExecutionState[#{vcc_key_str2}] = v
|
|
264
|
+
v
|
|
265
|
+
else
|
|
266
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{vcc_key_str2}]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
def details_keys
|
|
270
|
+
v = ActiveSupport::IsolatedExecutionState[#{dk_key_str}]
|
|
271
|
+
return v if v
|
|
272
|
+
if Ractor.main? && instance_variable_defined?(:@details_keys)
|
|
273
|
+
v = @details_keys
|
|
274
|
+
else
|
|
275
|
+
v = Concurrent::Map.new
|
|
276
|
+
end
|
|
277
|
+
ActiveSupport::IsolatedExecutionState[#{dk_key_str}] = v
|
|
278
|
+
v
|
|
279
|
+
end
|
|
280
|
+
def digest_cache(details)
|
|
281
|
+
dc = (ActiveSupport::IsolatedExecutionState[#{dc_key_str}] ||= Concurrent::Map.new)
|
|
282
|
+
dc[details_cache_key(details)] ||= Concurrent::Map.new
|
|
283
|
+
end
|
|
284
|
+
def details_cache_key(details)
|
|
285
|
+
details_keys.fetch(details) do
|
|
286
|
+
if formats = details[:formats]
|
|
287
|
+
unless ::ActionView::Template::Types.valid_symbols?(formats)
|
|
288
|
+
details = details.dup
|
|
289
|
+
details[:formats] &= ::ActionView::Template::Types.symbols
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
details_keys[details] ||= ::ActionView::TemplateDetails::Requested.new(**details)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
RUBY
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def _install_template_handlers_patch
|
|
299
|
+
return if @template_handlers_patched
|
|
300
|
+
@template_handlers_patched = true
|
|
301
|
+
_register_patch :template_handlers, "8.1"
|
|
302
|
+
return unless defined?(::ActionView::Template::Handlers)
|
|
303
|
+
# Eager-load the handler classes in main so workers don't need to
|
|
304
|
+
# autoload them (workers can't autoload).
|
|
305
|
+
if Ractor.main?
|
|
306
|
+
::ActionView::Template::Handlers::Raw rescue nil
|
|
307
|
+
::ActionView::Template::Handlers::ERB rescue nil
|
|
308
|
+
::ActionView::Template::Handlers::Html rescue nil
|
|
309
|
+
::ActionView::Template::Handlers::Builder rescue nil
|
|
310
|
+
end
|
|
311
|
+
h = ::ActionView::Template::Handlers
|
|
312
|
+
th_key = :ractor_rails_shim_av_template_handlers
|
|
313
|
+
dth_key = :ractor_rails_shim_av_default_template_handlers
|
|
314
|
+
th_key_str = th_key.inspect
|
|
315
|
+
dth_key_str = dth_key.inspect
|
|
316
|
+
# The handler registry lives in class variables (@@template_handlers,
|
|
317
|
+
# @@default_template_handlers, @@template_extensions) whose values are
|
|
318
|
+
# mutable Hashes holding handler instances — and an unshareable `:ruby`
|
|
319
|
+
# lambda. A worker Ractor cannot read these class vars. Route the
|
|
320
|
+
# registry through IsolatedExecutionState: each Ractor builds its own
|
|
321
|
+
# handler map (the defaults are deterministic), and in main we seed from
|
|
322
|
+
# the live class var (capturing any custom handlers gems registered at
|
|
323
|
+
# boot). The `:ruby` lambda makes the map unshareable, so the old
|
|
324
|
+
# SHAREABLE_FALLBACK approach (which skips unshareable values) left
|
|
325
|
+
# workers with an empty map. These are instance methods (Handlers is
|
|
326
|
+
# extended into ActionView::Template, and the render path calls them on
|
|
327
|
+
# the Template instance), so they must be defined on the module itself,
|
|
328
|
+
# not just the singleton class.
|
|
329
|
+
h.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
330
|
+
def self._ractor_rails_shim_handlers
|
|
331
|
+
map = ActiveSupport::IsolatedExecutionState[#{th_key_str}]
|
|
332
|
+
return map unless map.nil?
|
|
333
|
+
if Ractor.main?
|
|
334
|
+
cv = class_variable_get(:@@template_handlers) rescue nil
|
|
335
|
+
cv = nil if cv && cv.empty?
|
|
336
|
+
if cv
|
|
337
|
+
ActiveSupport::IsolatedExecutionState[#{th_key_str}] = cv
|
|
338
|
+
return cv
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
built = {
|
|
342
|
+
raw: ::ActionView::Template::Handlers::Raw.new,
|
|
343
|
+
erb: ::ActionView::Template::Handlers::ERB.new,
|
|
344
|
+
html: ::ActionView::Template::Handlers::Html.new,
|
|
345
|
+
builder: ::ActionView::Template::Handlers::Builder.new,
|
|
346
|
+
ruby: ->(_, source) { source },
|
|
347
|
+
}
|
|
348
|
+
ActiveSupport::IsolatedExecutionState[#{th_key_str}] = built
|
|
349
|
+
built
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def self._ractor_rails_shim_persist(map)
|
|
353
|
+
ActiveSupport::IsolatedExecutionState[#{th_key_str}] = map
|
|
354
|
+
class_variable_set(:@@template_handlers, map) if Ractor.main?
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def self.extensions
|
|
358
|
+
self._ractor_rails_shim_handlers.keys
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def registered_template_handler(extension)
|
|
362
|
+
extension && ::ActionView::Template::Handlers._ractor_rails_shim_handlers[extension.to_sym]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def handler_for_extension(extension)
|
|
366
|
+
registered_template_handler(extension) || ::ActionView::Template::Handlers::ERB.new
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def template_handler_extensions
|
|
370
|
+
::ActionView::Template::Handlers._ractor_rails_shim_handlers.keys.map(&:to_s).sort
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def register_template_handler(*extensions, handler)
|
|
374
|
+
map = ::ActionView::Template::Handlers._ractor_rails_shim_handlers.dup
|
|
375
|
+
extensions.each { |ext| map[ext.to_sym] = handler }
|
|
376
|
+
::ActionView::Template::Handlers._ractor_rails_shim_persist(map)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def unregister_template_handler(*extensions)
|
|
380
|
+
map = ::ActionView::Template::Handlers._ractor_rails_shim_handlers.dup
|
|
381
|
+
extensions.each { |ext| map.delete(ext.to_sym) }
|
|
382
|
+
::ActionView::Template::Handlers._ractor_rails_shim_persist(map)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def register_default_template_handler(extension, klass)
|
|
386
|
+
register_template_handler(extension, klass)
|
|
387
|
+
end
|
|
388
|
+
RUBY
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Patch ActionView::PathRegistry to not read its raw class ivars
|
|
392
|
+
# (@view_paths_by_class, @file_system_resolvers) from a worker Ractor.
|
|
393
|
+
# These are populated at boot (view paths registered by the app). For a
|
|
394
|
+
# frozen shared app they're read-only; workers read them via the shareable
|
|
395
|
+
# fallback (built from main's values, made shareable). `get_view_paths` is
|
|
396
|
+
# called per-request during view lookup; `all_file_system_resolvers` is
|
|
397
|
+
# called by the exception backtrace builder.
|
|
398
|
+
def _install_path_registry_patch
|
|
399
|
+
return if @path_registry_patched
|
|
400
|
+
@path_registry_patched = true
|
|
401
|
+
_register_patch :path_registry, "8.1"
|
|
402
|
+
return unless defined?(::ActionView::PathRegistry)
|
|
403
|
+
pr = ::ActionView::PathRegistry
|
|
404
|
+
vpc_key = :ractor_rails_shim_path_registry_view_paths_by_class
|
|
405
|
+
fsr_key = :ractor_rails_shim_path_registry_file_system_resolvers
|
|
406
|
+
vpc_key_str = vpc_key.inspect
|
|
407
|
+
fsr_key_str = fsr_key.inspect
|
|
408
|
+
pr.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
409
|
+
def get_view_paths(klass)
|
|
410
|
+
h = ActiveSupport::IsolatedExecutionState[#{vpc_key_str}]
|
|
411
|
+
h = (Ractor.main? ? (instance_variable_defined?(:@view_paths_by_class) ? instance_variable_get(:@view_paths_by_class) : {}) : RactorRailsShim::SHAREABLE_FALLBACK[#{vpc_key_str}]) if h.nil?
|
|
412
|
+
h[klass] || get_view_paths(klass.superclass)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def set_view_paths(klass, paths)
|
|
416
|
+
h = ActiveSupport::IsolatedExecutionState[#{vpc_key_str}] ||= (Ractor.main? ? (instance_variable_defined?(:@view_paths_by_class) ? instance_variable_get(:@view_paths_by_class) : {}) : {})
|
|
417
|
+
h[klass] = paths
|
|
418
|
+
instance_variable_set(:@view_paths_by_class, h) if Ractor.main?
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def all_file_system_resolvers
|
|
422
|
+
h = ActiveSupport::IsolatedExecutionState[#{fsr_key_str}]
|
|
423
|
+
h = (Ractor.main? ? (instance_variable_defined?(:@file_system_resolvers) ? instance_variable_get(:@file_system_resolvers) : {}) : RactorRailsShim::SHAREABLE_FALLBACK[#{fsr_key_str}]) if h.nil?
|
|
424
|
+
h.values
|
|
425
|
+
end
|
|
426
|
+
RUBY
|
|
427
|
+
# Register so the fallback builder captures + shares these.
|
|
428
|
+
CLASS_ATTRIBUTES << ["ActionView::PathRegistry", :view_paths_by_class, vpc_key, {}]
|
|
429
|
+
CLASS_ATTRIBUTES << ["ActionView::PathRegistry", :file_system_resolvers, fsr_key, {}]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Patch ActionView::FileSystemResolver#_find_all. The original reads the
|
|
433
|
+
# resolver's `@unbound_templates` cache, which `make_app_shareable!`
|
|
434
|
+
# rewrites from a Concurrent::Map into a frozen Hash (Concurrent::Map
|
|
435
|
+
# refuses #freeze). The original then calls `cache.compute_if_absent`
|
|
436
|
+
# (a Concurrent::Map API) on it, which a frozen Hash lacks -> NoMethodError
|
|
437
|
+
# in a worker Ractor. Route the per-virtual-path cache through
|
|
438
|
+
# IsolatedExecutionState instead: each Ractor builds its own mutable Hash
|
|
439
|
+
# (deterministic from disk via `unbound_templates_from_path`), so the
|
|
440
|
+
# frozen shareable app graph is never mutated.
|
|
441
|
+
def _install_action_view_resolver_patch
|
|
442
|
+
return if @action_view_resolver_patched
|
|
443
|
+
@action_view_resolver_patched = true
|
|
444
|
+
_register_patch :action_view_resolver, "8.1"
|
|
445
|
+
return unless defined?(::ActionView::FileSystemResolver)
|
|
446
|
+
# Eager-load nested constants referenced below (workers can't autoload).
|
|
447
|
+
if Ractor.main?
|
|
448
|
+
::ActionView::TemplateDetails rescue nil
|
|
449
|
+
::ActionView::TemplatePath rescue nil
|
|
450
|
+
end
|
|
451
|
+
::ActionView::FileSystemResolver.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
452
|
+
def _find_all(name, prefix, partial, details, key, locals)
|
|
453
|
+
requested_details = key || ::ActionView::TemplateDetails::Requested.new(**details)
|
|
454
|
+
virtual = ::ActionView::TemplatePath.virtual(name, prefix, partial)
|
|
455
|
+
# Key the cache by resolver path AND virtual path: each resolver
|
|
456
|
+
# (app/views, each gem) has its own @path and its own templates.
|
|
457
|
+
# Keying only by virtual path would let the first resolver poison
|
|
458
|
+
# the cache for all others (e.g. app/views caches [] for
|
|
459
|
+
# devise/sessions/new, hiding the template that lives in the
|
|
460
|
+
# devise gem resolver).
|
|
461
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_resolver_cache] ||= {})
|
|
462
|
+
cache_key = [@path, virtual]
|
|
463
|
+
unbound_templates =
|
|
464
|
+
if cache.key?(cache_key)
|
|
465
|
+
cache[cache_key]
|
|
466
|
+
else
|
|
467
|
+
path = ::ActionView::TemplatePath.build(name, prefix, partial)
|
|
468
|
+
tmpls = unbound_templates_from_path(path)
|
|
469
|
+
cache[cache_key] = tmpls
|
|
470
|
+
tmpls
|
|
471
|
+
end
|
|
472
|
+
filter_and_sort_by_details(unbound_templates, requested_details).map do |unbound_template|
|
|
473
|
+
unbound_template.bind_locals(locals)
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
RUBY
|
|
477
|
+
|
|
478
|
+
# Patch ActionView::Resolver::PathParser#parse. The resolver's
|
|
479
|
+
# @path_parser instance is part of the shareable app graph frozen by
|
|
480
|
+
# make_app_shareable!, and the original method memoizes its compiled
|
|
481
|
+
# regex in `@regex ||= build_path_regex` — assigning @regex on a frozen
|
|
482
|
+
# object raises FrozenError in a worker Ractor. Route the memoization
|
|
483
|
+
# through IsolatedExecutionState keyed by the parser's object_id, so each
|
|
484
|
+
# Ractor compiles its own regex once without mutating the frozen object.
|
|
485
|
+
if defined?(::ActionView::Resolver::PathParser)
|
|
486
|
+
pp = ::ActionView::Resolver::PathParser
|
|
487
|
+
pp_key = :ractor_rails_shim_path_parser_regex
|
|
488
|
+
pp_key_str = pp_key.inspect
|
|
489
|
+
pp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
490
|
+
def parse(path)
|
|
491
|
+
regex = ActiveSupport::IsolatedExecutionState[:"#{pp_key_str}_\#{object_id}"] ||= build_path_regex
|
|
492
|
+
match = regex.match(path)
|
|
493
|
+
path = ::ActionView::TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
|
|
494
|
+
details = ::ActionView::TemplateDetails.new(
|
|
495
|
+
match[:locale]&.to_sym,
|
|
496
|
+
match[:handler]&.to_sym,
|
|
497
|
+
match[:format]&.to_sym,
|
|
498
|
+
match[:variant]&.to_sym
|
|
499
|
+
)
|
|
500
|
+
::ActionView::Resolver::PathParser::ParsedPath.new(path, details)
|
|
501
|
+
end
|
|
502
|
+
RUBY
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Patch ActionView::AbstractRenderer::ObjectRendering#partial_path. The
|
|
507
|
+
# original reads `PREFIXED_PARTIAL_NAMES` — a `Concurrent::Map` constant
|
|
508
|
+
# (nested Concurrent::Maps) — and writes a nested entry via
|
|
509
|
+
# `PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= ...`. Concurrent::Map
|
|
510
|
+
# is intrinsically unshareable (it refuses #freeze), so a worker Ractor
|
|
511
|
+
# cannot read the constant NOR write to it. Redefine the method to use a
|
|
512
|
+
# per-Ractor Hash via IsolatedExecutionState (each Ractor builds its own
|
|
513
|
+
# cache from `merge_prefix_into_object_path`, which is deterministic).
|
|
514
|
+
def _install_action_view_partial_path_patch
|
|
515
|
+
return if @action_view_partial_path_patched
|
|
516
|
+
@action_view_partial_path_patched = true
|
|
517
|
+
_register_patch :action_view_partial_path, "8.1"
|
|
518
|
+
return unless defined?(::ActionView::AbstractRenderer)
|
|
519
|
+
::ActionView::AbstractRenderer::ObjectRendering.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
520
|
+
def partial_path(object, view)
|
|
521
|
+
object = object.to_model if object.respond_to?(:to_model)
|
|
522
|
+
path = if object.respond_to?(:to_partial_path)
|
|
523
|
+
object.to_partial_path
|
|
524
|
+
else
|
|
525
|
+
raise ArgumentError.new("\#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
|
|
526
|
+
end
|
|
527
|
+
if view.prefix_partial_path_with_controller_namespace
|
|
528
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_prefixed_partial_names] ||= {})
|
|
529
|
+
cache[@context_prefix] ||= {}
|
|
530
|
+
cache[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
|
531
|
+
else
|
|
532
|
+
path
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
RUBY
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Patch ActionView::Helpers::Tags::TextField.field_type (and the subclasses
|
|
539
|
+
# EmailField/PasswordField/... that inherit it). The original memoizes its
|
|
540
|
+
# computed String in a lazy class ivar (`@field_type ||= name...`). The
|
|
541
|
+
# class ivar is per-subclass and unshareable-writable from a worker Ractor.
|
|
542
|
+
# Route the cache through IsolatedExecutionState keyed by the class name so
|
|
543
|
+
# each Ractor builds its own copy; the computation is deterministic.
|
|
544
|
+
def _install_action_view_field_type_patch
|
|
545
|
+
return if @action_view_field_type_patched
|
|
546
|
+
@action_view_field_type_patched = true
|
|
547
|
+
_register_patch :action_view_field_type, "8.1"
|
|
548
|
+
return unless defined?(::ActionView::Helpers::Tags::TextField)
|
|
549
|
+
tf = ::ActionView::Helpers::Tags::TextField
|
|
550
|
+
tf.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
551
|
+
def field_type
|
|
552
|
+
key = :"ractor_rails_shim_field_type_\#{name}"
|
|
553
|
+
v = ActiveSupport::IsolatedExecutionState[key]
|
|
554
|
+
return v if v
|
|
555
|
+
ft = name.split("::").last.sub("Field", "").downcase
|
|
556
|
+
ActiveSupport::IsolatedExecutionState[key] = ft
|
|
557
|
+
ft
|
|
558
|
+
end
|
|
559
|
+
RUBY
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Patch ActionView::Helpers::OutputSafetyHelper#safe_join. Its default
|
|
563
|
+
# separator parameter is `sep = $,` — a reference to the `$` global, which a
|
|
564
|
+
# worker Ractor cannot read (Ractor::IsolationError: can not access global
|
|
565
|
+
# variable $,). The `$` global is nil in every normal Rails process, so
|
|
566
|
+
# defaulting to nil reproduces the identical behaviour without touching the
|
|
567
|
+
# global.
|
|
568
|
+
def _install_action_view_safe_join_patch
|
|
569
|
+
return if @action_view_safe_join_patched
|
|
570
|
+
@action_view_safe_join_patched = true
|
|
571
|
+
_register_patch :action_view_safe_join, "8.1"
|
|
572
|
+
return unless defined?(::ActionView::Helpers::OutputSafetyHelper)
|
|
573
|
+
::ActionView::Helpers::OutputSafetyHelper.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
574
|
+
def safe_join(array, sep = nil)
|
|
575
|
+
sep = ERB::Util.unwrapped_html_escape(sep)
|
|
576
|
+
array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
|
|
577
|
+
end
|
|
578
|
+
RUBY
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
end
|
|
582
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch two ActiveModel/ActiveRecord behaviors so worker Ractors (under a
|
|
4
|
+
# frozen `:ractor` shared graph) can build and persist records.
|
|
5
|
+
#
|
|
6
|
+
# 1. Attribute#dup_or_share (writes): see ActiveModelAttributePatch below.
|
|
7
|
+
# 2. AttributeRegistration class caches (reads): `@attribute_types`,
|
|
8
|
+
# `@default_attributes`, etc. hold ActiveModel::Type instances, which are
|
|
9
|
+
# NOT Ractor-shareable. Under `:ractor` the shim freezes the graph, so those
|
|
10
|
+
# class-ivar values become unshareable; a worker reading them raises
|
|
11
|
+
# `Ractor::IsolationError: can not get unshareable values from instance
|
|
12
|
+
# variables ... (@attribute_types from Post)`, and a worker writing them
|
|
13
|
+
# (the `||=` memoization) raises `can not set instance variables ...`.
|
|
14
|
+
#
|
|
15
|
+
# Fix: serve these caches from per-Ractor storage (ActiveSupport::
|
|
16
|
+
# IsolatedExecutionState), keyed by the class. Each worker Ractor computes
|
|
17
|
+
# and keeps its OWN copy — it never touches the shared class ivar, so there
|
|
18
|
+
# is no cross-boundary (unshareable) value and no class-ivar write. In the
|
|
19
|
+
# main Ractor this behaves identically to the original (compute once, cache).
|
|
20
|
+
# The values are deterministic from the schema, so per-Ractor caching is
|
|
21
|
+
# behavior-preserving.
|
|
22
|
+
|
|
23
|
+
module RactorRailsShim
|
|
24
|
+
module ActiveModelAttributePatch
|
|
25
|
+
def self.included(base)
|
|
26
|
+
base.prepend(InstanceMethods)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module InstanceMethods
|
|
30
|
+
def dup_or_share # :nodoc:
|
|
31
|
+
if frozen?
|
|
32
|
+
self.class.from_database(
|
|
33
|
+
name,
|
|
34
|
+
value_before_type_cast,
|
|
35
|
+
type,
|
|
36
|
+
defined?(@value) ? @value : nil
|
|
37
|
+
)
|
|
38
|
+
else
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module ActiveModelAttributeRegistrationPatch
|
|
46
|
+
def self.prepended(base)
|
|
47
|
+
base.prepend(InstanceMethods)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module InstanceMethods
|
|
51
|
+
def _default_attributes # :nodoc:
|
|
52
|
+
key = :"rrs_default_attributes_#{object_id}"
|
|
53
|
+
ActiveSupport::IsolatedExecutionState[key] ||=
|
|
54
|
+
::ActiveModel::AttributeSet.new({}).tap do |attribute_set|
|
|
55
|
+
apply_pending_attribute_modifications(attribute_set)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def attribute_types # :nodoc:
|
|
60
|
+
key = :"rrs_attribute_types_#{object_id}"
|
|
61
|
+
ActiveSupport::IsolatedExecutionState[key] ||= begin
|
|
62
|
+
types = _default_attributes.cast_types
|
|
63
|
+
types.default = ::ActiveModel::Type.default_value
|
|
64
|
+
types
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module ActiveRecordAttributesPatch
|
|
71
|
+
def self.prepended(base)
|
|
72
|
+
base.prepend(InstanceMethods)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module InstanceMethods
|
|
76
|
+
# ActiveRecord overrides _default_attributes (attributes.rb:253) with a
|
|
77
|
+
# version that reads the @default_attributes class ivar and opens a
|
|
78
|
+
# connection to build the AttributeSet. Same per-Ractor fix as above:
|
|
79
|
+
# cache in IES so workers never read/write the shared class ivar.
|
|
80
|
+
def _default_attributes # :nodoc:
|
|
81
|
+
key = :"rrs_default_attributes_#{object_id}"
|
|
82
|
+
ActiveSupport::IsolatedExecutionState[key] ||= begin
|
|
83
|
+
attributes_hash = with_connection do |connection|
|
|
84
|
+
columns_hash.transform_values do |column|
|
|
85
|
+
::ActiveModel::Attribute.from_database(
|
|
86
|
+
column.name, column.default, type_for_column(connection, column)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
attribute_set = ::ActiveModel::AttributeSet.new(attributes_hash)
|
|
91
|
+
apply_pending_attribute_modifications(attribute_set)
|
|
92
|
+
attribute_set
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|