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,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch the Rails module's class-level accessors (Rails.application,
|
|
4
|
+
# Rails.env, Rails.cache, etc.) to route through IsolatedExecutionState.
|
|
5
|
+
# The generic constant-sharing utilities (make_constant_shareable,
|
|
6
|
+
# split_const_path, do_install_shareable_constants, install_shareable_constants,
|
|
7
|
+
# shareable_constants) now live in core.rb (alongside SHAREABLE_CONSTANTS).
|
|
8
|
+
|
|
9
|
+
module RactorRailsShim
|
|
10
|
+
class << self
|
|
11
|
+
# Railties constants (the rest are in their per-concern files: rack.rb,
|
|
12
|
+
# action_view.rb, action_controller.rb, action_dispatch.rb,
|
|
13
|
+
# active_support.rb, warden.rb). Each file concats into
|
|
14
|
+
# RactorRailsShim::SHAREABLE_CONSTANTS (defined empty in core.rb).
|
|
15
|
+
SHAREABLE_CONSTANTS.concat([
|
|
16
|
+
"Rails::Railtie::ABSTRACT_RAILTIES",
|
|
17
|
+
"Rails::AppLoader::EXECUTABLES",
|
|
18
|
+
"Rails::Command::HELP_MAPPINGS",
|
|
19
|
+
"Rails::Command::VERSION_MAPPINGS",
|
|
20
|
+
"Rails::Application::INITIAL_VARIABLES",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
# Patch the Rails module's class-level accessors (Rails.application,
|
|
24
|
+
# Rails.env, Rails.cache, etc.) to route through IsolatedExecutionState.
|
|
25
|
+
# Defers via a load hook if Rails isn't defined yet (the config/boot.rb case).
|
|
26
|
+
def install_rails_module
|
|
27
|
+
_register_patch :rails_module, "8.1"
|
|
28
|
+
if defined?(::Rails)
|
|
29
|
+
patch_rails_module!(::Rails)
|
|
30
|
+
else
|
|
31
|
+
install_rails_load_hook
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Defer the Rails-module patch until Rails is defined. A TracePoint on
|
|
36
|
+
# :class fires when `rails.rb` opens `module Rails` (module bodies fire
|
|
37
|
+
# as :class); once the constant is assigned, we patch once and disable
|
|
38
|
+
# the hook.
|
|
39
|
+
def install_rails_load_hook
|
|
40
|
+
return if @rails_load_hook_installed
|
|
41
|
+
@rails_load_hook_installed = true
|
|
42
|
+
|
|
43
|
+
@rails_tp = TracePoint.new(:class) do |trace|
|
|
44
|
+
if defined?(::Rails) && !@rails_module_patched
|
|
45
|
+
@rails_tp.disable
|
|
46
|
+
patch_rails_module!(::Rails)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
@rails_tp.enable
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The actual Rails-module patch. Idempotent. Must be called from the
|
|
53
|
+
# main Ractor (it prepends onto Rails.singleton_class).
|
|
54
|
+
def patch_rails_module!(mod)
|
|
55
|
+
return if @rails_module_patched
|
|
56
|
+
@rails_module_patched = true
|
|
57
|
+
do_install_shareable_constants
|
|
58
|
+
_patch_rails_module_body(mod)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def _patch_rails_module_body(mod)
|
|
62
|
+
k = KEYS
|
|
63
|
+
|
|
64
|
+
# Register the Rails module accessors in CLASS_ATTRIBUTES so the
|
|
65
|
+
# shareable-fallback builder captures their main-ractor values at
|
|
66
|
+
# prepare_for_ractors! / make_app_shareable! time and exposes them to
|
|
67
|
+
# worker Ractors (e.g. Rails.logger is read per-request by
|
|
68
|
+
# Rails::Rack::SilenceRequest). `application` is NOT registered — workers
|
|
69
|
+
# get the shared app via Ractor.new(app), not via Rails.application.
|
|
70
|
+
CLASS_ATTRIBUTES << ["Rails", :logger, k[:logger], nil]
|
|
71
|
+
CLASS_ATTRIBUTES << ["Rails", :cache, k[:cache], nil]
|
|
72
|
+
CLASS_ATTRIBUTES << ["Rails", :backtrace_cleaner, k[:backtrace_cleaner], nil]
|
|
73
|
+
CLASS_ATTRIBUTES << ["Rails", :app_class, k[:app_class], nil]
|
|
74
|
+
|
|
75
|
+
# We PREPEND a module onto Rails.singleton_class rather than redefine
|
|
76
|
+
# the methods directly, because Rails defines its own `application`,
|
|
77
|
+
# `env`, etc. LATER in rails.rb (`class << self; def application; ...`).
|
|
78
|
+
# A direct module_eval redefinition would be clobbered when Rails'
|
|
79
|
+
# own `def` runs afterward. A prepended module sits in front of the
|
|
80
|
+
# singleton class in the method lookup chain and survives a later
|
|
81
|
+
# `def` on the same class — so our IES-routed reader stays in front.
|
|
82
|
+
# We call `super` to fall back to Rails' original method for the
|
|
83
|
+
# main-ractor lazy-init path (which reads the @application ivar —
|
|
84
|
+
# safe in the main ractor).
|
|
85
|
+
patch = Module.new
|
|
86
|
+
patch.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
87
|
+
# application: IES first; main ractor falls back to Rails' own
|
|
88
|
+
# lazy init via super. Worker ractors return nil (their own IES
|
|
89
|
+
# slot is empty until they boot their own app).
|
|
90
|
+
def application
|
|
91
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:application].inspect}]
|
|
92
|
+
return v unless v.nil?
|
|
93
|
+
if Ractor.main?
|
|
94
|
+
super
|
|
95
|
+
else
|
|
96
|
+
RactorRailsShim.const_defined?(:SHAREABLE_APP) ? RactorRailsShim::SHAREABLE_APP : nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def application=(val)
|
|
101
|
+
ActiveSupport::IsolatedExecutionState[#{k[:application].inspect}] = val
|
|
102
|
+
super if Ractor.main?
|
|
103
|
+
val
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Simple accessors: app_class, cache, logger, backtrace_cleaner.
|
|
107
|
+
# Workers fall back to the shareable fallback (built from main's
|
|
108
|
+
# value at make_app_shareable! time) when their own IES slot is empty.
|
|
109
|
+
def app_class
|
|
110
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:app_class].inspect}]
|
|
111
|
+
return v unless v.nil?
|
|
112
|
+
return super if Ractor.main?
|
|
113
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{k[:app_class].inspect}]
|
|
114
|
+
end
|
|
115
|
+
def app_class=(val)
|
|
116
|
+
ActiveSupport::IsolatedExecutionState[#{k[:app_class].inspect}] = val
|
|
117
|
+
super if Ractor.main?
|
|
118
|
+
val
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cache
|
|
122
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:cache].inspect}]
|
|
123
|
+
return v unless v.nil?
|
|
124
|
+
return super if Ractor.main?
|
|
125
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{k[:cache].inspect}]
|
|
126
|
+
end
|
|
127
|
+
def cache=(val)
|
|
128
|
+
ActiveSupport::IsolatedExecutionState[#{k[:cache].inspect}] = val
|
|
129
|
+
super if Ractor.main?
|
|
130
|
+
val
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def logger
|
|
134
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:logger].inspect}]
|
|
135
|
+
return v unless v.nil?
|
|
136
|
+
return super if Ractor.main?
|
|
137
|
+
# Loggers are intrinsically mutable (formatters hold tag stacks,
|
|
138
|
+
# logdevs hold IO + Mutex) and can't be shared read-only. Build a
|
|
139
|
+
# per-worker ActiveSupport::BroadcastLogger (which mixes in
|
|
140
|
+
# LoggerSilence#silence, used by Rails::Rack::SilenceRequest)
|
|
141
|
+
# writing to $stderr (each Ractor has its own $stderr stream) and
|
|
142
|
+
# cache it in IES so subsequent reads return the same instance.
|
|
143
|
+
built = ActiveSupport::BroadcastLogger.new(Logger.new($stderr))
|
|
144
|
+
ActiveSupport::IsolatedExecutionState[#{k[:logger].inspect}] = built
|
|
145
|
+
built
|
|
146
|
+
end
|
|
147
|
+
def logger=(val)
|
|
148
|
+
ActiveSupport::IsolatedExecutionState[#{k[:logger].inspect}] = val
|
|
149
|
+
super if Ractor.main?
|
|
150
|
+
val
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def backtrace_cleaner
|
|
154
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:backtrace_cleaner].inspect}]
|
|
155
|
+
return v unless v.nil?
|
|
156
|
+
return super if Ractor.main?
|
|
157
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{k[:backtrace_cleaner].inspect}]
|
|
158
|
+
end
|
|
159
|
+
def backtrace_cleaner=(val)
|
|
160
|
+
ActiveSupport::IsolatedExecutionState[#{k[:backtrace_cleaner].inspect}] = val
|
|
161
|
+
super if Ractor.main?
|
|
162
|
+
val
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# env: worker ractors build their own EnvironmentInquirer from ENV
|
|
166
|
+
# (no @ _env ivar to read). Main ractor falls back to super, which
|
|
167
|
+
# lazily builds and caches in @_env.
|
|
168
|
+
def env
|
|
169
|
+
v = ActiveSupport::IsolatedExecutionState[#{k[:env].inspect}]
|
|
170
|
+
return v unless v.nil?
|
|
171
|
+
if Ractor.main?
|
|
172
|
+
super
|
|
173
|
+
else
|
|
174
|
+
built = ActiveSupport::EnvironmentInquirer.new(
|
|
175
|
+
ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
|
|
176
|
+
)
|
|
177
|
+
ActiveSupport::IsolatedExecutionState[#{k[:env].inspect}] = built
|
|
178
|
+
built
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def env=(val)
|
|
183
|
+
v = ActiveSupport::EnvironmentInquirer.new(val)
|
|
184
|
+
ActiveSupport::IsolatedExecutionState[#{k[:env].inspect}] = v
|
|
185
|
+
super if Ractor.main?
|
|
186
|
+
v
|
|
187
|
+
end
|
|
188
|
+
RUBY
|
|
189
|
+
mod.singleton_class.prepend(patch)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ractor-safe URL helper generation.
|
|
4
|
+
#
|
|
5
|
+
# Rails builds every URL helper method (`posts_path`, `_routes`,
|
|
6
|
+
# `_generate_paths_by_default`, the `direct`/`resolve` helpers, …) with
|
|
7
|
+
# `define_method(&block)` / `redefine_method(&block)`, and uses the
|
|
8
|
+
# `RouteSet::PATH` / `RouteSet::UNKNOWN` lambdas as the `url_strategy` callable.
|
|
9
|
+
# All of those are Procs created in the main Ractor; calling them from a worker
|
|
10
|
+
# Ractor that shares the frozen app graph raises
|
|
11
|
+
# "defined with an un-shareable Proc in a different Ractor".
|
|
12
|
+
#
|
|
13
|
+
# Fixes (must run BEFORE routes are drawn, i.e. before
|
|
14
|
+
# `Rails.application.initialize!`):
|
|
15
|
+
#
|
|
16
|
+
# * Replace `RouteSet::PATH` / `RouteSet::UNKNOWN` with shareable `Method`
|
|
17
|
+
# objects (a `Method` is Ractor-shareable, unlike a lambda).
|
|
18
|
+
# * Regenerate the named helpers and `_routes` / `_generate_paths_by_default`
|
|
19
|
+
# as compiled `def` methods (shareable) instead of `define_method` blocks.
|
|
20
|
+
# The per-helper `CustomUrlHelper`/`UrlHelper` object is stored on the
|
|
21
|
+
# module as a constant and looked up inside the `def`.
|
|
22
|
+
#
|
|
23
|
+
# NOTE: all `module_eval` bodies are built with string *concatenation* (no
|
|
24
|
+
# `#{}` inside string literals) so they are not interpolated at patch-apply
|
|
25
|
+
# time. Method-level `#{}` in symbol literals (e.g. `:"RRS_HELPER_#{name}"`)
|
|
26
|
+
# is ordinary Ruby interpolation evaluated when the generated method runs.
|
|
27
|
+
|
|
28
|
+
module RactorRailsShim
|
|
29
|
+
class << self
|
|
30
|
+
def install_route_helpers_patch
|
|
31
|
+
return if @route_helpers_patched
|
|
32
|
+
@route_helpers_patched = true
|
|
33
|
+
_register_patch :route_helpers, "8.1"
|
|
34
|
+
return unless defined?(::ActionDispatch::Routing::RouteSet)
|
|
35
|
+
|
|
36
|
+
rs = ::ActionDispatch::Routing::RouteSet
|
|
37
|
+
|
|
38
|
+
# 1. Replace the PATH / UNKNOWN lambdas with shareable Method objects.
|
|
39
|
+
begin
|
|
40
|
+
rs.send(:remove_const, :PATH) if rs.const_defined?(:PATH, false)
|
|
41
|
+
rescue
|
|
42
|
+
end
|
|
43
|
+
rs.const_set(:PATH, ::ActionDispatch::Http::URL.method(:path_for)) rescue nil
|
|
44
|
+
begin
|
|
45
|
+
rs.send(:remove_const, :UNKNOWN) if rs.const_defined?(:UNKNOWN, false)
|
|
46
|
+
rescue
|
|
47
|
+
end
|
|
48
|
+
rs.const_set(:UNKNOWN, ::ActionDispatch::Http::URL.method(:url_for)) rescue nil
|
|
49
|
+
|
|
50
|
+
# 2. Named route helpers -> compiled `def` (regular `resources` routes).
|
|
51
|
+
rs.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
52
|
+
def define_url_helper(mod, name, helper, url_strategy)
|
|
53
|
+
const_name = :"RRS_HELPER_\#{name}"
|
|
54
|
+
mod.const_set(const_name, helper)
|
|
55
|
+
strategy = url_strategy.equal?(PATH) ? :PATH : :UNKNOWN
|
|
56
|
+
body = "def " + name.to_s + "(*args)\\n" \
|
|
57
|
+
" last = args.last\\n" \
|
|
58
|
+
" options = case last\\n" \
|
|
59
|
+
" when Hash then args.pop\\n" \
|
|
60
|
+
" when ActionController::Parameters then args.pop.to_h\\n" \
|
|
61
|
+
" end\\n" \
|
|
62
|
+
" ::ActionDispatch::Routing::RouteSet.const_get(" + const_name.inspect + ").call(self, " + name.inspect + ", args, options, ::ActionDispatch::Routing::RouteSet.const_get(" + strategy.inspect + "))\\n" \
|
|
63
|
+
"end"
|
|
64
|
+
mod.module_eval(body, __FILE__, __LINE__ + 1)
|
|
65
|
+
end
|
|
66
|
+
RUBY
|
|
67
|
+
|
|
68
|
+
# 3. `direct` / `resolve` helpers -> compiled `def`.
|
|
69
|
+
if defined?(::ActionDispatch::Routing::RouteSet::NamedRouteCollection)
|
|
70
|
+
::ActionDispatch::Routing::RouteSet::NamedRouteCollection.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
71
|
+
def add_url_helper(name, defaults, &block)
|
|
72
|
+
helper = CustomUrlHelper.new(name, defaults, &block)
|
|
73
|
+
path_name = :"\#{name}_path"
|
|
74
|
+
url_name = :"\#{name}_url"
|
|
75
|
+
@path_helpers_module.const_set(:"RRS_HELPER_\#{path_name}", helper)
|
|
76
|
+
@url_helpers_module.const_set(:"RRS_HELPER_\#{url_name}", helper)
|
|
77
|
+
pbody = "def " + path_name.to_s + "(*args)\\n const_get(:\\\"RRS_HELPER_" + path_name.to_s + "\\\").call(self, args, true)\\nend"
|
|
78
|
+
ubody = "def " + url_name.to_s + "(*args)\\n const_get(:\\\"RRS_HELPER_" + url_name.to_s + "\\\").call(self, args, false)\\nend"
|
|
79
|
+
@path_helpers_module.module_eval(pbody, __FILE__, __LINE__ + 1)
|
|
80
|
+
@url_helpers_module.module_eval(ubody, __FILE__, __LINE__ + 1)
|
|
81
|
+
@path_helpers << path_name
|
|
82
|
+
@url_helpers << url_name
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
RUBY
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 4. `_routes` / `_generate_paths_by_default` -> compiled `def`.
|
|
89
|
+
rs.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
90
|
+
alias_method :_rrs_orig_generate_url_helpers, :generate_url_helpers
|
|
91
|
+
def generate_url_helpers(supports_path)
|
|
92
|
+
routes = self
|
|
93
|
+
mod = _rrs_orig_generate_url_helpers(supports_path)
|
|
94
|
+
mod.module_eval("def _routes\n @_routes || ::Rails.application.routes\nend", __FILE__, __LINE__ + 1)
|
|
95
|
+
mod.module_eval("def _generate_paths_by_default\n " + supports_path.inspect + "\nend", __FILE__, __LINE__ + 1)
|
|
96
|
+
mod
|
|
97
|
+
end
|
|
98
|
+
RUBY
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The module-level singleton `_routes` (set via `redefine_singleton_method`
|
|
102
|
+
# in the gem's `included` block) is still a `define_method` block. It is not
|
|
103
|
+
# on the request hot-path, but make it shareable anyway. Called after the
|
|
104
|
+
# module has been included into ActionView/ActionController (i.e. from
|
|
105
|
+
# `prepare_for_ractors!`).
|
|
106
|
+
def fix_url_helpers_singleton_routes
|
|
107
|
+
return unless defined?(::Rails) && ::Rails.application
|
|
108
|
+
return unless ::Rails.application.routes.respond_to?(:url_helpers)
|
|
109
|
+
mod = ::Rails.application.routes.url_helpers
|
|
110
|
+
return unless mod.respond_to?(:singleton_class)
|
|
111
|
+
mod.singleton_class.class_eval do
|
|
112
|
+
def _routes
|
|
113
|
+
::Rails.application.routes
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
rescue
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch RubyGems `Gem.paths` (and thus `Gem.path`) to be Ractor-safe.
|
|
4
|
+
#
|
|
5
|
+
# In development, Propshaft serves assets on the fly and, on every request,
|
|
6
|
+
# runs its cache sweeper. That calls ActiveSupport::FileUpdateChecker which
|
|
7
|
+
# reads `Gem.path` -> `Gem.paths` -> the unshareable `@paths` class ivar on
|
|
8
|
+
# `Gem`. Reading that ivar from a worker Ractor raises Ractor::IsolationError
|
|
9
|
+
# ("can not get unshareable values ... @paths from Gem"). Any other dev code
|
|
10
|
+
# path that reads `Gem.path` from a worker hits the same wall.
|
|
11
|
+
#
|
|
12
|
+
# Fix: in the main Ractor `Gem.paths` behaves exactly as before (aliased to
|
|
13
|
+
# the original). In a worker Ractor we return a shareable snapshot of the
|
|
14
|
+
# main Ractor's gem paths, captured at prepare_for_ractors! time (after
|
|
15
|
+
# Bundler has configured gem paths, so the snapshot is complete).
|
|
16
|
+
|
|
17
|
+
module RactorRailsShim
|
|
18
|
+
class << self
|
|
19
|
+
# Installed at `install` (boot) time. Only redefines the reader; the
|
|
20
|
+
# snapshot is filled in later by `snapshot_gem_paths!`.
|
|
21
|
+
def install_rubygems
|
|
22
|
+
return if @rubygems_patched
|
|
23
|
+
@rubygems_patched = true
|
|
24
|
+
_register_patch :rubygems, "all"
|
|
25
|
+
return unless defined?(::Gem)
|
|
26
|
+
patch_rubygems!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def patch_rubygems!
|
|
30
|
+
return if @rubygems_method_patched
|
|
31
|
+
@rubygems_method_patched = true
|
|
32
|
+
gem = ::Gem
|
|
33
|
+
unless gem.singleton_class.method_defined?(:__shim_original_gem_paths)
|
|
34
|
+
gem.singleton_class.alias_method :__shim_original_gem_paths, :paths
|
|
35
|
+
end
|
|
36
|
+
# `def` (not `define_method`) so the method has no captured binding and
|
|
37
|
+
# is callable from any Ractor. `Ractor.main?` + a shareable constant are
|
|
38
|
+
# both Ractor-safe.
|
|
39
|
+
gem.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
40
|
+
def paths
|
|
41
|
+
if Ractor.main?
|
|
42
|
+
__shim_original_gem_paths
|
|
43
|
+
else
|
|
44
|
+
::RactorRailsShim::GEM_PATHS_SNAPSHOT
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Called from prepare_for_ractors! (post-boot, main Ractor) so the
|
|
51
|
+
# snapshot reflects Bundler-configured gem paths. Also called from
|
|
52
|
+
# install as a safety net so the constant is never undefined.
|
|
53
|
+
def snapshot_gem_paths!
|
|
54
|
+
return unless defined?(::Gem)
|
|
55
|
+
return if defined?(::RactorRailsShim::GEM_PATHS_SNAPSHOT)
|
|
56
|
+
snap = ::Gem.paths
|
|
57
|
+
begin
|
|
58
|
+
Ractor.make_shareable(snap)
|
|
59
|
+
rescue StandardError
|
|
60
|
+
snap = Ractor.make_shareable(::Gem.path)
|
|
61
|
+
end
|
|
62
|
+
::RactorRailsShim.const_set(:GEM_PATHS_SNAPSHOT, snap)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Make Rails' URL generation Ractor-safe.
|
|
4
|
+
#
|
|
5
|
+
# Two distinct problems exist in Rails' URL helpers under a frozen, shared
|
|
6
|
+
# (Ractor) graph:
|
|
7
|
+
#
|
|
8
|
+
# 1. Named route helpers, `_routes`, and `_generate_paths_by_default` are all
|
|
9
|
+
# defined with `define_method(&block)` / `redefine_method(&block)`, so their
|
|
10
|
+
# bodies are Procs created in the *main* Ractor. Any worker that calls them
|
|
11
|
+
# raises "defined with an un-shareable Proc in a different Ractor".
|
|
12
|
+
#
|
|
13
|
+
# 2. `RouteSet::PATH` / `RouteSet::UNKNOWN` are lambdas used as the
|
|
14
|
+
# `url_strategy` callable inside every helper; calling them from a worker is
|
|
15
|
+
# also un-shareable.
|
|
16
|
+
#
|
|
17
|
+
# These are addressed in `route_helpers.rb` (run *before* routes are drawn):
|
|
18
|
+
# the helper methods are regenerated as compiled `def`s and `PATH`/`UNKNOWN`
|
|
19
|
+
# are replaced with shareable `Method` objects.
|
|
20
|
+
#
|
|
21
|
+
# This file adds a defensive safety net: if any residual un-shareable error
|
|
22
|
+
# slips through, we re-dispatch the same call from this shim-defined method
|
|
23
|
+
# (whose scope is the worker's own Ractor).
|
|
24
|
+
|
|
25
|
+
module RactorRailsShim
|
|
26
|
+
class << self
|
|
27
|
+
def install_url_helpers_patch
|
|
28
|
+
return if @url_helpers_patched
|
|
29
|
+
@url_helpers_patched = true
|
|
30
|
+
_register_patch :url_helpers, "8.1"
|
|
31
|
+
return unless defined?(::ActiveRecord::Base)
|
|
32
|
+
|
|
33
|
+
if defined?(::ActionView::RoutingUrlFor)
|
|
34
|
+
::ActionView::RoutingUrlFor.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
35
|
+
alias_method :_rrs_orig_view_url_for, :url_for
|
|
36
|
+
def url_for(options = nil)
|
|
37
|
+
_rrs_orig_view_url_for(options)
|
|
38
|
+
rescue => e
|
|
39
|
+
raise unless e.message.include?("un-shareable Proc")
|
|
40
|
+
if options.is_a?(Hash) || options.is_a?(ActionController::Parameters)
|
|
41
|
+
full_url_for(options)
|
|
42
|
+
else
|
|
43
|
+
meth = _generate_paths_by_default ? :path : :url
|
|
44
|
+
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.public_send(meth)
|
|
45
|
+
builder.handle_model_call(self, options)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
RUBY
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if defined?(::ActionDispatch::Routing::UrlFor)
|
|
52
|
+
::ActionDispatch::Routing::UrlFor.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
53
|
+
alias_method :_rrs_orig_full_url_for, :full_url_for
|
|
54
|
+
def full_url_for(options = nil)
|
|
55
|
+
_rrs_orig_full_url_for(options)
|
|
56
|
+
rescue => e
|
|
57
|
+
raise unless e.message.include?("un-shareable Proc")
|
|
58
|
+
if options.is_a?(Hash) || options.is_a?(ActionController::Parameters)
|
|
59
|
+
route_name = options.delete :use_route
|
|
60
|
+
merged = options.to_h.symbolize_keys.reverse_merge!(url_options)
|
|
61
|
+
# `self._routes` is an un-shareable `define_method` block in a
|
|
62
|
+
# worker Ractor, so calling it re-raises the same error. Route the
|
|
63
|
+
# fallback through the worker-safe shareable RouteSet instead.
|
|
64
|
+
if RactorRailsShim.const_defined?(:SHAREABLE_ROUTES)
|
|
65
|
+
RactorRailsShim::SHAREABLE_ROUTES.url_for(merged, route_name)
|
|
66
|
+
else
|
|
67
|
+
_routes.url_for(merged, route_name)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url
|
|
71
|
+
builder.handle_model_call(self, options)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
RUBY
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch Warden::Hooks lazy class-ivar accessors (@_on_request ||= [] etc).
|
|
4
|
+
# Warden middleware holds 6 lazy-init class ivars for callback arrays.
|
|
5
|
+
# make_app_shareable! freezes the middleware instance; the ||= lazy init
|
|
6
|
+
# tries to WRITE on the frozen instance → IsolationError in workers.
|
|
7
|
+
# The callbacks (Procs) were registered at boot and already ran in main;
|
|
8
|
+
# workers treat them as empty (correct for a read-only shared app).
|
|
9
|
+
|
|
10
|
+
module RactorRailsShim
|
|
11
|
+
# Devise constants that need to be made shareable.
|
|
12
|
+
SHAREABLE_CONSTANTS.concat([
|
|
13
|
+
"Devise::ParameterSanitizer::DEFAULT_PERMITTED_ATTRIBUTES",
|
|
14
|
+
"Devise::Mapping::DEFAULTS",
|
|
15
|
+
"Devise::DEVS",
|
|
16
|
+
"Devise::URLS",
|
|
17
|
+
"Devise::STRATEGIES",
|
|
18
|
+
"Devise::CONTROLLERS",
|
|
19
|
+
"Devise::MODULES",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
# Source-location constant for the Devise scope constraint Proc (moved from
|
|
23
|
+
# make_shareable.rb so the Devise-related callable lives with the Devise patch).
|
|
24
|
+
DEVISE_SCOPE_LOC = "/devise/rails/routes.rb".freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def _install_warden_hooks_patch
|
|
28
|
+
return if @warden_patched
|
|
29
|
+
@warden_patched = true
|
|
30
|
+
_register_patch :warden_hooks, "8.1"
|
|
31
|
+
return unless defined?(::Warden::Hooks)
|
|
32
|
+
::Warden::Hooks.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
33
|
+
def _after_set_user
|
|
34
|
+
if Ractor.main? && instance_variable_defined?(:@_after_set_user)
|
|
35
|
+
@_after_set_user
|
|
36
|
+
else
|
|
37
|
+
[]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
def _before_failure
|
|
41
|
+
if Ractor.main? && instance_variable_defined?(:@_before_failure)
|
|
42
|
+
@_before_failure
|
|
43
|
+
else
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
def _after_failed_fetch
|
|
48
|
+
if Ractor.main? && instance_variable_defined?(:@_after_failed_fetch)
|
|
49
|
+
@_after_failed_fetch
|
|
50
|
+
else
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
def _before_logout
|
|
55
|
+
if Ractor.main? && instance_variable_defined?(:@_before_logout)
|
|
56
|
+
@_before_logout
|
|
57
|
+
else
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
def _on_request
|
|
62
|
+
if Ractor.main? && instance_variable_defined?(:@_on_request)
|
|
63
|
+
@_on_request
|
|
64
|
+
else
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Patch Warden::Strategies#_strategies. The strategy registry is a lazy
|
|
72
|
+
# class ivar (`@strategies ||= {}`) on the Warden::Strategies module; a
|
|
73
|
+
# worker Ractor reading it raises "can not get unshareable values from
|
|
74
|
+
# instance variables of classes/modules from non-main Ractors" (Devise's
|
|
75
|
+
# `current_user` / `user_signed_in?` in a layout hits
|
|
76
|
+
# Warden::Strategies[label] -> _strategies). Capture the (shareable)
|
|
77
|
+
# registry in main and expose it via a constant that workers read.
|
|
78
|
+
def _install_warden_strategies_patch
|
|
79
|
+
return if @warden_strategies_patched
|
|
80
|
+
@warden_strategies_patched = true
|
|
81
|
+
_register_patch :warden_strategies, "8.1"
|
|
82
|
+
return unless defined?(::Warden::Strategies)
|
|
83
|
+
::Warden::Strategies.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
84
|
+
def _strategies
|
|
85
|
+
if Ractor.main?
|
|
86
|
+
@strategies ||= {}
|
|
87
|
+
else
|
|
88
|
+
RactorRailsShim::SHAREABLE_WARDEN_STRATEGIES || {}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
RUBY
|
|
92
|
+
if Ractor.main?
|
|
93
|
+
begin
|
|
94
|
+
strat = ::Warden::Strategies.instance_variable_get(:@strategies)
|
|
95
|
+
strat = Ractor.make_shareable(strat) if strat && !Ractor.shareable?(strat)
|
|
96
|
+
RactorRailsShim.const_set(:SHAREABLE_WARDEN_STRATEGIES, strat) unless RactorRailsShim.const_defined?(:SHAREABLE_WARDEN_STRATEGIES)
|
|
97
|
+
rescue
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Warden registers per-scope session serializers with
|
|
104
|
+
# `Warden::SessionSerializer.send(:define_method, method_name, &block)`
|
|
105
|
+
# (warden-1.2.9/lib/warden/manager.rb:71). The block is created while the
|
|
106
|
+
# app boots in the main Ractor, so the resulting method is Ractor-bound:
|
|
107
|
+
# invoking it (e.g. `user_serialize`) from a worker Ractor raises
|
|
108
|
+
# "defined with an un-shareable Proc in a different Ractor".
|
|
109
|
+
#
|
|
110
|
+
# Devise's block body is simply `mapping.to.serialize_into_session(record)`.
|
|
111
|
+
# We re-register the serializers as plain `def` methods (which are NOT
|
|
112
|
+
# Ractor-bound) that delegate to the model class's own
|
|
113
|
+
# `serialize_into_session` / `serialize_from_session` class methods — both
|
|
114
|
+
# worker-safe — so the chain is callable from any worker Ractor.
|
|
115
|
+
def _install_warden_serializer_patch
|
|
116
|
+
return if @warden_serializer_patched
|
|
117
|
+
@warden_serializer_patched = true
|
|
118
|
+
_register_patch :warden_serializer, "8.1"
|
|
119
|
+
return unless defined?(::Warden::SessionSerializer)
|
|
120
|
+
|
|
121
|
+
if defined?(::Devise) && ::Devise.respond_to?(:mappings)
|
|
122
|
+
::Devise.mappings.each do |scope, mapping|
|
|
123
|
+
model = mapping.to
|
|
124
|
+
::Warden::SessionSerializer.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
125
|
+
def #{scope}_serialize(record)
|
|
126
|
+
#{model}.serialize_into_session(record)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def #{scope}_deserialize(*keys)
|
|
130
|
+
# Devise's serialize_into_session returns [[id], salt]. The key
|
|
131
|
+
# passed by Warden::SessionSerializer#fetch is that value, but in
|
|
132
|
+
# the kino :ractor worker the per-request session that Warden's
|
|
133
|
+
# serializer sees can hold an extra wrapping layer
|
|
134
|
+
# ([[[id], salt]]). Flatten so serialize_from_session(key, salt)
|
|
135
|
+
# always receives exactly two arguments.
|
|
136
|
+
#{model}.serialize_from_session(*keys.flatten)
|
|
137
|
+
end
|
|
138
|
+
RUBY
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
::Warden::SessionSerializer.class_eval do
|
|
143
|
+
unless method_defined?(:serialize)
|
|
144
|
+
def serialize(user)
|
|
145
|
+
user
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
unless method_defined?(:deserialize)
|
|
150
|
+
def deserialize(key)
|
|
151
|
+
key
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- Devise scope-constraint callable (moved from make_shareable.rb) ---
|
|
158
|
+
# Defined via string eval on the singleton class so it's referenced the same
|
|
159
|
+
# way the original code did (RactorRailsShim.singleton_class.const_get).
|
|
160
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
161
|
+
class DeviseMappingCallable
|
|
162
|
+
def initialize(mapping); @mapping = mapping; end
|
|
163
|
+
def call(request)
|
|
164
|
+
request.env["devise.mapping"] = @mapping
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
RUBY
|
|
169
|
+
|
|
170
|
+
# Build a shareable replacement for a Devise scope constraint.
|
|
171
|
+
# The original Proc (devise/rails/routes.rb:363) does:
|
|
172
|
+
# request.env["devise.mapping"] = Devise.mappings[scope]
|
|
173
|
+
# true
|
|
174
|
+
# The scope is captured in the Proc's binding. We call the original
|
|
175
|
+
# Proc once in main with a mock request to capture the mapping, then
|
|
176
|
+
# make it shareable and wrap it in a DeviseMappingCallable.
|
|
177
|
+
def _devise_mapping_replacement(proc_obj, _parent)
|
|
178
|
+
mock_env = { "devise.mapping" => nil }
|
|
179
|
+
mock_req = Struct.new(:env).new(mock_env)
|
|
180
|
+
begin
|
|
181
|
+
proc_obj.call(mock_req)
|
|
182
|
+
rescue
|
|
183
|
+
end
|
|
184
|
+
mapping = mock_env["devise.mapping"]
|
|
185
|
+
if mapping
|
|
186
|
+
mapping = _devise_mapping_snapshot(mapping)
|
|
187
|
+
end
|
|
188
|
+
if mapping
|
|
189
|
+
DeviseMappingCallable.new(mapping)
|
|
190
|
+
else
|
|
191
|
+
CallableConst.new(true)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|