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,806 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/lazy_load_hooks"
|
|
4
|
+
|
|
5
|
+
# Core: module-level constants, the install entry point, prepare_for_ractors!,
|
|
6
|
+
# version detection helpers, and the patch registry. All other patch files
|
|
7
|
+
# reopen `class << self` to add their `_install_*` methods.
|
|
8
|
+
|
|
9
|
+
module RactorRailsShim
|
|
10
|
+
# The keys under which each global is stored in IsolatedExecutionState.
|
|
11
|
+
# Namespaced to avoid collisions with Rails' own uses of IES.
|
|
12
|
+
KEYS = {
|
|
13
|
+
application: :ractor_rails_shim_application,
|
|
14
|
+
app_class: :ractor_rails_shim_app_class,
|
|
15
|
+
cache: :ractor_rails_shim_cache,
|
|
16
|
+
logger: :ractor_rails_shim_logger,
|
|
17
|
+
env: :ractor_rails_shim_env,
|
|
18
|
+
backtrace_cleaner: :ractor_rails_shim_backtrace_cleaner
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Registry of every class_attribute definition the shim's `redefine` patch
|
|
22
|
+
# has seen. Each entry is [owner_name, namespaced_name (Symbol), key (Symbol)]
|
|
23
|
+
# so that at `prepare_for_ractors!` time we can capture the main-ractor value
|
|
24
|
+
# of each attribute, make it shareable, and expose it as a read-only fallback
|
|
25
|
+
# for worker Ractors (whose own IES slot is empty). Without this, framework
|
|
26
|
+
# class config (ActionController::Base.config, etc.) is per-Ractor-nil in
|
|
27
|
+
# workers and request dispatch dies (e.g. default_static_extension -> config
|
|
28
|
+
# is nil). The fallback is ONLY read by workers; the main ractor keeps its
|
|
29
|
+
# own live (possibly mutable) value in its IES slot, untouched.
|
|
30
|
+
#
|
|
31
|
+
# The fallback table itself is built once, at prepare_for_ractors! time, and
|
|
32
|
+
# made shareable; workers read it via a constant (RactorRailsShim::SHAREABLE_FALLBACK).
|
|
33
|
+
CLASS_ATTRIBUTES = []
|
|
34
|
+
# Shareable registry: controller class → abstract? boolean. Populated at
|
|
35
|
+
# prepare_for_ractors! time by scanning all AbstractController::Base
|
|
36
|
+
# descendants for their @abstract ivar. Workers read this via the
|
|
37
|
+
# patched AbstractController::Base.abstract? (per-class values can't live in
|
|
38
|
+
# per-Ractor IES). Made shareable (frozen) at prepare time.
|
|
39
|
+
ABSTRACT_REGISTRY = Ractor.make_shareable({})
|
|
40
|
+
# Runtime registry: mattr_accessor IES key → default value. Written at
|
|
41
|
+
# mattr-definition time (boot, in main). The mattr reader (string-eval'd)
|
|
42
|
+
# looks the default up here by key — we CANNOT inline arbitrary default
|
|
43
|
+
# values into the eval'd method body (a Logger's `.inspect` is
|
|
44
|
+
# `#<Logger:...>`, invalid Ruby). Read by workers when both their IES slot
|
|
45
|
+
# and SHAREABLE_FALLBACK are empty. NOT made shareable (some defaults like
|
|
46
|
+
# Logger are intrinsically unshareable); workers only reach here if the
|
|
47
|
+
# value is a simple shareable literal (Symbol/String/Integer), in which
|
|
48
|
+
# case reading the constant is fine because... actually it IS a constant
|
|
49
|
+
# holding an unshareable Hash → worker read would raise. So this registry is
|
|
50
|
+
# ONLY safe to read from workers for shareable defaults. We guard the reader
|
|
51
|
+
# to only consult it for defaults that are Ractor.shareable?.
|
|
52
|
+
MATTR_DEFAULTS = {}
|
|
53
|
+
# class_attribute default values, keyed by IES key. Written at
|
|
54
|
+
# class_attribute-definition time (boot, in main). The class_attribute reader
|
|
55
|
+
# falls back to this in the MAIN ractor when the IES slot is empty (which it
|
|
56
|
+
# is on non-boot threads — IES is thread-local, and Puma's request threads
|
|
57
|
+
# have empty slots). NOT made shareable (values may be mutable Hashes/Arrays);
|
|
58
|
+
# only safe to read from the main ractor. Workers use SHAREABLE_FALLBACK
|
|
59
|
+
# (built at prepare_for_ractors! time) instead.
|
|
60
|
+
CLASS_ATTR_VALUES = {}
|
|
61
|
+
# Shareable subset of MATTR_DEFAULTS: only defaults that are
|
|
62
|
+
# Ractor.shareable? (so workers can read the constant safely). Written at
|
|
63
|
+
# mattr-definition time (boot, in main, before workers spawn); frozen +
|
|
64
|
+
# made shareable at prepare_for_ractors! time.
|
|
65
|
+
SHAREABLE_MATTR_DEFAULTS = {}
|
|
66
|
+
# Registry of constant path strings ("A::B::C") whose values are mutable
|
|
67
|
+
# Arrays/Hashes/Sets that need to be made shareable (deep-frozen) at boot.
|
|
68
|
+
# Each per-concern file concats its own constants into this array. Users
|
|
69
|
+
# can add their own via RactorRailsShim.shareable_constants << "MyGem::LIST".
|
|
70
|
+
SHAREABLE_CONSTANTS = []
|
|
71
|
+
# Registry of [ClassName, :ivar] pairs: class-level instance variables whose
|
|
72
|
+
# values are mutable (Hashes/Arrays/objects) and must be made Ractor-shareable
|
|
73
|
+
# (deep-frozen) at boot so worker Ractors can read them. Unlike
|
|
74
|
+
# SHAREABLE_CONSTANTS (top-level constants), these are class instance
|
|
75
|
+
# variables (e.g. ActiveSupport::Editor.@editors, Warden::Strategies.@strategies)
|
|
76
|
+
# that hold unshareable values and are read during request dispatch.
|
|
77
|
+
SHAREABLE_CLASS_IVARS = []
|
|
78
|
+
# Shareable registry: controller class → its built view_context_class.
|
|
79
|
+
# Populated at prepare_for_ractors! time by calling view_context_class on
|
|
80
|
+
# each loaded controller in main (build_view_context_class uses
|
|
81
|
+
# Class.new{...} blocks → un-shareable Proc from a worker). Made shareable.
|
|
82
|
+
VIEW_CONTEXT_REGISTRY = Ractor.make_shareable({})
|
|
83
|
+
# Frozen, shareable fallback table for class_attribute / mattr_accessor
|
|
84
|
+
# values. Built once at prepare_for_ractors! time from the main ractor's
|
|
85
|
+
# live values (class_attribute IES slot / mattr @@sym), each made shareable
|
|
86
|
+
# via callable-replacement + make_shareable. Workers read this via the
|
|
87
|
+
# RactorRailsShim::SHAREABLE_FALLBACK constant when their own IES slot is
|
|
88
|
+
# empty. Values that can't be made shareable are skipped (workers see nil
|
|
89
|
+
# for those and must set their own).
|
|
90
|
+
SHAREABLE_FALLBACK = Ractor.make_shareable({})
|
|
91
|
+
|
|
92
|
+
# Versions each patch was tested against. Populated by the install_*
|
|
93
|
+
# methods as they register themselves. A patch applies to a runtime Rails
|
|
94
|
+
# version only if the runtime segment matches one of its tested entries.
|
|
95
|
+
# This is the "load different patches for different Rails versions"
|
|
96
|
+
# infrastructure: to add 7.x support, write version-specific variants and
|
|
97
|
+
# tag them here. Defined on the module (not the singleton class) so it's
|
|
98
|
+
# readable from outside as RactorRailsShim::PATCH_VERSIONS.
|
|
99
|
+
PATCH_VERSIONS = {}
|
|
100
|
+
|
|
101
|
+
# Raised under :strict version policy when the runtime Rails/Ruby version
|
|
102
|
+
# isn't in the tested set. Defined on the module so it's catchable as
|
|
103
|
+
# RactorRailsShim::UnsupportedVersionError.
|
|
104
|
+
class UnsupportedVersionError < StandardError; end
|
|
105
|
+
|
|
106
|
+
class << self
|
|
107
|
+
# Accessor for the abstract-controller registry (written by abstract! in
|
|
108
|
+
# main, read by abstract? in workers). Reassigned to a shareable frozen
|
|
109
|
+
# Hash at prepare_for_ractors! time.
|
|
110
|
+
attr_accessor :_abstract_registry
|
|
111
|
+
attr_accessor :_view_context_registry
|
|
112
|
+
attr_accessor :_view_context_fallback
|
|
113
|
+
|
|
114
|
+
# Policy for version mismatches. One of :warn (default), :strict, :off.
|
|
115
|
+
# Set before `install`:
|
|
116
|
+
# RactorRailsShim.version_policy = :strict
|
|
117
|
+
# Defaults to :warn when never explicitly set.
|
|
118
|
+
def version_policy
|
|
119
|
+
@version_policy || :warn
|
|
120
|
+
end
|
|
121
|
+
attr_writer :version_policy
|
|
122
|
+
|
|
123
|
+
SUPPORTED_RUBY = RactorRailsShim::Version::SUPPORTED_RUBY
|
|
124
|
+
SUPPORTED_RAILS = "8.1"
|
|
125
|
+
|
|
126
|
+
# Install all the patches. Safe to call multiple times (idempotent).
|
|
127
|
+
#
|
|
128
|
+
# May be called either before or after Rails is loaded:
|
|
129
|
+
# - If Rails is already defined (e.g. `Bundler.require` ran first), the
|
|
130
|
+
# Rails module accessors are patched immediately.
|
|
131
|
+
# - If Rails is not yet defined (the normal `config/boot.rb` case, where
|
|
132
|
+
# `install` is called before `require "rails"`), a one-shot load hook
|
|
133
|
+
# defers the Rails-module patch until `rails.rb` is loaded. The
|
|
134
|
+
# `mattr_accessor` macro patch (a `Module.prepend`) applies
|
|
135
|
+
# immediately regardless, because it patches the macro itself, not
|
|
136
|
+
# any Rails constant.
|
|
137
|
+
# True when the shim should install its THREAD-server (Puma/Falcon) mode
|
|
138
|
+
# instead of the default Ractor (kino) mode. In thread mode Ractor.main? is
|
|
139
|
+
# true, so Rails' own globals (class variables / class ivars) are
|
|
140
|
+
# thread-safe and used as-is; only the class_attribute callback-chain
|
|
141
|
+
# isolation fix and the nil-safe callback replay are installed. The other
|
|
142
|
+
# patches route framework globals through per-Ractor
|
|
143
|
+
# IsolatedExecutionState, which is empty on Puma's request threads and
|
|
144
|
+
# would break the app, so they are skipped.
|
|
145
|
+
#
|
|
146
|
+
# Set explicitly via RactorRailsShim.thread_mode = true, or implicitly from
|
|
147
|
+
# ENV["SERVER"] (puma|falcon|thin|webrick|thread*). Detected in install.
|
|
148
|
+
def thread_mode?
|
|
149
|
+
return @thread_mode if defined?(@thread_mode)
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
attr_writer :thread_mode
|
|
154
|
+
|
|
155
|
+
def install
|
|
156
|
+
_check_version_support
|
|
157
|
+
@thread_mode = !!(ENV["SERVER"] && ENV["SERVER"] =~ /puma|falcon|thin|webrick|thread/i) unless defined?(@thread_mode)
|
|
158
|
+
|
|
159
|
+
if thread_mode?
|
|
160
|
+
# Minimal install for thread (Puma/Falcon) servers: only the
|
|
161
|
+
# class_attribute isolation fix + nil-safe callback replay. The other
|
|
162
|
+
# patches route framework globals through per-Ractor IES, which is
|
|
163
|
+
# empty on Puma's request threads and would break the app, so they are
|
|
164
|
+
# skipped; the original Rails globals are thread-safe and used as-is.
|
|
165
|
+
install_class_attribute
|
|
166
|
+
install_execution_wrapper
|
|
167
|
+
# Capture each controller's OWN declared before_action/after_action
|
|
168
|
+
# filters at declaration time (during eager load) by intercepting
|
|
169
|
+
# ActiveSupport::Callbacks.set_callback. This must be installed BEFORE
|
|
170
|
+
# eager load so declarations are captured as they happen — the
|
|
171
|
+
# class_attribute callback chain is corrupted by an eager-load leak
|
|
172
|
+
# under Ruby 4.0.5 + Rails 8.1.3 + Devise, so reading __callbacks later
|
|
173
|
+
# yields a wrong, unshareable chain. Install requires active_support/
|
|
174
|
+
# callbacks to be loaded, so require it first; install runs before the
|
|
175
|
+
# app's eager_load, so every controller declaration is captured.
|
|
176
|
+
require "active_support/callbacks" rescue nil
|
|
177
|
+
_install_callback_declaration_capture!
|
|
178
|
+
else
|
|
179
|
+
install_mattr_accessor
|
|
180
|
+
install_class_attribute
|
|
181
|
+
install_zeitwerk_registry
|
|
182
|
+
install_rubygems
|
|
183
|
+
install_rails_module
|
|
184
|
+
install_shareable_constants
|
|
185
|
+
install_execution_wrapper
|
|
186
|
+
require "active_support/callbacks" rescue nil
|
|
187
|
+
_install_callback_declaration_capture!
|
|
188
|
+
# Patch ActionView::Base.with_empty_template_cache EARLY (before eager
|
|
189
|
+
# load) so production's DetailsKey.view_context_class uses the block-free
|
|
190
|
+
# version. The framework's original defines compiled_method_container via
|
|
191
|
+
# define_method(&block) — an un-shareable Proc that breaks worker
|
|
192
|
+
# Ractors. on_load fires as soon as ActionView is required, well before
|
|
193
|
+
# the app's eager_load.
|
|
194
|
+
ActiveSupport.on_load(:action_view) do
|
|
195
|
+
RactorRailsShim._install_with_empty_template_cache_patch
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
@installed = true
|
|
199
|
+
true
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def installed?
|
|
203
|
+
@installed ||= false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# --- Generic constant-sharing utilities (moved from rails_module.rb) -----
|
|
207
|
+
# These are framework-agnostic; SHAREABLE_CONSTANTS lives here too, so the
|
|
208
|
+
# whole constant-shareability machinery is owned by core.rb.
|
|
209
|
+
|
|
210
|
+
def shareable_constants
|
|
211
|
+
SHAREABLE_CONSTANTS
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def install_shareable_constants
|
|
215
|
+
# Called at install time; if ActiveSupport isn't loaded yet, the
|
|
216
|
+
# constants don't exist. We re-run from patch_rails_module! (which
|
|
217
|
+
# fires once Rails — and thus ActiveSupport — is defined). Guarded
|
|
218
|
+
# by @shareable_constants_done so both paths are safe.
|
|
219
|
+
_register_patch :shareable_constants, "8.1"
|
|
220
|
+
return unless defined?(::ActiveSupport)
|
|
221
|
+
|
|
222
|
+
do_install_shareable_constants
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Run after Rails is fully booted (after Rails.application.initialize!)
|
|
226
|
+
# and BEFORE spawning worker Ractors. Re-attempts to make every
|
|
227
|
+
# registered constant shareable; constants that didn't exist at install
|
|
228
|
+
# time (e.g. Rails::Railtie, loaded after `module Rails` opens) get
|
|
229
|
+
# fixed here. Safe to call multiple times; already-shareable constants
|
|
230
|
+
# are no-ops.
|
|
231
|
+
#
|
|
232
|
+
# This MUST run in the main Ractor (const_set writes the constant table).
|
|
233
|
+
# Public wrapper is `prepare_for_ractors!` above.
|
|
234
|
+
def do_install_shareable_constants
|
|
235
|
+
shareable_constants.each { |path| make_constant_shareable(path) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Resolve a constant path string to a value, and if it exists and is
|
|
239
|
+
# not already shareable, replace it with its shareable (deep-frozen)
|
|
240
|
+
# version. Returns true if the constant was made shareable (or already
|
|
241
|
+
# was); false if it doesn't exist yet (caller may retry).
|
|
242
|
+
def make_constant_shareable(const_path)
|
|
243
|
+
owner, name = split_const_path(const_path)
|
|
244
|
+
return false unless owner && name
|
|
245
|
+
return true if owner.const_defined?(name, false) == false
|
|
246
|
+
|
|
247
|
+
val = owner.const_get(name, false)
|
|
248
|
+
return true if Ractor.shareable?(val)
|
|
249
|
+
|
|
250
|
+
shareable = _make_value_shareable(val)
|
|
251
|
+
return true unless shareable
|
|
252
|
+
|
|
253
|
+
# Deep-freeze and reassign. Ractor.make_shareable mutates `val` in
|
|
254
|
+
# place (freezing it and its reachable objects) and returns it.
|
|
255
|
+
# const_set warns "already initialized constant" because Rails'
|
|
256
|
+
# environment_inquirer.rb defined the constant first. The reassign is
|
|
257
|
+
# intentional (we're replacing the mutable value with its frozen
|
|
258
|
+
# shareable twin), so silence that one warning.
|
|
259
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
260
|
+
begin
|
|
261
|
+
owner.const_set(name, shareable)
|
|
262
|
+
ensure
|
|
263
|
+
$VERBOSE = verbose
|
|
264
|
+
end
|
|
265
|
+
true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Best-effort shareable replacement for a constant value. Monitor/Mutex
|
|
269
|
+
# become a NoOpLock (never contended post-boot). BasicObject instances
|
|
270
|
+
# (used as sentinel sentinels, e.g. PRIMARY_KEY_NOT_SET) can't be frozen
|
|
271
|
+
# (BasicObject has no #freeze method) — replace with a frozen Symbol.
|
|
272
|
+
# Everything else is deep-frozen via Ractor.make_shareable; if that fails
|
|
273
|
+
# (e.g. a Proc, or a Concurrent::Map / TypeMap holding Procs — both
|
|
274
|
+
# intrinsically unshareable and needing upstream Rails changes), returns
|
|
275
|
+
# nil and the constant is left as-is (the worker will raise a clear
|
|
276
|
+
# IsolationError on read).
|
|
277
|
+
def _make_value_shareable(val)
|
|
278
|
+
if (val.is_a?(::Monitor) rescue false) || (val.is_a?(::Mutex) rescue false)
|
|
279
|
+
Ractor.make_shareable(NoOpLock.new)
|
|
280
|
+
elsif !(val.respond_to?(:freeze) rescue false)
|
|
281
|
+
# BasicObject subclasses don't have #freeze/#respond_to? (Kernel not
|
|
282
|
+
# included). Replace with a frozen Symbol sentinel — it's compared
|
|
283
|
+
# with `equal?`, and a frozen Symbol is always shareable.
|
|
284
|
+
Ractor.make_shareable(:"__shim_unshareable_sentinel__")
|
|
285
|
+
else
|
|
286
|
+
begin
|
|
287
|
+
Ractor.make_shareable(val)
|
|
288
|
+
rescue => e
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Split "A::B::C" into [A::B (module), :C]. Returns [nil, nil] if the
|
|
295
|
+
# parent isn't defined.
|
|
296
|
+
def split_const_path(path)
|
|
297
|
+
parts = path.split("::")
|
|
298
|
+
return [Object, parts.first.to_sym] if parts.size == 1
|
|
299
|
+
parent = parts[0...-1].inject(Object) { |ns, n| ns.const_get(n) } rescue nil
|
|
300
|
+
return [nil, nil] unless parent
|
|
301
|
+
[parent, parts.last.to_sym]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Public API: run after Rails.application.initialize! and BEFORE spawning
|
|
305
|
+
# worker Ractors. Makes every registered constant shareable (deep-freeze).
|
|
306
|
+
# Constants that didn't exist at install time (e.g. Rails::Railtie, loaded
|
|
307
|
+
# after `module Rails` opens) get fixed here. Idempotent; safe to call
|
|
308
|
+
# multiple times. Must run in the main Ractor.
|
|
309
|
+
#
|
|
310
|
+
# NOTE: this does NOT build the framework-config shareable fallback. That
|
|
311
|
+
# step is folded into `make_app_shareable!` because some class_attribute /
|
|
312
|
+
# mattr_accessor values reference the app graph, and making them shareable
|
|
313
|
+
# must happen AFTER the app itself is already frozen (otherwise the app
|
|
314
|
+
# gets frozen prematurely and precompute/proc-replacement can't mutate
|
|
315
|
+
# it). If you call prepare_for_ractors! standalone (without
|
|
316
|
+
# make_app_shareable!), worker Ractors will see nil for framework config
|
|
317
|
+
# values that couldn't be shared without freezing the app — set them
|
|
318
|
+
# explicitly per worker, or use make_app_shareable!.
|
|
319
|
+
# Shared list of every framework patch install method. Both
|
|
320
|
+
# `prepare_for_ractors!` (pre-worker boot) and `make_app_shareable!`
|
|
321
|
+
# (post-boot, pre-freeze) call this, so the two no longer duplicate the
|
|
322
|
+
# literal `_install_*` list. Each `_install_*` is idempotent (guarded by
|
|
323
|
+
# its own @*_patched flag), so calling the full set at either point is a
|
|
324
|
+
# no-op for already-applied patches. Duplicates from the original
|
|
325
|
+
# prepare_for_ractors! list (_install_csrf_reset_patch,
|
|
326
|
+
# _install_messages_serializer_patch, _install_activerecord_delegation_patch)
|
|
327
|
+
# are collapsed here.
|
|
328
|
+
def _install_all_framework_patches
|
|
329
|
+
_install_rack_request_patch
|
|
330
|
+
_install_inflector_patch
|
|
331
|
+
_install_module_introspection_patch
|
|
332
|
+
_install_parameter_encoding_patch
|
|
333
|
+
_install_path_registry_patch
|
|
334
|
+
_install_action_view_resolver_patch
|
|
335
|
+
_install_action_view_partial_path_patch
|
|
336
|
+
_install_action_view_field_type_patch
|
|
337
|
+
_install_action_view_safe_join_patch
|
|
338
|
+
_install_abstract_controller_patch
|
|
339
|
+
_install_action_controller_controller_name_patch
|
|
340
|
+
_install_flash_helpers_patch
|
|
341
|
+
_install_csrf_reset_patch
|
|
342
|
+
_install_active_support_error_reporter_patch
|
|
343
|
+
_install_lookup_context_patch
|
|
344
|
+
_install_i18n_patch
|
|
345
|
+
_install_i18n_backend_patch
|
|
346
|
+
_install_i18n_interpolation_patch
|
|
347
|
+
_install_messages_serializer_patch
|
|
348
|
+
_install_template_handlers_patch
|
|
349
|
+
_install_execution_context_patch
|
|
350
|
+
_install_request_parameter_parsers_patch
|
|
351
|
+
_install_query_parser_patch
|
|
352
|
+
_install_rack_utils_patch
|
|
353
|
+
_install_log_subscriber_patch
|
|
354
|
+
_install_local_cache_patch
|
|
355
|
+
_install_reloader_patch
|
|
356
|
+
_install_exception_wrapper_patch
|
|
357
|
+
_install_action_dispatch_routing_patch
|
|
358
|
+
_install_action_dispatch_mounted_helpers_patch
|
|
359
|
+
_install_action_dispatch_http_url_patch
|
|
360
|
+
_install_journey_routes_patch
|
|
361
|
+
_install_warden_hooks_patch
|
|
362
|
+
_install_warden_strategies_patch
|
|
363
|
+
_install_devise_failure_app_patch
|
|
364
|
+
_install_activerecord_connection_handler_patch
|
|
365
|
+
_install_activerecord_configurations_patch
|
|
366
|
+
_install_activerecord_db_config_handlers_patch
|
|
367
|
+
_install_activerecord_query_transformers_patch
|
|
368
|
+
_install_activerecord_module_attrs_patch
|
|
369
|
+
_install_activerecord_deduplicable_patch
|
|
370
|
+
_install_activerecord_pool_config_patch
|
|
371
|
+
_install_activerecord_reaper_patch
|
|
372
|
+
_install_arel_visitor_dispatch_cache_patch
|
|
373
|
+
_install_arel_bind_block_patch
|
|
374
|
+
_install_activerecord_quoting_cache_patch
|
|
375
|
+
_install_activerecord_serialize_cast_value_patch
|
|
376
|
+
_install_activerecord_delegation_patch
|
|
377
|
+
_install_activerecord_primary_key_patch
|
|
378
|
+
_install_activerecord_query_constraints_patch
|
|
379
|
+
_install_activerecord_relation_delegate_cache_patch
|
|
380
|
+
_install_activerecord_model_classes_patch
|
|
381
|
+
_install_active_model_naming_patch
|
|
382
|
+
_install_active_record_core_patch
|
|
383
|
+
_install_active_record_inheritance_patch
|
|
384
|
+
_install_active_record_model_schema_patch
|
|
385
|
+
_install_activerecord_model_schema_patch
|
|
386
|
+
_install_openssl_digest_patch
|
|
387
|
+
_install_caching_key_generator_patch
|
|
388
|
+
_install_active_model_conversion_patch
|
|
389
|
+
_install_activerecord_find_by_cache_patch
|
|
390
|
+
_install_activerecord_migration_patch
|
|
391
|
+
_install_activerecord_transaction_callbacks_patch
|
|
392
|
+
_install_activerecord_query_logs_patch
|
|
393
|
+
_install_kaminari_config_patch
|
|
394
|
+
_install_propshaft_patch
|
|
395
|
+
_install_devise_url_helpers_patch
|
|
396
|
+
_install_devise_authenticatable_patch
|
|
397
|
+
_install_polymorphic_routes_patch
|
|
398
|
+
_install_orm_adapter_patch
|
|
399
|
+
_install_warden_serializer_patch
|
|
400
|
+
_install_json_encoding_patch
|
|
401
|
+
_install_active_model_attribute_patch
|
|
402
|
+
_install_hash_compute_if_absent_patch
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def prepare_for_ractors!
|
|
406
|
+
do_install_shareable_constants
|
|
407
|
+
RactorRailsShim._freeze_shareable_class_ivars! if RactorRailsShim.respond_to?(:_freeze_shareable_class_ivars!)
|
|
408
|
+
snapshot_gem_paths!
|
|
409
|
+
snapshot_query_logs!
|
|
410
|
+
_install_all_framework_patches
|
|
411
|
+
install_url_helpers_patch
|
|
412
|
+
fix_url_helpers_singleton_routes
|
|
413
|
+
_warm_active_record_class_caches!
|
|
414
|
+
_freeze_active_record_class_ivars!
|
|
415
|
+
_freeze_global_class_ivars!
|
|
416
|
+
_freeze_global_constants!
|
|
417
|
+
_freeze_messages_constants!
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Verify the runtime matches the versions the shim was developed against.
|
|
421
|
+
# The shim's patches target specific Rails 8.1 class layouts and Ruby 4.0
|
|
422
|
+
# Ractor semantics. On other versions, the patches may silently miss or
|
|
423
|
+
# break things. Behavior on mismatch is governed by `version_policy`:
|
|
424
|
+
#
|
|
425
|
+
# :warn (default) print a warning to $stderr, proceed anyway
|
|
426
|
+
# :strict raise RactorRailsShim::UnsupportedVersionError
|
|
427
|
+
# :off silent (for advanced users / experimentation)
|
|
428
|
+
#
|
|
429
|
+
# Ruby mismatch always warns (Ractor semantics are not stable across
|
|
430
|
+
# majors); Rails mismatch uses the policy. This is real version detection
|
|
431
|
+
# (Gem::Version-based), not a string-prefix compare, so pre-release and
|
|
432
|
+
# patch versions sort correctly.
|
|
433
|
+
def _check_version_support
|
|
434
|
+
@version_policy ||= :warn
|
|
435
|
+
unless RactorRailsShim::Version.supported_ruby?
|
|
436
|
+
msg = "ractor-rails-shim: Ruby #{RUBY_VERSION} — shim developed " \
|
|
437
|
+
"against Ruby #{SUPPORTED_RUBY}. Ractor semantics may differ; " \
|
|
438
|
+
"the shim may break. Proceeding anyway."
|
|
439
|
+
_version_mismatch(msg)
|
|
440
|
+
end
|
|
441
|
+
if RactorRailsShim::Version.rails &&
|
|
442
|
+
!RactorRailsShim::Version.supported_rails?
|
|
443
|
+
rv = ::Rails::VERSION::STRING
|
|
444
|
+
msg = "ractor-rails-shim: Rails #{rv} — shim developed against " \
|
|
445
|
+
"Rails #{RactorRailsShim::Version::TESTED_RAILS.join(", ")}. " \
|
|
446
|
+
"Class layouts (class_attribute, callbacks, PathRegistry, etc.) " \
|
|
447
|
+
"may differ; patches may miss blockers. Proceeding anyway. " \
|
|
448
|
+
"Set RactorRailsShim.version_policy = :strict to make this " \
|
|
449
|
+
"fatal; :off to silence."
|
|
450
|
+
_version_mismatch(msg)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Apply the configured policy to a mismatch message.
|
|
455
|
+
def _version_mismatch(message)
|
|
456
|
+
case version_policy
|
|
457
|
+
when :strict
|
|
458
|
+
raise UnsupportedVersionError, message
|
|
459
|
+
when :off
|
|
460
|
+
# silent
|
|
461
|
+
else
|
|
462
|
+
warn message
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Report which registered patches apply to the runtime Rails version
|
|
467
|
+
# (and which were skipped because they're untested on it). Useful for
|
|
468
|
+
# diagnostics and CI. Returns a Hash: { applied: [...], skipped: [...] }.
|
|
469
|
+
def applicable_patches
|
|
470
|
+
seg = RactorRailsShim::Version.rails_segment
|
|
471
|
+
applied = []
|
|
472
|
+
skipped = []
|
|
473
|
+
PATCH_VERSIONS.each do |name, tested|
|
|
474
|
+
if seg.nil? || tested.include?(seg)
|
|
475
|
+
applied << name
|
|
476
|
+
else
|
|
477
|
+
skipped << { name: name, tested: tested, runtime: seg }
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
{ applied: applied, skipped: skipped }
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Record that a patch was developed/tested against the given Rails version
|
|
484
|
+
# segments. Called by each install_* method. This populates PATCH_VERSIONS
|
|
485
|
+
# so `applicable_patches` can report what applied to the runtime. To add
|
|
486
|
+
# support for a new version, add the segment here (after writing/testing
|
|
487
|
+
# the variant) — no other wiring needed.
|
|
488
|
+
def _register_patch(name, *tested_segments)
|
|
489
|
+
existing = PATCH_VERSIONS[name] || []
|
|
490
|
+
PATCH_VERSIONS[name] = (existing + tested_segments).uniq
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Capture a frozen name -> object map for every constant the application's
|
|
494
|
+
# Zeitwerk loaders manage. Runs in the MAIN Ractor, after eager load, where
|
|
495
|
+
# all app constants are defined. The map travels to worker Ractors, which
|
|
496
|
+
# use it to (re)bind the constant *names* into their own namespaces.
|
|
497
|
+
#
|
|
498
|
+
# Why this is needed: a Ractor boundary does NOT share top-level constant
|
|
499
|
+
# *names* — only the class/module *objects* reachable from the frozen shared
|
|
500
|
+
# app graph cross the boundary. A worker Ractor therefore sees
|
|
501
|
+
# `RactorRailsShim`, `ActiveRecord`, `ApplicationRecord`, the controllers,
|
|
502
|
+
# etc. (objects reachable from the app), but NOT the application's own
|
|
503
|
+
# model constants (e.g. `Post`): the object is in the graph, but its name
|
|
504
|
+
# is not bound in the worker, so `PostsController#index`'s `Post` reference
|
|
505
|
+
# raises NameError. Rebinding the captured names fixes it without
|
|
506
|
+
# re-running autoloading (which is itself impossible in a worker, since
|
|
507
|
+
# `Zeitwerk::Loader.new` raises IsolationError off the main Ractor).
|
|
508
|
+
def capture_app_constants
|
|
509
|
+
map = {}
|
|
510
|
+
return map unless defined?(::Rails) && Rails.respond_to?(:autoloaders)
|
|
511
|
+
[Rails.autoloaders.main, Rails.autoloaders.once].each do |loader|
|
|
512
|
+
next unless loader.respond_to?(:all_expected_cpaths)
|
|
513
|
+
begin
|
|
514
|
+
loader.all_expected_cpaths.values.each do |cpath|
|
|
515
|
+
obj = Object.const_get(cpath) rescue next
|
|
516
|
+
begin
|
|
517
|
+
Ractor.make_shareable(obj) unless Ractor.shareable?(obj)
|
|
518
|
+
rescue
|
|
519
|
+
next
|
|
520
|
+
end
|
|
521
|
+
map[cpath] = obj if Ractor.shareable?(obj)
|
|
522
|
+
end
|
|
523
|
+
rescue => e
|
|
524
|
+
warn "[ractor_rails_shim] capture_app_constants: #{e.class}: #{e.message}"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
map.freeze
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Build the shareable Rack app handed to kino. Captures the application's
|
|
531
|
+
# constants in the main Ractor and wraps the frozen, shareable app in a
|
|
532
|
+
# WorkerApp that rebinds those constants (and initializes the worker's
|
|
533
|
+
# ActiveRecord connection) on the first request served by each worker
|
|
534
|
+
# Ractor. Returns a shareable WorkerApp instance.
|
|
535
|
+
def worker_app(frozen_app)
|
|
536
|
+
bindings = capture_app_constants
|
|
537
|
+
WorkerApp.new(frozen_app, bindings)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# See patches/active_model_attribute.rb. When the frozen `:ractor` graph is
|
|
541
|
+
# built, each model class's `_default_attributes` template (and the
|
|
542
|
+
# FromDatabase instances within it) is deep-frozen. `Attribute#dup_or_share`
|
|
543
|
+
# returns `self` for immutable column types, so a worker's NEW record would
|
|
544
|
+
# share a frozen Attribute and raise FrozenError on first read/write. This
|
|
545
|
+
# patch makes a frozen receiver yield a fresh, mutable Attribute so writes
|
|
546
|
+
# (POST/create) work in workers. No-op in normal (unfrozen) Rails.
|
|
547
|
+
def _install_active_model_attribute_patch
|
|
548
|
+
return @am_attribute_patched if defined?(@am_attribute_patched) && @am_attribute_patched
|
|
549
|
+
@am_attribute_patched = true
|
|
550
|
+
return unless defined?(::ActiveModel::Attribute)
|
|
551
|
+
::ActiveModel::Attribute.include(::RactorRailsShim::ActiveModelAttributePatch)
|
|
552
|
+
if defined?(::ActiveModel::AttributeRegistration) &&
|
|
553
|
+
::ActiveModel::AttributeRegistration.const_defined?(:ClassMethods)
|
|
554
|
+
::ActiveModel::AttributeRegistration::ClassMethods.prepend(
|
|
555
|
+
::RactorRailsShim::ActiveModelAttributeRegistrationPatch
|
|
556
|
+
)
|
|
557
|
+
end
|
|
558
|
+
if defined?(::ActiveRecord::Attributes) &&
|
|
559
|
+
::ActiveRecord::Attributes.const_defined?(:ClassMethods)
|
|
560
|
+
::ActiveRecord::Attributes::ClassMethods.prepend(
|
|
561
|
+
::RactorRailsShim::ActiveRecordAttributesPatch
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
if defined?(::ActiveRecord::ModelSchema) &&
|
|
565
|
+
::ActiveRecord::ModelSchema.const_defined?(:ClassMethods)
|
|
566
|
+
::ActiveRecord::ModelSchema::ClassMethods.prepend(
|
|
567
|
+
::RactorRailsShim::ActiveRecordModelSchemaPatch
|
|
568
|
+
)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# The shim's make_app_shareable! replaces Concurrent::Map instance variables
|
|
573
|
+
# (which are not Ractor-shareable) with plain Hashes so workers can read
|
|
574
|
+
# them. But Rails code calls Concurrent::Map#compute_if_absent on these
|
|
575
|
+
# caches (e.g. ActiveModel::AttributeMethods' attribute_method_patterns_
|
|
576
|
+
# cache). Plain Hash lacks that method, so we add a compatible definition.
|
|
577
|
+
# For a MUTABLE cache the shim freezes the replaced Hash (so it is shareable
|
|
578
|
+
# across workers); mutating it would raise FrozenError. So when the receiver
|
|
579
|
+
# is frozen we route the store to per-Ractor IES keyed by the Hash identity
|
|
580
|
+
# and the key — giving each worker its own cache entry without mutating the
|
|
581
|
+
# shared object. Semantics otherwise match Concurrent::Map.
|
|
582
|
+
def _install_hash_compute_if_absent_patch
|
|
583
|
+
return @hash_compute_if_absent_patched if defined?(@hash_compute_if_absent_patched) && @hash_compute_if_absent_patched
|
|
584
|
+
@hash_compute_if_absent_patched = true
|
|
585
|
+
return if ::Hash.method_defined?(:compute_if_absent)
|
|
586
|
+
::Hash.prepend(Module.new do
|
|
587
|
+
def compute_if_absent(key)
|
|
588
|
+
if frozen?
|
|
589
|
+
ies_key = :"rrs_cia_#{object_id}_#{key}"
|
|
590
|
+
ActiveSupport::IsolatedExecutionState[ies_key] ||= yield(key)
|
|
591
|
+
elsif key?(key)
|
|
592
|
+
self[key]
|
|
593
|
+
else
|
|
594
|
+
self[key] = yield(key)
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Freeze (make Ractor-shareable) every instance variable on every ActiveRecord
|
|
601
|
+
# model class in the MAIN Ractor, before the graph is frozen. Many AR model
|
|
602
|
+
# classes cache unshareable values in class-level ivars (@pending_attribute_
|
|
603
|
+
# modifications, @column_defaults, @symbol_column_to_string_name_hash,
|
|
604
|
+
# @yaml_encoder, @dangerous_attribute_methods, ...). A worker Ractor cannot
|
|
605
|
+
# read an unshareable class-ivar value (Ractor::IsolationError) nor set one.
|
|
606
|
+
# Freezing them in main (where setting is allowed) yields shareable values
|
|
607
|
+
# that workers read without writing. AR Type objects freeze cleanly, so this
|
|
608
|
+
# is behavior-preserving; per-request code never mutates model class ivars.
|
|
609
|
+
def _freeze_active_record_class_ivars!
|
|
610
|
+
return unless defined?(::ActiveRecord::Base)
|
|
611
|
+
models = [::ActiveRecord::Base] + (::ActiveRecord::Base.descendants rescue [])
|
|
612
|
+
models.each do |klass|
|
|
613
|
+
# NOTE: do NOT skip abstract classes (e.g. a primary_abstract_class
|
|
614
|
+
# ApplicationRecord). Workers recurse into them via
|
|
615
|
+
# apply_pending_attribute_modifications, so their class ivars must also
|
|
616
|
+
# be shareable.
|
|
617
|
+
klass.instance_variables.each do |ivar|
|
|
618
|
+
val = klass.instance_variable_get(ivar)
|
|
619
|
+
next if val.nil? || Ractor.shareable?(val)
|
|
620
|
+
begin
|
|
621
|
+
Ractor.make_shareable(val)
|
|
622
|
+
rescue
|
|
623
|
+
nil
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Freeze (make Ractor-shareable) unshareable class-level ivars on GLOBAL
|
|
630
|
+
# classes (Time/Date timezone caches, I18n locale caches, ...) in the MAIN
|
|
631
|
+
# Ractor, before the graph is frozen. Unlike model classes, these are shared
|
|
632
|
+
# singletons whose class ivars (e.g. Time's @zone_default / @zone_cache) hold
|
|
633
|
+
# unshareable values that a worker Ractor would otherwise fail to read
|
|
634
|
+
# (Ractor::IsolationError). Freezing them in main yields shareable values.
|
|
635
|
+
def _freeze_global_class_ivars!
|
|
636
|
+
classes = %w[Time Date DateTime I18n].filter_map { |n| Object.const_get(n) rescue nil }
|
|
637
|
+
classes.each do |klass|
|
|
638
|
+
klass.instance_variables.each do |ivar|
|
|
639
|
+
val = klass.instance_variable_get(ivar)
|
|
640
|
+
next if val.nil? || Ractor.shareable?(val)
|
|
641
|
+
begin
|
|
642
|
+
Ractor.make_shareable(val)
|
|
643
|
+
rescue
|
|
644
|
+
nil
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Replace GLOBAL constants that hold non-shareable values (e.g.
|
|
651
|
+
# Time/Date/DateTime::DATE_FORMATS contain Proc values) with frozen,
|
|
652
|
+
# shareable equivalents so worker Ractors can read them. Proc-valued format
|
|
653
|
+
# entries are dropped (to_fs falls back to to_s for those formats). This is
|
|
654
|
+
# done in the MAIN Ractor, where const_set is allowed.
|
|
655
|
+
def _freeze_global_constants!
|
|
656
|
+
constants = %w[Time Date DateTime].filter_map do |n|
|
|
657
|
+
mod = Object.const_get(n) rescue nil
|
|
658
|
+
mod.is_a?(Module) ? [mod, :DATE_FORMATS] : nil
|
|
659
|
+
end
|
|
660
|
+
constants.each do |mod, name|
|
|
661
|
+
next unless mod.const_defined?(name, false)
|
|
662
|
+
val = mod.const_get(name, false)
|
|
663
|
+
next if Ractor.shareable?(val)
|
|
664
|
+
shareable = if val.is_a?(Hash)
|
|
665
|
+
h = {}
|
|
666
|
+
val.each { |k, v| h[k] = v if Ractor.shareable?(v) }
|
|
667
|
+
h.freeze
|
|
668
|
+
elsif val.is_a?(Array)
|
|
669
|
+
val.select { |v| Ractor.shareable?(v) }.freeze
|
|
670
|
+
else
|
|
671
|
+
val
|
|
672
|
+
end
|
|
673
|
+
begin
|
|
674
|
+
mod.const_set(name, shareable)
|
|
675
|
+
rescue
|
|
676
|
+
nil
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# ActiveSupport::Messages::Metadata holds non-shareable Array constants
|
|
682
|
+
# (ENVELOPE_SERIALIZERS / TIMESTAMP_SERIALIZERS) of serializer Modules, used
|
|
683
|
+
# by MessageEncryptor during flash/session cookie serialization. A worker
|
|
684
|
+
# Ractor reading these constants (e.g. on `redirect_to`, which encrypts a
|
|
685
|
+
# flash message) raises Ractor::IsolationError. The Arrays are shareable once
|
|
686
|
+
# frozen (their elements are Modules), so deep-freeze and const_set the
|
|
687
|
+
# shareable copy back so workers read a shareable constant.
|
|
688
|
+
def _freeze_messages_constants!
|
|
689
|
+
# Load ActiveSupport::MessagePack in the MAIN Ractor FIRST. metadata.rb
|
|
690
|
+
# registers an `ActiveSupport.on_load(:message_pack)` callback that mutates
|
|
691
|
+
# ENVELOPE_SERIALIZERS / TIMESTAMP_SERIALIZERS via `<<`. If that callback
|
|
692
|
+
# first fires inside a worker Ractor (which happens the first time a
|
|
693
|
+
# cookie's `detect_format` probes MessagePackWithFallback.dumped?, because
|
|
694
|
+
# it lazily requires "active_support/message_pack"), it runs against the
|
|
695
|
+
# already-frozen arrays below and raises
|
|
696
|
+
# FrozenError: can't modify frozen Array
|
|
697
|
+
# Loading here makes the callback fire once, in main, against the
|
|
698
|
+
# non-frozen arrays; load hooks never fire again in workers.
|
|
699
|
+
# Gem::LoadError is a ScriptError (not StandardError), so a bare
|
|
700
|
+
# `rescue nil` on the require would NOT catch a missing msgpack gem.
|
|
701
|
+
begin
|
|
702
|
+
require "active_support/message_pack"
|
|
703
|
+
rescue LoadError
|
|
704
|
+
return
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
mod = (Object.const_get(:ActiveSupport) rescue nil)&.const_get(:Messages, false) rescue nil
|
|
708
|
+
mod = mod&.const_get(:Metadata, false) rescue nil
|
|
709
|
+
return unless mod.is_a?(Module)
|
|
710
|
+
%i[ENVELOPE_SERIALIZERS TIMESTAMP_SERIALIZERS].each do |name|
|
|
711
|
+
next unless mod.const_defined?(name, false)
|
|
712
|
+
val = mod.const_get(name, false)
|
|
713
|
+
next if Ractor.shareable?(val)
|
|
714
|
+
shareable = Ractor.make_shareable(val) rescue val
|
|
715
|
+
begin
|
|
716
|
+
mod.const_set(name, shareable)
|
|
717
|
+
rescue
|
|
718
|
+
nil
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Warm ActiveRecord model classes' lazily-computed, shareable class-ivar
|
|
724
|
+
# memoizations in the MAIN Ractor, BEFORE the graph is frozen. Methods like
|
|
725
|
+
# the timestamp_attribute_* helpers cache frozen Arrays of strings (shareable
|
|
726
|
+
# once warmed), so pre-populating them here lets workers read via `||=`
|
|
727
|
+
# without ever setting the class ivar. (Class ivars holding unshareable
|
|
728
|
+
# values are handled by _freeze_active_record_class_ivars!.)
|
|
729
|
+
def _warm_active_record_class_caches!
|
|
730
|
+
return unless defined?(::ActiveRecord::Base)
|
|
731
|
+
models = [::ActiveRecord::Base] + (::ActiveRecord::Base.descendants rescue [])
|
|
732
|
+
warmers = %i[
|
|
733
|
+
timestamp_attributes_for_create_in_model
|
|
734
|
+
timestamp_attributes_for_update_in_model
|
|
735
|
+
all_timestamp_attributes_in_model
|
|
736
|
+
sequence_name
|
|
737
|
+
columns
|
|
738
|
+
column_names
|
|
739
|
+
attribute_names
|
|
740
|
+
column_defaults
|
|
741
|
+
symbol_column_to_string_name_hash
|
|
742
|
+
returning_columns_for_insert
|
|
743
|
+
yaml_encoder
|
|
744
|
+
attribute_types
|
|
745
|
+
]
|
|
746
|
+
models.each do |klass|
|
|
747
|
+
next if klass.abstract_class?
|
|
748
|
+
warmers.each do |m|
|
|
749
|
+
next unless klass.respond_to?(m, true)
|
|
750
|
+
begin
|
|
751
|
+
klass.send(m)
|
|
752
|
+
rescue
|
|
753
|
+
nil
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
# A shareable Rack wrapper that performs per-worker initialization lazily,
|
|
760
|
+
# inside the worker Ractor's request path (kino's :ractor mode has no
|
|
761
|
+
# per-worker init hook). On the first request served by a worker it:
|
|
762
|
+
#
|
|
763
|
+
# 1. rebinds the captured application constants into that worker's
|
|
764
|
+
# namespace (so bare `Post` etc. resolve), then
|
|
765
|
+
# 2. ensures the worker's ActiveRecord connection handler is initialized.
|
|
766
|
+
#
|
|
767
|
+
# The wrapper holds only shareable state (@app, @bindings), so the instance
|
|
768
|
+
# is Ractor.make_shareable. `Ractor.current` provides per-worker storage for
|
|
769
|
+
# the one-time guard, avoiding any top-level constant reference.
|
|
770
|
+
class WorkerApp
|
|
771
|
+
def initialize(app, bindings)
|
|
772
|
+
@app = app
|
|
773
|
+
@bindings = bindings
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def call(env)
|
|
777
|
+
setup_once!
|
|
778
|
+
@app.call(env)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
private
|
|
782
|
+
|
|
783
|
+
def setup_once!
|
|
784
|
+
return if Ractor.current[:rrs_worker_ready]
|
|
785
|
+
rebind_constants
|
|
786
|
+
RactorRailsShim.init_worker_ar_connections! if defined?(RactorRailsShim)
|
|
787
|
+
Ractor.current[:rrs_worker_ready] = true
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def rebind_constants
|
|
791
|
+
@bindings.each do |cpath, obj|
|
|
792
|
+
parent = Object
|
|
793
|
+
parts = cpath.split("::")
|
|
794
|
+
parts[0...-1].each do |p|
|
|
795
|
+
parent = if parent.const_defined?(p, false)
|
|
796
|
+
parent.const_get(p, false)
|
|
797
|
+
else
|
|
798
|
+
parent.const_set(p, Module.new)
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
leaf = parts.last
|
|
802
|
+
parent.const_set(leaf, obj) unless parent.const_defined?(leaf, false)
|
|
803
|
+
end
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
end
|