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,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rewrite Module.mattr_accessor (and friends) so the accessor methods
|
|
4
|
+
# route through IsolatedExecutionState. Uses prepend + module_eval with
|
|
5
|
+
# strings to avoid cross-ractor binding issues.
|
|
6
|
+
|
|
7
|
+
module RactorRailsShim
|
|
8
|
+
class << self
|
|
9
|
+
def install_mattr_accessor
|
|
10
|
+
_register_patch :mattr_accessor, "8.1"
|
|
11
|
+
return if @mattr_patched
|
|
12
|
+
@mattr_patched = true
|
|
13
|
+
|
|
14
|
+
::Module.prepend(Module.new {
|
|
15
|
+
# The prepended module's body is evaluated in the main ractor at
|
|
16
|
+
# prepend time; the methods it defines are callable from any ractor
|
|
17
|
+
# because they're defined via string eval (no captured binding).
|
|
18
|
+
# But mattr_accessor itself runs at app boot in the main ractor, and
|
|
19
|
+
# the per-accessor redefinition must also use string eval.
|
|
20
|
+
#
|
|
21
|
+
# IMPORTANT: Rails' mattr_accessor/cattr_accessor stores values in
|
|
22
|
+
# CLASS VARIABLES (@@sym), not class instance variables (@sym). The
|
|
23
|
+
# default value is written via class_variable_set("@@sym", default).
|
|
24
|
+
# Class variables are also subject to Ractor::IsolationError from
|
|
25
|
+
# non-main ractors (verified on Ruby 4.0.5), so we route through IES
|
|
26
|
+
# the same way — but the main-ractor fallback must read @@sym, and
|
|
27
|
+
# the seed must run in the main ractor at define-time (via super).
|
|
28
|
+
def mattr_accessor(*syms, instance_reader: true, instance_writer: true,
|
|
29
|
+
instance_accessor: true, default: nil, **kwargs, &block)
|
|
30
|
+
shareable = kwargs[:shareable]
|
|
31
|
+
mod_name = name
|
|
32
|
+
|
|
33
|
+
# Compute the default value the same way Rails does, so we can
|
|
34
|
+
# seed worker-ractor IES slots with it (workers can't read @@sym).
|
|
35
|
+
# The block form is evaluated once here (in main ractor) like Rails.
|
|
36
|
+
sym_default = block_given? && default.nil? ? yield : default
|
|
37
|
+
|
|
38
|
+
super # define the methods via the original path (sets @@sym)
|
|
39
|
+
|
|
40
|
+
syms.each do |sym|
|
|
41
|
+
key = :"ractor_rails_shim_mattr_#{mod_name}_#{sym}"
|
|
42
|
+
key_str = key.inspect
|
|
43
|
+
cv = "@@#{sym}"
|
|
44
|
+
cv_str = cv.inspect
|
|
45
|
+
|
|
46
|
+
# Register so _build_shareable_fallback! can capture the main-ractor
|
|
47
|
+
# value (read from @@sym) at prepare_for_ractors! time. The label
|
|
48
|
+
# is just for diagnostics. The default is stored too so the
|
|
49
|
+
# fallback builder can use it when the live value can't be shared.
|
|
50
|
+
RactorRailsShim::CLASS_ATTRIBUTES << [mod_name, sym, key, sym_default]
|
|
51
|
+
# Store the default in a runtime registry (NOT inlined into the
|
|
52
|
+
# eval'd method body — arbitrary objects like Logger have invalid
|
|
53
|
+
# `.inspect` output). The reader looks it up by key.
|
|
54
|
+
RactorRailsShim::MATTR_DEFAULTS[key] = sym_default
|
|
55
|
+
# If the default is shareable, add to the shareable subset. We
|
|
56
|
+
# rebuild the constant as a new frozen shareable Hash each time
|
|
57
|
+
# (so workers can read the constant even before prepare_for_ractors!
|
|
58
|
+
# runs — e.g. unit tests). const_set warns "already initialized
|
|
59
|
+
# constant"; silence it.
|
|
60
|
+
if sym_default && Ractor.shareable?(sym_default)
|
|
61
|
+
h = RactorRailsShim::SHAREABLE_MATTR_DEFAULTS.dup
|
|
62
|
+
h[key] = sym_default
|
|
63
|
+
h.freeze
|
|
64
|
+
Ractor.make_shareable(h)
|
|
65
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
66
|
+
begin
|
|
67
|
+
RactorRailsShim.const_set(:SHAREABLE_MATTR_DEFAULTS, h)
|
|
68
|
+
ensure
|
|
69
|
+
$VERBOSE = verbose
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Redefine the class reader via string eval (no captured binding).
|
|
74
|
+
# Class variables are only touched from the main ractor; worker
|
|
75
|
+
# ractors fall back to SHAREABLE_FALLBACK (built from main's @@sym
|
|
76
|
+
# at prepare_for_ractors! time) when their own IES slot is empty.
|
|
77
|
+
# NOTE: we deliberately do NOT inline the default value here —
|
|
78
|
+
# arbitrary objects (e.g. Logger) have invalid `.inspect` output.
|
|
79
|
+
# The fallback builder captures the live value (which may equal
|
|
80
|
+
# the default) at prepare time.
|
|
81
|
+
singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
82
|
+
def #{sym}
|
|
83
|
+
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
84
|
+
return v unless v.nil?
|
|
85
|
+
|
|
86
|
+
if #{!!shareable}
|
|
87
|
+
if class_variable_defined?(#{cv_str})
|
|
88
|
+
class_variable_get(#{cv_str})
|
|
89
|
+
end
|
|
90
|
+
elsif Ractor.main?
|
|
91
|
+
if class_variable_defined?(#{cv_str})
|
|
92
|
+
class_variable_get(#{cv_str})
|
|
93
|
+
else
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
# Worker: try the shareable fallback (built from main's @@sym
|
|
98
|
+
# at prepare_for_ractors! time). If empty, try the
|
|
99
|
+
# definition-time default (only the shareable subset — the
|
|
100
|
+
# full MATTR_DEFAULTS holds unshareable defaults like Logger
|
|
101
|
+
# which workers can't read via the constant).
|
|
102
|
+
fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
|
|
103
|
+
return fb unless fb.nil?
|
|
104
|
+
RactorRailsShim::SHAREABLE_MATTR_DEFAULTS[#{key_str}]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def #{sym}=(val)
|
|
109
|
+
ActiveSupport::IsolatedExecutionState[#{key_str}] = val
|
|
110
|
+
if Ractor.main?
|
|
111
|
+
class_variable_set(#{cv_str}, val) if class_variable_defined?(#{cv_str})
|
|
112
|
+
class_variable_set(#{cv_str}, val) unless class_variable_defined?(#{cv_str})
|
|
113
|
+
end
|
|
114
|
+
val
|
|
115
|
+
end
|
|
116
|
+
RUBY
|
|
117
|
+
|
|
118
|
+
# Instance readers/writers route through IES directly (NOT
|
|
119
|
+
# self.class.#{sym}). Rails' original uses @@sym (a class variable
|
|
120
|
+
# inherited by including classes); the shim routes through IES,
|
|
121
|
+
# so the instance reader must also use IES. Using self.class.sym
|
|
122
|
+
# would fail for mattr_accessor on Modules (e.g.
|
|
123
|
+
# ActionView::Helpers::FormHelper#form_with_generates_ids):
|
|
124
|
+
# self.class is the including class (ActionView::Base), which
|
|
125
|
+
# doesn't have the module's singleton method.
|
|
126
|
+
# Only redefine if instance_accessor is on (matches Rails).
|
|
127
|
+
if instance_reader && instance_accessor
|
|
128
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
129
|
+
def #{sym}
|
|
130
|
+
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
131
|
+
return v unless v.nil?
|
|
132
|
+
if Ractor.main?
|
|
133
|
+
self.class.class_variable_defined?(#{cv_str}) ? self.class.class_variable_get(#{cv_str}) : nil
|
|
134
|
+
else
|
|
135
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
RUBY
|
|
139
|
+
end
|
|
140
|
+
if instance_writer && instance_accessor
|
|
141
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
142
|
+
def #{sym}=(val)
|
|
143
|
+
ActiveSupport::IsolatedExecutionState[#{key_str}] = val
|
|
144
|
+
self.class.class_variable_set(#{cv_str}, val) if Ractor.main? && self.class.class_variable_defined?(#{cv_str})
|
|
145
|
+
val
|
|
146
|
+
end
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# cattr_accessor is an alias for mattr_accessor in Rails; route it too.
|
|
153
|
+
if method_defined?(:cattr_accessor, true)
|
|
154
|
+
alias_method :_unshimmed_cattr_accessor, :cattr_accessor
|
|
155
|
+
def cattr_accessor(*args, **kwargs, &block)
|
|
156
|
+
mattr_accessor(*args, **kwargs, &block)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
})
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patches for OpenSSL digest classes so they can be used from worker Ractors.
|
|
4
|
+
#
|
|
5
|
+
# `openssl/digest.rb` defines each algorithm class (SHA256, etc.) as:
|
|
6
|
+
#
|
|
7
|
+
# klass = Class.new(self) {
|
|
8
|
+
# define_method(:initialize, ->(data = nil) { super(name, data) })
|
|
9
|
+
# }
|
|
10
|
+
# singleton.class_eval {
|
|
11
|
+
# define_method(:digest) { |data| new.digest(data) }
|
|
12
|
+
# define_method(:hexdigest) { |data| new.hexdigest(data) }
|
|
13
|
+
# }
|
|
14
|
+
#
|
|
15
|
+
# The `->(data = nil) { ... }` lambda (and the singleton `digest`/`hexdigest`
|
|
16
|
+
# blocks) are compiled in the MAIN Ractor. Instantiating the class from a
|
|
17
|
+
# worker Ractor (`OpenSSL::Digest::SHA256.new`, called e.g. by
|
|
18
|
+
# `ActiveSupport::KeyGenerator#generate_key` -> `OpenSSL::PKCS5.pbkdf2_hmac`
|
|
19
|
+
# -> `@hash_digest_class.new`) invokes that lambda and raises
|
|
20
|
+
# "defined with an un-shareable Proc in a different Ractor". This blocks every
|
|
21
|
+
# request that touches the cookie/session key generator (i.e. all of them).
|
|
22
|
+
#
|
|
23
|
+
# Fix: redefine the methods with string-eval (no captured binding) so they are
|
|
24
|
+
# callable from any Ractor. Behaviour is identical. Applied at
|
|
25
|
+
# prepare_for_ractors! time (after OpenSSL is loaded).
|
|
26
|
+
|
|
27
|
+
module RactorRailsShim
|
|
28
|
+
class << self
|
|
29
|
+
def _install_openssl_digest_patch
|
|
30
|
+
return if @openssl_digest_patched
|
|
31
|
+
@openssl_digest_patched = true
|
|
32
|
+
_register_patch :openssl_digest, "8.1"
|
|
33
|
+
return unless defined?(::OpenSSL::Digest)
|
|
34
|
+
|
|
35
|
+
algos = %w(MD4 MD5 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512)
|
|
36
|
+
algos.each do |name|
|
|
37
|
+
klass_name = name.tr("-", "_")
|
|
38
|
+
next unless ::OpenSSL::Digest.const_defined?(klass_name)
|
|
39
|
+
klass = ::OpenSSL::Digest.const_get(klass_name)
|
|
40
|
+
next unless klass.is_a?(::Class)
|
|
41
|
+
|
|
42
|
+
# Replace the lambda-based `initialize` (super(name, data)) with a
|
|
43
|
+
# string-eval'd method that holds no captured Proc.
|
|
44
|
+
klass.class_eval "def initialize(data = nil); super(#{name.inspect}, data); end"
|
|
45
|
+
|
|
46
|
+
# Replace the singleton `digest`/`hexdigest` blocks the same way.
|
|
47
|
+
klass.singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
48
|
+
def digest(data)
|
|
49
|
+
new.digest(data)
|
|
50
|
+
end
|
|
51
|
+
def hexdigest(data)
|
|
52
|
+
new.hexdigest(data)
|
|
53
|
+
end
|
|
54
|
+
RUBY
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch OrmAdapter::ToAdapter#to_adapter. The original memoizes the adapter on
|
|
4
|
+
# the model CLASS via `@_to_adapter ||= self::OrmAdapter.new(self)`. A worker
|
|
5
|
+
# Ractor cannot set an instance variable on a class/module, so it raises
|
|
6
|
+
# "Ractor::IsolationError: can not set instance variables of classes/modules by
|
|
7
|
+
# non-main Ractors". Route the per-class cache through IsolatedExecutionState,
|
|
8
|
+
# keyed by the class's (stable, shared) object_id. Each Ractor builds its own
|
|
9
|
+
# adapter and caches it locally; the adapter only holds a reference to the
|
|
10
|
+
# (shared) model class, so no cross-boundary objects are involved.
|
|
11
|
+
|
|
12
|
+
module RactorRailsShim
|
|
13
|
+
class << self
|
|
14
|
+
def _install_orm_adapter_patch
|
|
15
|
+
return if @orm_adapter_patched
|
|
16
|
+
@orm_adapter_patched = true
|
|
17
|
+
_register_patch :orm_adapter, "8.1"
|
|
18
|
+
return unless defined?(::OrmAdapter::ToAdapter)
|
|
19
|
+
|
|
20
|
+
::OrmAdapter::ToAdapter.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
21
|
+
def to_adapter
|
|
22
|
+
key = :"ractor_rails_shim_orm_adapter_\#{object_id}"
|
|
23
|
+
v = ActiveSupport::IsolatedExecutionState[key]
|
|
24
|
+
return v unless v.nil?
|
|
25
|
+
adapter = self::OrmAdapter.new(self)
|
|
26
|
+
ActiveSupport::IsolatedExecutionState[key] = adapter
|
|
27
|
+
adapter
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.
|
|
4
|
+
#
|
|
5
|
+
# Two Ractor-safety problems live here:
|
|
6
|
+
#
|
|
7
|
+
# 1. The class body populates a `CACHE` constant with HelperMethodBuilder
|
|
8
|
+
# instances that hold un-shareable lambdas (e.g. `->(name) { name.route_key }`).
|
|
9
|
+
# Because the constant lives on the shared class, a worker Ractor that reads
|
|
10
|
+
# `CACHE[:path][nil]` touches a main-Ractor-owned object and raises
|
|
11
|
+
# "defined with an un-shareable Proc in a different Ractor". Fix: stop reading
|
|
12
|
+
# `CACHE` — build a fresh builder in the *calling* Ractor on every lookup.
|
|
13
|
+
#
|
|
14
|
+
# 2. `HelperMethodBuilder#handle_model_call` calls `polymorphic_mapping`, which
|
|
15
|
+
# reads `target._routes.polymorphic_mappings` — a shared hash of main-built
|
|
16
|
+
# Procs. Reading it from a worker raises the same error. Fix: skip the shared
|
|
17
|
+
# mapping and build the route from the (worker-owned) builder's own logic.
|
|
18
|
+
|
|
19
|
+
module RactorRailsShim
|
|
20
|
+
class << self
|
|
21
|
+
def _install_polymorphic_routes_patch
|
|
22
|
+
return if @polymorphic_routes_patched
|
|
23
|
+
@polymorphic_routes_patched = true
|
|
24
|
+
_register_patch :polymorphic_routes, "8.1"
|
|
25
|
+
return unless defined?(::ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder)
|
|
26
|
+
hmb = ::ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder
|
|
27
|
+
hmb.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
28
|
+
def get(action, type)
|
|
29
|
+
type = type.to_sym
|
|
30
|
+
build action, type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def url
|
|
34
|
+
build nil, "url"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path
|
|
38
|
+
build nil, "path"
|
|
39
|
+
end
|
|
40
|
+
RUBY
|
|
41
|
+
hmb.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
42
|
+
def handle_model_call(target, record)
|
|
43
|
+
if mapping = polymorphic_mapping(target, record) rescue nil
|
|
44
|
+
mapping.call(target, [record], suffix == "path")
|
|
45
|
+
else
|
|
46
|
+
method, args = handle_model(record)
|
|
47
|
+
target.public_send(method, *args)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
# The module-level `polymorphic_path` / `polymorphic_url` (called by
|
|
53
|
+
# `form_with` when it infers the URL from a model) invoke
|
|
54
|
+
# `mapping.call` directly. A custom `resolve` mapping Proc is built in
|
|
55
|
+
# the main Ractor and is un-shareable, so calling it from a worker
|
|
56
|
+
# Ractor raises "defined with an un-shareable Proc in a different
|
|
57
|
+
# Ractor". Rescue that and fall through to the (worker-safe)
|
|
58
|
+
# HelperMethodBuilder path, which derives the route from the record's
|
|
59
|
+
# model name — the same fallback the original code uses when no mapping
|
|
60
|
+
# is registered at all.
|
|
61
|
+
pm = ::ActionDispatch::Routing::PolymorphicRoutes
|
|
62
|
+
pm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
63
|
+
def polymorphic_path(record_or_hash_or_array, options = {})
|
|
64
|
+
if ::Hash === record_or_hash_or_array
|
|
65
|
+
options = record_or_hash_or_array.merge(options)
|
|
66
|
+
record = options.delete :id
|
|
67
|
+
return polymorphic_path record, options
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if mapping = (polymorphic_mapping(record_or_hash_or_array) rescue nil)
|
|
71
|
+
begin
|
|
72
|
+
return mapping.call(self, [record_or_hash_or_array, options], true)
|
|
73
|
+
rescue ::Ractor::Error
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
opts = options.dup
|
|
78
|
+
action = opts.delete :action
|
|
79
|
+
type = :path
|
|
80
|
+
|
|
81
|
+
HelperMethodBuilder.polymorphic_method self,
|
|
82
|
+
record_or_hash_or_array,
|
|
83
|
+
action,
|
|
84
|
+
type,
|
|
85
|
+
opts
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def polymorphic_url(record_or_hash_or_array, options = {})
|
|
89
|
+
if ::Hash === record_or_hash_or_array
|
|
90
|
+
options = record_or_hash_or_array.merge(options)
|
|
91
|
+
record = options.delete :id
|
|
92
|
+
return polymorphic_url record, options
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if mapping = (polymorphic_mapping(record_or_hash_or_array) rescue nil)
|
|
96
|
+
begin
|
|
97
|
+
return mapping.call(self, [record_or_hash_or_array, options], false)
|
|
98
|
+
rescue ::Ractor::Error
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
opts = options.dup
|
|
103
|
+
action = opts.delete :action
|
|
104
|
+
type = opts.delete(:routing_type) || :url
|
|
105
|
+
|
|
106
|
+
HelperMethodBuilder.polymorphic_method self,
|
|
107
|
+
record_or_hash_or_array,
|
|
108
|
+
action,
|
|
109
|
+
type,
|
|
110
|
+
opts
|
|
111
|
+
end
|
|
112
|
+
RUBY
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch for Propshaft (the default Rails 8 asset pipeline). The Assembly and
|
|
4
|
+
# LoadPath memoize their derived values in lazy instance ivars via `||=`:
|
|
5
|
+
#
|
|
6
|
+
# Propshaft::Assembly#load_path -> @load_path ||= Propshaft::LoadPath.new(...)
|
|
7
|
+
# Propshaft::Assembly#compilers -> @compilers ||= Propshaft::Compilers.new(...)
|
|
8
|
+
# Propshaft::LoadPath#assets_by_path -> @cached_assets_by_path ||= ...
|
|
9
|
+
# Propshaft::LoadPath#asset_paths_by_type -> (@cached_... ||= Hash.new)[ct] ||= ...
|
|
10
|
+
# Propshaft::LoadPath#asset_paths_by_glob -> (@cached_... ||= Hash.new)[g] ||= ...
|
|
11
|
+
#
|
|
12
|
+
# The Assembly/LoadPath live inside the frozen, shared Rails.application graph
|
|
13
|
+
# (set by the propshaft railtie at boot, then deep-frozen by
|
|
14
|
+
# make_app_shareable!). A worker Ractor calling `||=` to populate these ivars
|
|
15
|
+
# raises FrozenError because the host object is frozen.
|
|
16
|
+
#
|
|
17
|
+
# Fix: warm every Assembly/LoadPath lazy ivar in MAIN (before freezing) so the
|
|
18
|
+
# `||=` short-circuits in workers (the ivar is already set → truthy → no
|
|
19
|
+
# assignment). The two `asset_paths_by_*` methods additionally perform a
|
|
20
|
+
# *write* to their inner Hash even when the key already exists (`h[k] ||= v`
|
|
21
|
+
# is `h[k] = h[k] || v`, which assigns to a frozen Hash). Route those two
|
|
22
|
+
# through IsolatedExecutionState (each Ractor builds its own cache from
|
|
23
|
+
# `assets`, which is already warmed read-only).
|
|
24
|
+
|
|
25
|
+
module RactorRailsShim
|
|
26
|
+
class << self
|
|
27
|
+
def _install_propshaft_patch
|
|
28
|
+
return if @propshaft_patched
|
|
29
|
+
@propshaft_patched = true
|
|
30
|
+
_register_patch :propshaft, "1.3"
|
|
31
|
+
return unless defined?(::Propshaft::LoadPath)
|
|
32
|
+
|
|
33
|
+
lp = ::Propshaft::LoadPath
|
|
34
|
+
lp.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
35
|
+
def asset_paths_by_type(content_type)
|
|
36
|
+
cache = (ActiveSupport::IsolatedExecutionState[:"ractor_rails_shim_propshaft_type_\#{object_id}"] ||= {})
|
|
37
|
+
if cache.key?(content_type)
|
|
38
|
+
cache[content_type]
|
|
39
|
+
else
|
|
40
|
+
cache[content_type] = extract_logical_paths_from(assets.select { |a| a.content_type == ::Mime::EXTENSION_LOOKUP[content_type] })
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def asset_paths_by_glob(glob)
|
|
45
|
+
cache = (ActiveSupport::IsolatedExecutionState[:"ractor_rails_shim_propshaft_glob_\#{object_id}"] ||= {})
|
|
46
|
+
if cache.key?(glob)
|
|
47
|
+
cache[glob]
|
|
48
|
+
else
|
|
49
|
+
cache[glob] = extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(glob) })
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
RUBY
|
|
53
|
+
|
|
54
|
+
if lp.method_defined?(:asset_paths_by_path)
|
|
55
|
+
lp.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
56
|
+
def asset_paths_by_path(path)
|
|
57
|
+
cache = (ActiveSupport::IsolatedExecutionState[:"ractor_rails_shim_propshaft_path_\#{object_id}"] ||= {})
|
|
58
|
+
if cache.key?(path)
|
|
59
|
+
cache[path]
|
|
60
|
+
else
|
|
61
|
+
cache[path] = extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(path) })
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Warm every Propshaft Assembly/LoadPath lazy ivar in MAIN (before the app
|
|
69
|
+
# is frozen). Warmed ivars are truthy, so the `||=` memoization in workers
|
|
70
|
+
# short-circuits instead of attempting to assign onto a frozen object.
|
|
71
|
+
# Assets::by_path is itself memoized; warming `assets` populates it so
|
|
72
|
+
# workers only read the frozen cache.
|
|
73
|
+
def _precompute_propshaft!(app)
|
|
74
|
+
return unless Ractor.main?
|
|
75
|
+
assets = app.assets rescue nil
|
|
76
|
+
return unless assets
|
|
77
|
+
assets.load_path rescue nil
|
|
78
|
+
assets.compilers rescue nil
|
|
79
|
+
assets.resolver rescue nil
|
|
80
|
+
assets.prefix rescue nil
|
|
81
|
+
assets.processor rescue nil
|
|
82
|
+
# Build the asset map (LoadPath#assets -> assets_by_path) so the inner
|
|
83
|
+
# cache is populated and frozen before workers read it. Then warm each
|
|
84
|
+
# Asset's lazy ivars (@content_type, @digest) in MAIN — they are memoized
|
|
85
|
+
# via `||=` and the Asset objects live inside the frozen, shared app
|
|
86
|
+
# graph, so a worker reading them would raise FrozenError. Warming here
|
|
87
|
+
# (while still mutable) lets the `||=` short-circuit in workers.
|
|
88
|
+
assets.load_path.assets.each do |asset|
|
|
89
|
+
asset.content_type rescue nil
|
|
90
|
+
asset.digest rescue nil
|
|
91
|
+
end
|
|
92
|
+
# Warm the resolver manifest cache. Propshaft::Resolver::Static#manifest
|
|
93
|
+
# memoizes `@manifest ||= Propshaft::Manifest.from_path(...)`; the
|
|
94
|
+
# resolver is frozen in the shared graph, so without warming, a worker
|
|
95
|
+
# rendering a `stylesheet_link_tag` (or reading asset integrity) for a
|
|
96
|
+
# PRECOMPILED manifest raises FrozenError ("can't modify frozen
|
|
97
|
+
# Propshaft::Resolver::Static"). `#manifest` is private — invoke via
|
|
98
|
+
# send to trigger the `||=` in MAIN (while still mutable) so workers only
|
|
99
|
+
# read the frozen, cached value.
|
|
100
|
+
resolver = assets.resolver rescue nil
|
|
101
|
+
if resolver.respond_to?(:manifest, true)
|
|
102
|
+
begin
|
|
103
|
+
resolver.send(:manifest)
|
|
104
|
+
rescue
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patches for Rack: Rack::Request class-level attr_accessors and
|
|
4
|
+
# Rack::Utils singleton attr_accessors.
|
|
5
|
+
|
|
6
|
+
module RactorRailsShim
|
|
7
|
+
# Rack constants whose values are mutable and need to be made shareable.
|
|
8
|
+
SHAREABLE_CONSTANTS.concat([
|
|
9
|
+
"Rack::Utils::PATH_SEPS",
|
|
10
|
+
"Rack::Utils::HTTP_STATUS_CODES",
|
|
11
|
+
"Rack::Utils::COMMON_SEP",
|
|
12
|
+
"Rack::Utils::STATUS_WITH_NO_ENTITY_BODY",
|
|
13
|
+
"Rack::Utils::SYMBOL_TO_STATUS_CODE",
|
|
14
|
+
"Rack::MethodOverride::ALLOWED_METHODS",
|
|
15
|
+
"Rack::MethodOverride::METHOD_OVERRIDES",
|
|
16
|
+
"Rack::MethodOverride::HTTP_METHODS",
|
|
17
|
+
"Rack::Headers::KNOWN_HEADERS",
|
|
18
|
+
"Rack::Request::Helpers::FORM_DATA_MEDIA_TYPES",
|
|
19
|
+
"Rack::Request::Helpers::PARSEABLE_DATA_MEDIA_TYPES",
|
|
20
|
+
"Rack::Request::Helpers::DEFAULT_PORTS",
|
|
21
|
+
"Rack::Mime::MIME_TYPES",
|
|
22
|
+
"Rack::Files::ALLOWED_VERBS",
|
|
23
|
+
"Rack::Files::ALLOW_HEADER",
|
|
24
|
+
"Rack::Response::STATUS_WITH_NO_ENTITY_BODY",
|
|
25
|
+
# Multipart file-upload parser constants, read by worker Ractors on every
|
|
26
|
+
# POST (Rack::Request#POST -> parse_multipart -> Parser.parse). TEMPFILE_FACTORY
|
|
27
|
+
# is a lambda; EMPTY is a MultipartInfo sentinel instance; REENCODE_DUMMY_
|
|
28
|
+
# ENCODINGS is a mutable Hash. None are shareable by default.
|
|
29
|
+
"Rack::Multipart::Parser::TEMPFILE_FACTORY",
|
|
30
|
+
"Rack::Multipart::Parser::EMPTY",
|
|
31
|
+
"Rack::Multipart::Parser::REENCODE_DUMMY_ENCODINGS",
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
# Source-location constant used by make_app_shareable!'s proc-replacement
|
|
35
|
+
# graph traversal (moved here from make_shareable.rb so the Rack concern's
|
|
36
|
+
# pieces live together).
|
|
37
|
+
FILES_LOC = "/rack/files.rb".freeze
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# Patch Rack::Request's class-level attr_accessors (forwarded_priority,
|
|
41
|
+
# x_forwarded_proto_priority) to not read @ivars from a worker Ractor.
|
|
42
|
+
# The values are frozen-Symbol Arrays (shareable); route the cache
|
|
43
|
+
# through IES with the same default in workers. Read per-request via
|
|
44
|
+
# ActionDispatch::RemoteIp. Applied at prepare_for_ractors! time (after
|
|
45
|
+
# Rails boots, so Rack is loaded).
|
|
46
|
+
def _install_rack_request_patch
|
|
47
|
+
return if @rack_request_patched
|
|
48
|
+
@rack_request_patched = true
|
|
49
|
+
_register_patch :rack_request, "8.1"
|
|
50
|
+
return unless defined?(::Rack::Request)
|
|
51
|
+
req = ::Rack::Request
|
|
52
|
+
fp_key = :ractor_rails_shim_rack_forwarded_priority
|
|
53
|
+
xp_key = :ractor_rails_shim_rack_x_forwarded_proto_priority
|
|
54
|
+
fp_key_str = fp_key.inspect
|
|
55
|
+
xp_key_str = xp_key.inspect
|
|
56
|
+
req.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
57
|
+
def forwarded_priority
|
|
58
|
+
v = ActiveSupport::IsolatedExecutionState[#{fp_key_str}]
|
|
59
|
+
return v unless v.nil?
|
|
60
|
+
if Ractor.main? && instance_variable_defined?(:@forwarded_priority)
|
|
61
|
+
@forwarded_priority
|
|
62
|
+
else
|
|
63
|
+
[:forwarded, :x_forwarded]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
def forwarded_priority=(val)
|
|
67
|
+
ActiveSupport::IsolatedExecutionState[#{fp_key_str}] = val
|
|
68
|
+
@forwarded_priority = val if Ractor.main?
|
|
69
|
+
val
|
|
70
|
+
end
|
|
71
|
+
def x_forwarded_proto_priority
|
|
72
|
+
v = ActiveSupport::IsolatedExecutionState[#{xp_key_str}]
|
|
73
|
+
return v unless v.nil?
|
|
74
|
+
if Ractor.main? && instance_variable_defined?(:@x_forwarded_proto_priority)
|
|
75
|
+
@x_forwarded_proto_priority
|
|
76
|
+
else
|
|
77
|
+
[:proto, :scheme]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
def x_forwarded_proto_priority=(val)
|
|
81
|
+
ActiveSupport::IsolatedExecutionState[#{xp_key_str}] = val
|
|
82
|
+
@x_forwarded_proto_priority = val if Ractor.main?
|
|
83
|
+
val
|
|
84
|
+
end
|
|
85
|
+
RUBY
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Patch Rack::Utils singleton attr_accessors (default_query_parser,
|
|
89
|
+
# multipart_total_part_limit, multipart_file_limit) to not read @ivars
|
|
90
|
+
# from a worker Ractor. The values are shareable once frozen (QueryParser,
|
|
91
|
+
# Integers). Route through IES; workers read the shareable fallback.
|
|
92
|
+
# `default_query_parser` is read per-request during POST parsing.
|
|
93
|
+
def _install_rack_utils_patch
|
|
94
|
+
return if @rack_utils_patched
|
|
95
|
+
@rack_utils_patched = true
|
|
96
|
+
_register_patch :rack_utils, "8.1"
|
|
97
|
+
return unless defined?(::Rack::Utils)
|
|
98
|
+
u = ::Rack::Utils
|
|
99
|
+
dqp_key = :ractor_rails_shim_rack_utils_default_query_parser
|
|
100
|
+
mtp_key = :ractor_rails_shim_rack_utils_multipart_total_part_limit
|
|
101
|
+
mfl_key = :ractor_rails_shim_rack_utils_multipart_file_limit
|
|
102
|
+
dqp_key_str = dqp_key.inspect
|
|
103
|
+
mtp_key_str = mtp_key.inspect
|
|
104
|
+
mfl_key_str = mfl_key.inspect
|
|
105
|
+
u.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
106
|
+
def default_query_parser
|
|
107
|
+
v = ActiveSupport::IsolatedExecutionState[#{dqp_key_str}]
|
|
108
|
+
return v unless v.nil?
|
|
109
|
+
if Ractor.main? && instance_variable_defined?(:@default_query_parser)
|
|
110
|
+
v = @default_query_parser
|
|
111
|
+
ActiveSupport::IsolatedExecutionState[#{dqp_key_str}] = v
|
|
112
|
+
v
|
|
113
|
+
else
|
|
114
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{dqp_key_str}] || ::Rack::QueryParser::QueryParser.make_default(32)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
def multipart_total_part_limit
|
|
118
|
+
v = ActiveSupport::IsolatedExecutionState[#{mtp_key_str}]
|
|
119
|
+
return v unless v.nil?
|
|
120
|
+
if Ractor.main? && instance_variable_defined?(:@multipart_total_part_limit)
|
|
121
|
+
v = @multipart_total_part_limit
|
|
122
|
+
ActiveSupport::IsolatedExecutionState[#{mtp_key_str}] = v
|
|
123
|
+
v
|
|
124
|
+
else
|
|
125
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{mtp_key_str}] || 128
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
def multipart_file_limit
|
|
129
|
+
v = ActiveSupport::IsolatedExecutionState[#{mfl_key_str}]
|
|
130
|
+
return v unless v.nil?
|
|
131
|
+
if Ractor.main? && instance_variable_defined?(:@multipart_file_limit)
|
|
132
|
+
v = @multipart_file_limit
|
|
133
|
+
ActiveSupport::IsolatedExecutionState[#{mfl_key_str}] = v
|
|
134
|
+
v
|
|
135
|
+
else
|
|
136
|
+
RactorRailsShim::SHAREABLE_FALLBACK[#{mfl_key_str}] || 64
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
RUBY
|
|
140
|
+
CLASS_ATTRIBUTES << ["Rack::Utils", :default_query_parser, dqp_key, nil]
|
|
141
|
+
CLASS_ATTRIBUTES << ["Rack::Utils", :multipart_total_part_limit, mtp_key, nil]
|
|
142
|
+
CLASS_ATTRIBUTES << ["Rack::Utils", :multipart_file_limit, mfl_key, nil]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Find the Rack::Files (asset) server in the middleware chain, used by
|
|
146
|
+
# make_app_shareable! when replacing the Rack::Head#@app lambda (whose
|
|
147
|
+
# binding receiver is the Rack::Files instance). Moved here from
|
|
148
|
+
# make_shareable.rb so the Rack concern's pieces live together.
|
|
149
|
+
def _find_files_server(mw)
|
|
150
|
+
cur = mw
|
|
151
|
+
while cur
|
|
152
|
+
if cur.class.name == "ActionDispatch::Static"
|
|
153
|
+
return cur.instance_variable_get(:@file_server)
|
|
154
|
+
end
|
|
155
|
+
cur = cur.instance_variable_get(:@app) rescue nil
|
|
156
|
+
end
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|