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,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patches for AbstractController: controller_path, action_methods,
|
|
4
|
+
# abstract?, _prefixes, and ParameterEncoding.
|
|
5
|
+
# Each uses per-Ractor IES caches and guards Ractor.main? before reading
|
|
6
|
+
# class ivars.
|
|
7
|
+
|
|
8
|
+
module RactorRailsShim
|
|
9
|
+
# ActionController / AbstractController constants that need to be made shareable.
|
|
10
|
+
SHAREABLE_CONSTANTS.concat([
|
|
11
|
+
"ActionController::Rendering::RENDER_FORMATS_IN_PRIORITY",
|
|
12
|
+
"ActionController::Base::PROTECTED_IVARS",
|
|
13
|
+
"AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES",
|
|
14
|
+
# Strong-parameters scalar allow-list, read by permitted_scalar? on every
|
|
15
|
+
# permit/require. An Array of classes -> not shareable by default.
|
|
16
|
+
"ActionController::Parameters::PERMITTED_SCALAR_TYPES",
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Patch ActionController::ParameterEncoding::ClassMethods#action_encoding_template
|
|
21
|
+
# to not read @_parameter_encodings (a raw class ivar) from a worker
|
|
22
|
+
# Ractor. The default is an empty-ish Hash; for a frozen shared app workers
|
|
23
|
+
# get an empty frozen Hash (no per-action param encodings — correct for
|
|
24
|
+
# apps that don't declare `parameter_encoding`, e.g. the health controller).
|
|
25
|
+
def _install_parameter_encoding_patch
|
|
26
|
+
return if @param_encoding_patched
|
|
27
|
+
@param_encoding_patched = true
|
|
28
|
+
_register_patch :parameter_encoding, "8.1"
|
|
29
|
+
return unless defined?(::ActionController::ParameterEncoding)
|
|
30
|
+
pe = ::ActionController::ParameterEncoding::ClassMethods
|
|
31
|
+
pe.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
32
|
+
def action_encoding_template(action)
|
|
33
|
+
enc = if Ractor.main?
|
|
34
|
+
instance_variable_defined?(:@_parameter_encodings) ? @_parameter_encodings : nil
|
|
35
|
+
else
|
|
36
|
+
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_param_encodings]
|
|
37
|
+
end
|
|
38
|
+
if enc && enc.has_key?(action.to_s)
|
|
39
|
+
enc[action.to_s]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
RUBY
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Patch AbstractController::Base.controller_path to not write/read the
|
|
46
|
+
# @controller_path class ivar from a worker Ractor. Also patches
|
|
47
|
+
# action_methods, clear_action_methods!, abstract!, abstract?, and
|
|
48
|
+
# _prefixes to route through IES or use the shareable fallback.
|
|
49
|
+
def _install_abstract_controller_patch
|
|
50
|
+
return if @abstract_controller_patched
|
|
51
|
+
@abstract_controller_patched = true
|
|
52
|
+
_register_patch :abstract_controller, "8.1"
|
|
53
|
+
return unless defined?(::AbstractController::Base)
|
|
54
|
+
ac = ::AbstractController::Base
|
|
55
|
+
|
|
56
|
+
# Populate the shareable abstract registry from every loaded controller
|
|
57
|
+
# class's @abstract ivar (set by abstract! / inherited at boot). Workers
|
|
58
|
+
# read this via the patched abstract? (per-class values can't live in
|
|
59
|
+
# per-Ractor IES).
|
|
60
|
+
registry = {}
|
|
61
|
+
ac.descendants.each do |klass|
|
|
62
|
+
begin
|
|
63
|
+
registry[klass] = klass.instance_variable_get(:@abstract) if klass.instance_variable_defined?(:@abstract)
|
|
64
|
+
rescue => e
|
|
65
|
+
# ignore — best-effort
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
registry[ac] = ac.instance_variable_get(:@abstract) if ac.instance_variable_defined?(:@abstract)
|
|
69
|
+
registry.freeze
|
|
70
|
+
Ractor.make_shareable(registry)
|
|
71
|
+
self._abstract_registry = registry
|
|
72
|
+
ac.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
73
|
+
def controller_path
|
|
74
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_controller_path_cache] ||= {})
|
|
75
|
+
v = cache[self]
|
|
76
|
+
return v if v
|
|
77
|
+
if Ractor.main? && instance_variable_defined?(:@controller_path)
|
|
78
|
+
v = @controller_path
|
|
79
|
+
cache[self] = v
|
|
80
|
+
return v
|
|
81
|
+
end
|
|
82
|
+
computed = anonymous? ? nil : name.delete_suffix("Controller").underscore
|
|
83
|
+
cache[self] = computed
|
|
84
|
+
computed
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# action_methods: `@action_methods ||= public_instance_methods(true) -
|
|
88
|
+
# internal_methods).map(&:name).to_set` — raw class-ivar lazy init.
|
|
89
|
+
# The value is a Set of Symbols (shareable once frozen). Route through
|
|
90
|
+
# IES; workers compute it from public_instance_methods (no ivar read)
|
|
91
|
+
# and cache in their own slot. Read per-request during dispatch.
|
|
92
|
+
def action_methods
|
|
93
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_action_methods_cache] ||= {})
|
|
94
|
+
v = cache[self]
|
|
95
|
+
return v if v
|
|
96
|
+
if Ractor.main? && instance_variable_defined?(:@action_methods)
|
|
97
|
+
v = @action_methods
|
|
98
|
+
cache[self] = v
|
|
99
|
+
return v
|
|
100
|
+
end
|
|
101
|
+
methods = public_instance_methods(true) - internal_methods
|
|
102
|
+
methods.map!(&:name)
|
|
103
|
+
computed = methods.to_set
|
|
104
|
+
cache[self] = computed
|
|
105
|
+
computed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def clear_action_methods!
|
|
109
|
+
if Ractor.main?
|
|
110
|
+
@action_methods = nil
|
|
111
|
+
end
|
|
112
|
+
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_action_methods_cache] = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# abstract! / abstract / abstract? — raw class ivar (@abstract), a
|
|
116
|
+
# per-CLASS boolean. IES is per-Ractor (single value), so we can't use a
|
|
117
|
+
# single IES key for all classes. Instead use a shareable registry
|
|
118
|
+
# (Hash class→bool) built at prepare_for_ractors! time. Workers read
|
|
119
|
+
# the registry; main reads its live @abstract ivar (set by abstract!
|
|
120
|
+
# / inherited). `internal_methods` loops on abstract?.
|
|
121
|
+
def abstract!
|
|
122
|
+
RactorRailsShim._abstract_registry[self] = true if Ractor.main?
|
|
123
|
+
@abstract = true if Ractor.main?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def abstract
|
|
127
|
+
if Ractor.main? && instance_variable_defined?(:@abstract)
|
|
128
|
+
@abstract
|
|
129
|
+
else
|
|
130
|
+
(RactorRailsShim._abstract_registry || RactorRailsShim::ABSTRACT_REGISTRY)[self] || false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
alias_method :abstract?, :abstract
|
|
134
|
+
RUBY
|
|
135
|
+
|
|
136
|
+
# Patch ActionView::ViewPaths::ClassMethods#_prefixes (overrides any
|
|
137
|
+
# Base version). Original: `@_prefixes ||= begin; return local_prefixes
|
|
138
|
+
# if superclass.abstract?; local_prefixes + superclass._prefixes; end`.
|
|
139
|
+
# @_prefixes is a per-CLASS class ivar (workers can't read). Recurse
|
|
140
|
+
# using the patched abstract? and cache in a per-Ractor Hash by class.
|
|
141
|
+
if defined?(::ActionView::ViewPaths::ClassMethods)
|
|
142
|
+
vp = ::ActionView::ViewPaths::ClassMethods
|
|
143
|
+
vp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
144
|
+
def _prefixes
|
|
145
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_vp_prefixes_cache] ||= {})
|
|
146
|
+
v = cache[self]
|
|
147
|
+
return v if v
|
|
148
|
+
if Ractor.main? && instance_variable_defined?(:@_prefixes)
|
|
149
|
+
v = @_prefixes
|
|
150
|
+
cache[self] = v
|
|
151
|
+
return v
|
|
152
|
+
end
|
|
153
|
+
computed = if superclass.respond_to?(:abstract?) && superclass.abstract?
|
|
154
|
+
local_prefixes
|
|
155
|
+
elsif superclass.respond_to?(:_prefixes)
|
|
156
|
+
local_prefixes + superclass._prefixes
|
|
157
|
+
else
|
|
158
|
+
local_prefixes
|
|
159
|
+
end
|
|
160
|
+
cache[self] = computed
|
|
161
|
+
computed
|
|
162
|
+
end
|
|
163
|
+
RUBY
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# AbstractController::UrlFor::ClassMethods#action_methods ALSO has a
|
|
167
|
+
# `@action_methods ||= ...` lazy init (it overrides Base.action_methods
|
|
168
|
+
# to subtract route helper names). Patch it the same way.
|
|
169
|
+
if defined?(::AbstractController::UrlFor::ClassMethods)
|
|
170
|
+
url_for_cm = ::AbstractController::UrlFor::ClassMethods
|
|
171
|
+
url_for_cm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
172
|
+
def action_methods
|
|
173
|
+
cache = (ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_url_for_action_methods_cache] ||= {})
|
|
174
|
+
v = cache[self]
|
|
175
|
+
return v if v
|
|
176
|
+
if Ractor.main? && instance_variable_defined?(:@action_methods)
|
|
177
|
+
v = @action_methods
|
|
178
|
+
cache[self] = v
|
|
179
|
+
return v
|
|
180
|
+
end
|
|
181
|
+
# NOTE: the original reads `@action_methods ||= if _routes; super -
|
|
182
|
+
# _routes.named_routes.helper_names; else; super; end`. But
|
|
183
|
+
# `_routes` is a singleton method defined via `define_method` with
|
|
184
|
+
# a block (route_set.rb:610), capturing the defining Ractor's
|
|
185
|
+
# binding → "defined with an un-shareable Proc in a different
|
|
186
|
+
# Ractor" when called from a worker. Instead, read the route set
|
|
187
|
+
# directly from the shareable Rails.application (frozen, shared).
|
|
188
|
+
base = super
|
|
189
|
+
routes = Ractor.main? ? (respond_to?(:_routes) ? _routes : nil) : (defined?(::Rails) && ::Rails.application ? ::Rails.application.routes : nil)
|
|
190
|
+
computed = if routes
|
|
191
|
+
base - routes.named_routes.helper_names
|
|
192
|
+
else
|
|
193
|
+
base
|
|
194
|
+
end
|
|
195
|
+
cache[self] = computed
|
|
196
|
+
computed
|
|
197
|
+
end
|
|
198
|
+
RUBY
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Patch ActionController::Metal.controller_name (a class method). It
|
|
203
|
+
# memoizes its computed String in a lazy class ivar (`@controller_name ||=`),
|
|
204
|
+
# which a worker Ractor cannot write. Route the cache through
|
|
205
|
+
# IsolatedExecutionState keyed by the class name so each Ractor builds its
|
|
206
|
+
# own copy; the computation is deterministic from the class name.
|
|
207
|
+
def _install_action_controller_controller_name_patch
|
|
208
|
+
return if @action_controller_controller_name_patched
|
|
209
|
+
@action_controller_controller_name_patched = true
|
|
210
|
+
_register_patch :action_controller_controller_name, "8.1"
|
|
211
|
+
return unless defined?(::ActionController::Metal)
|
|
212
|
+
::ActionController::Metal.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
213
|
+
def controller_name
|
|
214
|
+
key = :"ractor_rails_shim_controller_name_\#{name}"
|
|
215
|
+
v = ActiveSupport::IsolatedExecutionState[key]
|
|
216
|
+
return v if v
|
|
217
|
+
cn = (name.demodulize.delete_suffix("Controller").underscore unless anonymous?)
|
|
218
|
+
ActiveSupport::IsolatedExecutionState[key] = cn
|
|
219
|
+
cn
|
|
220
|
+
end
|
|
221
|
+
RUBY
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# In the shared :ractor graph, Devise's engine controllers (e.g.
|
|
225
|
+
# Devise::SessionsController) end up with a nil `csrf_token_storage_strategy`
|
|
226
|
+
# at request time — the value is dropped when make_app_shareable! deep-freezes
|
|
227
|
+
# the app (RequestForgeryProtection sets it only on ActionController::Base.config,
|
|
228
|
+
# and the per-controller frozen config copy loses it). A worker then raises
|
|
229
|
+
# NoMethodError on `reset_csrf_token` during `reset_session` (logout /
|
|
230
|
+
# sign_out). Guard the reset so a missing strategy is a no-op — `reset_session`
|
|
231
|
+
# regenerates the session id anyway, discarding the CSRF token.
|
|
232
|
+
def _install_csrf_reset_patch
|
|
233
|
+
return if @csrf_reset_patched
|
|
234
|
+
@csrf_reset_patched = true
|
|
235
|
+
_register_patch :csrf_reset, "8.1"
|
|
236
|
+
return unless defined?(::ActionController::RequestForgeryProtection)
|
|
237
|
+
rfp = ::ActionController::RequestForgeryProtection
|
|
238
|
+
rfp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
239
|
+
def reset_csrf_token(request) # :doc:
|
|
240
|
+
request.env.delete(CSRF_TOKEN)
|
|
241
|
+
strat = csrf_token_storage_strategy
|
|
242
|
+
strat.reset(request) if strat
|
|
243
|
+
end
|
|
244
|
+
RUBY
|
|
245
|
+
|
|
246
|
+
# `csrf_token_storage_strategy` and `forgery_protection_strategy` are
|
|
247
|
+
# both delegated to the controller `config` (a class_attribute). In the
|
|
248
|
+
# shared :ractor graph the per-controller frozen `config` copy loses
|
|
249
|
+
# them, so a worker reads nil and CSRF token handling raises
|
|
250
|
+
# ("undefined method 'fetch' for nil" / "undefined method 'new' for
|
|
251
|
+
# nil"). Default to the standard SessionStore / Exception strategies so
|
|
252
|
+
# token ISSUANCE and VALIDATION work in workers.
|
|
253
|
+
#
|
|
254
|
+
# NOTE: prepending to RequestForgeryProtection (a module) would NOT
|
|
255
|
+
# reach the controllers, because ActionController::Base already
|
|
256
|
+
# *included* it before this patch runs. Prepend to the BASE CLASS so
|
|
257
|
+
# every controller (incl. Devise subclasses) picks up the default.
|
|
258
|
+
# The defaults are referenced via their constant paths (not captured
|
|
259
|
+
# locals) because `def` bodies do not close over enclosing locals.
|
|
260
|
+
::ActionController::Base.prepend(Module.new do
|
|
261
|
+
def csrf_token_storage_strategy
|
|
262
|
+
super || ::ActionController::RequestForgeryProtection::SessionStore.new
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def forgery_protection_strategy
|
|
266
|
+
super || ::ActionController::RequestForgeryProtection::ProtectionMethods::Exception
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Delegated to `config` (a class_attribute) which loses its value when
|
|
270
|
+
# make_app_shareable! deep-freezes the shared graph, so a worker reads
|
|
271
|
+
# nil. With a nil param key, form_authenticity_param reads params[nil]
|
|
272
|
+
# and CSRF VALIDATION rejects every POST (even with a valid token),
|
|
273
|
+
# because the token is carried under the real key (:authenticity_token).
|
|
274
|
+
# Default to the standard param name so validation can find the token.
|
|
275
|
+
def request_forgery_protection_token
|
|
276
|
+
super || :authenticity_token
|
|
277
|
+
end
|
|
278
|
+
end)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Patch the flash-type helper methods (`notice`, `alert`, ...) defined by
|
|
282
|
+
# `ActionController::Metal::Flash#add_flash_types` via
|
|
283
|
+
# `define_method(type) { request.flash[type] }`. That block is compiled
|
|
284
|
+
# in the MAIN Ractor, so calling it from a worker Ractor raises
|
|
285
|
+
# "defined with an un-shareable Proc in a different Ractor". Redefine each
|
|
286
|
+
# flash type as a string-eval'd method (no captured binding) so it is
|
|
287
|
+
# callable from any Ractor. Called at prepare_for_ractors! time, after
|
|
288
|
+
# the controllers are loaded and the types are known.
|
|
289
|
+
def _install_flash_helpers_patch
|
|
290
|
+
return if @flash_helpers_patched
|
|
291
|
+
@flash_helpers_patched = true
|
|
292
|
+
_register_patch :flash_helpers, "8.1"
|
|
293
|
+
return unless defined?(::ActionController::Base)
|
|
294
|
+
types = ::ActionController::Base._flash_types rescue []
|
|
295
|
+
types.each do |type|
|
|
296
|
+
::ActionController::Base.class_eval "def #{type}; request.flash[#{type.inspect}]; end"
|
|
297
|
+
::ActionController::Base.send(:private, type) if ::ActionController::Base.private_method_defined?(type) rescue nil
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|