ractor-rails-shim 0.2.6 → 0.3.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 +4 -4
- data/CHANGELOG.md +157 -0
- data/lib/ractor_rails_shim/fallback_ies.rb +20 -6
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +12 -16
- data/lib/ractor_rails_shim/patches/action_view.rb +1 -1
- data/lib/ractor_rails_shim/patches/activerecord.rb +8 -38
- data/lib/ractor_rails_shim/patches/callables.rb +155 -0
- data/lib/ractor_rails_shim/patches/class_attribute.rb +85 -84
- data/lib/ractor_rails_shim/patches/core.rb +71 -66
- data/lib/ractor_rails_shim/patches/devise.rb +1 -1
- data/lib/ractor_rails_shim/patches/kaminari.rb +1 -6
- data/lib/ractor_rails_shim/patches/make_shareable.rb +68 -203
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +34 -27
- data/lib/ractor_rails_shim/patches/rails_module.rb +2 -2
- data/lib/ractor_rails_shim/patches/route_helpers.rb +1 -1
- data/lib/ractor_rails_shim/patches.rb +38 -0
- data/lib/ractor_rails_shim/version.rb +1 -1
- data/lib/ractor_rails_shim/version_policy.rb +72 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21002fee5a30f663904441d8c016c035564095bf456048a25917657e9adab5fc
|
|
4
|
+
data.tar.gz: dcb6764139de152ae7b958112503efc7951d59ae211c04ee268da6bf63a2be51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28cf75ae0f4972142cee3965d5c68fd8b70c09b901dc60c186f178ac403fc7695c3f7e2e77e4591c9aa6aa0fc9c6d1ba5eafa8e3c64d5abad545b50159159d6c
|
|
7
|
+
data.tar.gz: b1bf30685c4b7082c5930a63693571e4d3affff2fa0b530fd355aaf5a57f5cf42280ff5a590f099705e8af573ae953d654ace88fbb813dc05e463379fa0c09c7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,163 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0]
|
|
11
|
+
|
|
12
|
+
### Changed — breaking (public API)
|
|
13
|
+
- **`worker_app` renamed to `worker_app!`.** The factory freezes +
|
|
14
|
+
`Ractor.make_shareable`s the `WorkerApp` — a destructive build — so it
|
|
15
|
+
now carries the bang the naming convention reserves for "mutates /
|
|
16
|
+
produces a frozen shareable." Callers following the README's
|
|
17
|
+
`Ractor.new(worker_app) { |a| a.call(env) }` example must update to
|
|
18
|
+
`worker_app!`. The old name is gone. (`patches/core.rb`)
|
|
19
|
+
|
|
20
|
+
- **`fix_url_helpers_singleton_routes` renamed to
|
|
21
|
+
`fix_url_helpers_singleton_routes!`.** Mutates the global routes
|
|
22
|
+
singleton; now bang-suffixed per the convention documented at the top
|
|
23
|
+
of `patches.rb`. (`patches/route_helpers.rb`)
|
|
24
|
+
|
|
25
|
+
- **Tightened `activesupport` dependency to `>= 8.1`.** The gemspec
|
|
26
|
+
previously declared `>= 7.0`, but the shim only supports Rails 8.1
|
|
27
|
+
(per `Version::TESTED_RAILS` / `SUPPORTED_RAILS`). The looser bound let
|
|
28
|
+
Bundler resolve against AS 7.x where the class-layout patches
|
|
29
|
+
(`class_attribute`, `Callbacks`, `PathRegistry`, …) would silently miss
|
|
30
|
+
blockers or redefine the wrong methods. Bound at `>= 8.1` so Bundler
|
|
31
|
+
fails fast on an unsupported Rails. (`ractor-rails-shim.gemspec`)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **`_apply_shareable_constants!` set the done-flag on undefined
|
|
35
|
+
constants.** It set `@shareable_constants_done = true` after the first
|
|
36
|
+
run unconditionally, even when registered constants didn't exist yet
|
|
37
|
+
(`make_constant_shareable` returned `false`). A later call (from
|
|
38
|
+
`make_app_shareable!` or `prepare_for_ractors!`) short-circuited on the
|
|
39
|
+
flag and never retried the now-loadable constants — workers then hit
|
|
40
|
+
`Ractor::IsolationError` on unshareable values (e.g.
|
|
41
|
+
`Rack::Utils::PATH_SEPS`, a Regexp not deep-frozen until
|
|
42
|
+
`make_shareable` runs on it). The flag now sets only when every
|
|
43
|
+
registered constant was made shareable; any `false` return leaves it
|
|
44
|
+
unset so the next call retries. Caught by the integration spec (boots
|
|
45
|
+
a real Rails 8.1 app, dispatches `GET /up` in a worker Ractor); pinned
|
|
46
|
+
by `apply_shareable_constants_retry_spec.rb`.
|
|
47
|
+
|
|
48
|
+
- **ActionFilter private ivar reads now gated through the `_swallow`
|
|
49
|
+
debug funnel.** The eval'd `set_callback` interceptor inlined
|
|
50
|
+
`af.instance_variable_get(:@conditional_key) rescue nil`, but
|
|
51
|
+
`instance_variable_get` returns nil for a missing ivar *without*
|
|
52
|
+
raising — the rescue never fired, and a silent Rails internal rename
|
|
53
|
+
(`@conditional_key` / `@actions`) would leave `only`/`except` nil,
|
|
54
|
+
making callbacks run for actions they shouldn't (security-relevant)
|
|
55
|
+
with no visible cause. New `_read_action_filter_constraints(af)` checks
|
|
56
|
+
`instance_variable_defined?` and emits a labeled
|
|
57
|
+
`[ractor_rails_shim] action filter constraints: missing ivar …`
|
|
58
|
+
warning under `debug=true`. Returns `[nil, nil]` for a bare object,
|
|
59
|
+
`[key, symbols]` when the ivars are present.
|
|
60
|
+
|
|
61
|
+
- **Dead ternary in thread-mode `class_attribute` reader.** The reader
|
|
62
|
+
carried `:__class_attr___callbacks == :__callbacks ? {} : nil`, but
|
|
63
|
+
the namespaced name is always `__class_attr_<name>` (never `<name>`),
|
|
64
|
+
so the check was always false — dead code that allocated a `Symbol`
|
|
65
|
+
via `inspect` on every read AND never delivered the intended `{}`
|
|
66
|
+
default for `__callbacks` (callers index the result, so `nil` would
|
|
67
|
+
`NoMethodError`). Replaced with a static decision based on the public
|
|
68
|
+
name, inlining the shared frozen `EMPTY_CALLBACKS_HASH` constant.
|
|
69
|
+
|
|
70
|
+
- **Redundant `class_variable_set` pair in `mattr_accessor` writer.**
|
|
71
|
+
The shim-generated writer had two complementary guarded lines
|
|
72
|
+
(`set if defined?` + `set unless defined?`) that together were one
|
|
73
|
+
unconditional set, with a wasted `class_variable_defined?` per write.
|
|
74
|
+
Replaced with a single `class_variable_set(cv, val) if Ractor.main?`.
|
|
75
|
+
|
|
76
|
+
- **`WorkerApp#setup_once!` race-spec cleanup leak.** The race spec's
|
|
77
|
+
`ensure` block restored `Thread::Mutex.new` via
|
|
78
|
+
`define_method { orig_new.call }`, leaving a block that captures a
|
|
79
|
+
main-Ractor-bound `Method`. When a later spec's worker Ractor called
|
|
80
|
+
`Thread::Mutex.new` it raised `IsolationError` (flaky
|
|
81
|
+
`WorkerAppSpec#test_0002`). Replaced with `remove_method(:new)` so the
|
|
82
|
+
call falls back to the class-defined `new` (no captured binding).
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- **Centralized `const_set`-with-suppressed-`$VERBOSE` into
|
|
86
|
+
`_reassign_shareable_const(name, value)`.** `SHAREABLE_FALLBACK`,
|
|
87
|
+
`SHAREABLE_MATTR_DEFAULTS`, `SHAREABLE_APP`, and
|
|
88
|
+
`SHAREABLE_DECLARED_CALLBACKS` were each rebuilt + reassigned via an
|
|
89
|
+
inlined `$VERBOSE = nil … const_set … ensure $VERBOSE = …` dance. The
|
|
90
|
+
dance now lives in one helper on `RactorRailsShim`; all rebuild sites
|
|
91
|
+
(including the 6 `activerecord.rb` sites + 1 `kaminari.rb` site) call
|
|
92
|
+
it. The constants stay frozen shareable `Hash`es (always readable from
|
|
93
|
+
any Ractor) — a mutable-then-frozen registry would not be shareable
|
|
94
|
+
until `freeze!`, breaking workers that read early.
|
|
95
|
+
|
|
96
|
+
- **Callable/lock object model extracted to `patches/callables.rb`.**
|
|
97
|
+
`NoOpProc` / `Callable` / `CallableConst` / `DeviseMappingSnapshot` /
|
|
98
|
+
`NoOpLock` / `NoOpLogDev` + the `_devise_mapping_snapshot` helper moved
|
|
99
|
+
out of the 964-line `make_shareable.rb` into their own file. Plain
|
|
100
|
+
class definitions replace the string-eval indirection (the eval was
|
|
101
|
+
stylistic, not behavioral). `make_shareable.rb`: 964 → 822 lines.
|
|
102
|
+
|
|
103
|
+
- **`VersionPolicy` module extracted from the `RactorRailsShim` god
|
|
104
|
+
module.** The version-policy + patch-registry concern (`:warn` /
|
|
105
|
+
`:strict` / `:off` switch, `PATCH_VERSIONS`, `_register_patch` /
|
|
106
|
+
`applicable_patches` / `_version_mismatch`) moved from the
|
|
107
|
+
`RactorRailsShim` singleton in `core.rb` to
|
|
108
|
+
`RactorRailsShim::VersionPolicy` (`version_policy.rb`). `core.rb` keeps
|
|
109
|
+
the public facade (`RactorRailsShim.version_policy`, `.applicable_patches`,
|
|
110
|
+
`::PATCH_VERSIONS`, `::UnsupportedVersionError`) delegating to the
|
|
111
|
+
module. Orthogonal to the already-extracted `RactorRailsShim::Version`
|
|
112
|
+
(detection).
|
|
113
|
+
|
|
114
|
+
- **`mattr_accessor` split into single-responsibility helpers.** The
|
|
115
|
+
per-symbol body did four things (call super, push to `CLASS_ATTRIBUTES`,
|
|
116
|
+
seed the default + rebuild the shareable constant, redefine the
|
|
117
|
+
reader/writer). Extracted `_seed_mattr_default(key, default)` and
|
|
118
|
+
`_register_for_fallback(mod_name, sym, key, default)`; `mattr_accessor`
|
|
119
|
+
now reads `super, register, seed, redefine`.
|
|
120
|
+
|
|
121
|
+
- **Thread-mode vs Ractor-mode `class_attribute` heredocs de-duplicated.**
|
|
122
|
+
Each branch inlined a near-identical ~25-line reader/writer heredoc,
|
|
123
|
+
differing only in the method name. Extracted
|
|
124
|
+
`_class_attr_thread_methods` / `_class_attr_ractor_methods` (each builds
|
|
125
|
+
one reader+writer pair); `redefine` calls the helper twice. One source
|
|
126
|
+
of truth per mode. Also fixed a misaligned `else` (12-space indent in a
|
|
127
|
+
10-space block). `class_attribute.rb`: 232 → 224 lines.
|
|
128
|
+
|
|
129
|
+
- **Freeze-path bare rescues routed through the `_swallow` debug funnel.**
|
|
130
|
+
`_freeze_shareable_class_ivars!`, `_freeze_declared_callbacks!`,
|
|
131
|
+
`_collect_controller_classes`, `_replace_locks_and_concurrent_maps!`,
|
|
132
|
+
`_replace_one_proc`, `_neutralize_logger_io!`,
|
|
133
|
+
`DeviseMappingSnapshot`, `_devise_mapping_snapshot`, `_warm_journey_routes`,
|
|
134
|
+
url-helper freeze, `DUMMY_END_NODE`, `MimeNegotiation`, AR db-config
|
|
135
|
+
handlers, AR query transformers, devise mappings, and the view-context
|
|
136
|
+
fallback now emit a labeled `[ractor_rails_shim] <label>: …` line to
|
|
137
|
+
`$stderr` under `debug=true` instead of bare `rescue; nil`. Silent by
|
|
138
|
+
default (backward compatible).
|
|
139
|
+
|
|
140
|
+
- **`fallback_ies.rb` moved into the `RactorRailsShim` namespace.** It
|
|
141
|
+
previously defined `ActiveSupport::IsolatedExecutionState` directly
|
|
142
|
+
inside the `ActiveSupport` namespace (guarded by `defined?`). Defining
|
|
143
|
+
an upstream-namespaced constant from a third-party gem is a
|
|
144
|
+
namespace-patch smell — if a future AS lazy-loads IES, load order
|
|
145
|
+
decides which definition wins. The fallback now lives at
|
|
146
|
+
`RactorRailsShim::FallbackIES` and is aliased onto
|
|
147
|
+
`ActiveSupport::IsolatedExecutionState` only when the real AS IES is
|
|
148
|
+
absent. The ~270 string-eval'd references across the patch files
|
|
149
|
+
resolve to the alias when AS is absent and to the real one when present.
|
|
150
|
+
|
|
151
|
+
- **Stream-of-consciousness comments trimmed to one-line invariants.**
|
|
152
|
+
Working-notes-style comments that narrated history ("actually it IS a
|
|
153
|
+
constant holding…", "Pre-fix the traversals only handled…") replaced
|
|
154
|
+
with one-line invariant statements. The history is in git.
|
|
155
|
+
|
|
156
|
+
### Documentation
|
|
157
|
+
- **`WorkerApp#setup_once!` race comment corrected (again).** A prior
|
|
158
|
+
0.2.6 entry claimed `Ractor.current[:key] ||= Thread::Mutex.new` is
|
|
159
|
+
atomic in Ruby 4.0.6. It is NOT — `||=` is a read-then-write, and N
|
|
160
|
+
racing threads produce N distinct mutexes (verified by spec under a
|
|
161
|
+
widened window). The code is saved by (1) MRI's GIL serializing the
|
|
162
|
+
flag check inside `synchronize` and (2) `rebind_constants` /
|
|
163
|
+
`init_worker_ar_connections!` both being idempotent. Comment now states
|
|
164
|
+
the real contract + the escape hatch if either property changes. Two
|
|
165
|
+
specs pin the idempotency contract.
|
|
166
|
+
|
|
10
167
|
## [0.2.6]
|
|
11
168
|
|
|
12
169
|
### Fixed
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Fallback IsolatedExecutionState when ActiveSupport is not available.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# ActiveSupport::IsolatedExecutionState
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
#
|
|
5
|
+
# The shim's string-eval'd patch methods reference the literal constant
|
|
6
|
+
# `ActiveSupport::IsolatedExecutionState` (no captured binding → callable
|
|
7
|
+
# from any Ractor). To avoid opening the ActiveSupport namespace from a
|
|
8
|
+
# third-party gem, the fallback is defined in the shim's own namespace
|
|
9
|
+
# (RactorRailsShim::FallbackIES) and aliased onto
|
|
10
|
+
# ActiveSupport::IsolatedExecutionState only when the real AS IES is absent.
|
|
11
|
+
# When AS is loaded, the real one wins untouched and no alias is created.
|
|
12
|
+
module RactorRailsShim
|
|
13
|
+
module FallbackIES
|
|
9
14
|
KEY = :active_support_execution_state_fallback
|
|
10
15
|
|
|
11
16
|
class << self
|
|
@@ -30,4 +35,13 @@ module ActiveSupport
|
|
|
30
35
|
end
|
|
31
36
|
end
|
|
32
37
|
end
|
|
33
|
-
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Alias onto ActiveSupport::IsolatedExecutionState only when the real AS IES
|
|
41
|
+
# is absent, so the shim's string-eval'd references resolve to the fallback
|
|
42
|
+
# without the shim opening the ActiveSupport namespace to define a module.
|
|
43
|
+
unless defined?(ActiveSupport::IsolatedExecutionState)
|
|
44
|
+
module ActiveSupport
|
|
45
|
+
IsolatedExecutionState = RactorRailsShim::FallbackIES
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -293,7 +293,7 @@ module RactorRailsShim
|
|
|
293
293
|
# while the objects are still mutable) populates the ivars so the frozen,
|
|
294
294
|
# shared copies already hold the values and workers only read them.
|
|
295
295
|
if Ractor.main? && defined?(::Rails) && ::Rails.application
|
|
296
|
-
|
|
296
|
+
_swallow("warm journey routes") do
|
|
297
297
|
rset = ::Rails.application.routes
|
|
298
298
|
all = []
|
|
299
299
|
all.concat(rset.named_routes.send(:routes).values) rescue nil
|
|
@@ -310,8 +310,6 @@ module RactorRailsShim
|
|
|
310
310
|
p.required_names rescue nil
|
|
311
311
|
p.optional_names rescue nil
|
|
312
312
|
end
|
|
313
|
-
rescue
|
|
314
|
-
nil
|
|
315
313
|
end
|
|
316
314
|
end
|
|
317
315
|
# Capture the (shareable) RouteSet so workers can build URLs without
|
|
@@ -422,15 +420,15 @@ module RactorRailsShim
|
|
|
422
420
|
nrc.alias_method(:define_url_helper_without_shim, :define_url_helper)
|
|
423
421
|
end
|
|
424
422
|
nrc.define_method(:define_url_helper) do |mod, name, helper, url_strategy|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
423
|
+
# Detach the helper from the live route object before deep-freezing
|
|
424
|
+
# it for cross-Ractor sharing. The non-optimized UrlHelper#call only
|
|
425
|
+
# needs @options / @segment_keys / @route_name to build the options
|
|
426
|
+
# hash and then delegates to `t._routes.url_for(route_name, ...)`,
|
|
427
|
+
# which looks the route up in the (shareable) RouteSet by name. The
|
|
428
|
+
# @route reference would pull the whole route graph into the freeze,
|
|
429
|
+
# freezing objects that make_app_shareable! must still be able to
|
|
430
|
+
# mutate (e.g. Devise route constraints) -> FrozenError.
|
|
431
|
+
RactorRailsShim._swallow("freeze url helper") do
|
|
434
432
|
if helper.respond_to?(:instance_variable_get)
|
|
435
433
|
helper.instance_variable_set(:@route, nil) rescue nil
|
|
436
434
|
opts = helper.instance_variable_get(:@options)
|
|
@@ -439,8 +437,6 @@ module RactorRailsShim
|
|
|
439
437
|
helper.instance_variable_set(:@segment_keys, segs.dup.freeze) rescue nil
|
|
440
438
|
end
|
|
441
439
|
helper = Ractor.make_shareable(helper)
|
|
442
|
-
rescue
|
|
443
|
-
nil
|
|
444
440
|
end
|
|
445
441
|
RactorRailsShim::URL_HELPERS[name] = helper
|
|
446
442
|
strategy_const = url_strategy.equal?(::ActionDispatch::Routing::RouteSet::PATH) ?
|
|
@@ -705,7 +701,7 @@ module RactorRailsShim
|
|
|
705
701
|
if defined?(::ActionDispatch::Journey::GTG::Builder) &&
|
|
706
702
|
::ActionDispatch::Journey::GTG::Builder.const_defined?(:DUMMY_END_NODE)
|
|
707
703
|
node = ::ActionDispatch::Journey::GTG::Builder.const_get(:DUMMY_END_NODE)
|
|
708
|
-
Ractor.make_shareable(node)
|
|
704
|
+
_swallow("make journey dummy end node shareable") { Ractor.make_shareable(node) }
|
|
709
705
|
end
|
|
710
706
|
end
|
|
711
707
|
|
|
@@ -787,7 +783,7 @@ module RactorRailsShim
|
|
|
787
783
|
next unless c.is_a?(::Array) || c.is_a?(::Hash)
|
|
788
784
|
next if c.frozen?
|
|
789
785
|
c.freeze
|
|
790
|
-
::Ractor.make_shareable(c)
|
|
786
|
+
_swallow("make mime negotiation constant shareable") { ::Ractor.make_shareable(c) }
|
|
791
787
|
end
|
|
792
788
|
rescue => e
|
|
793
789
|
warn "[ractor-rails-shim] _freeze_mime_negotiation!: #{e.class}: #{e.message}"
|
|
@@ -244,7 +244,7 @@ module RactorRailsShim
|
|
|
244
244
|
(defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application) ? ::Rails.application.routes : nil,
|
|
245
245
|
nil
|
|
246
246
|
)
|
|
247
|
-
Ractor.make_shareable(fallback)
|
|
247
|
+
fallback = _swallow("make view context fallback shareable") { Ractor.make_shareable(fallback) }
|
|
248
248
|
self._view_context_fallback = fallback
|
|
249
249
|
end
|
|
250
250
|
|
|
@@ -199,12 +199,7 @@ module RactorRailsShim
|
|
|
199
199
|
end
|
|
200
200
|
snapshot.freeze
|
|
201
201
|
Ractor.make_shareable(snapshot)
|
|
202
|
-
|
|
203
|
-
begin
|
|
204
|
-
const_set(:AR_CONFIGURATIONS_SNAPSHOT, snapshot)
|
|
205
|
-
ensure
|
|
206
|
-
$VERBOSE = verbose
|
|
207
|
-
end
|
|
202
|
+
_reassign_shareable_const(:AR_CONFIGURATIONS_SNAPSHOT, snapshot)
|
|
208
203
|
rescue => e
|
|
209
204
|
# Best-effort; if we can't capture configs, workers won't be able
|
|
210
205
|
# to auto-init connections. They can call init_worker_ar_connections!
|
|
@@ -459,12 +454,7 @@ module RactorRailsShim
|
|
|
459
454
|
pk_map[n] = pk if pk
|
|
460
455
|
end
|
|
461
456
|
shareable = Ractor.make_shareable(pk_map)
|
|
462
|
-
|
|
463
|
-
begin
|
|
464
|
-
const_set(:AR_PRIMARY_KEYS_SHAREABLE, shareable)
|
|
465
|
-
ensure
|
|
466
|
-
$VERBOSE = verbose
|
|
467
|
-
end
|
|
457
|
+
_reassign_shareable_const(:AR_PRIMARY_KEYS_SHAREABLE, shareable)
|
|
468
458
|
rescue => e
|
|
469
459
|
# best-effort
|
|
470
460
|
end
|
|
@@ -916,12 +906,7 @@ module RactorRailsShim
|
|
|
916
906
|
cfg = ::ActiveRecord::Base.configurations
|
|
917
907
|
cfg = Ractor.make_shareable(cfg) if cfg
|
|
918
908
|
if cfg
|
|
919
|
-
|
|
920
|
-
begin
|
|
921
|
-
const_set(:AR_CONFIGURATIONS_SHAREABLE, cfg)
|
|
922
|
-
ensure
|
|
923
|
-
$VERBOSE = verbose
|
|
924
|
-
end
|
|
909
|
+
_reassign_shareable_const(:AR_CONFIGURATIONS_SHAREABLE, cfg)
|
|
925
910
|
end
|
|
926
911
|
rescue => e
|
|
927
912
|
# best-effort
|
|
@@ -977,14 +962,9 @@ module RactorRailsShim
|
|
|
977
962
|
handlers = ::ActiveRecord::DatabaseConfigurations.db_config_handlers
|
|
978
963
|
# Make each handler Proc shareable (freezes its binding). A shareable
|
|
979
964
|
# Proc is callable from any Ractor.
|
|
980
|
-
handlers.each { |h| Ractor.make_shareable(h)
|
|
965
|
+
handlers.each { |h| _swallow("make ar db config handler shareable") { Ractor.make_shareable(h) } }
|
|
981
966
|
shareable = Ractor.make_shareable(handlers.dup)
|
|
982
|
-
|
|
983
|
-
begin
|
|
984
|
-
const_set(:AR_DB_CONFIG_HANDLERS_SHAREABLE, shareable)
|
|
985
|
-
ensure
|
|
986
|
-
$VERBOSE = verbose
|
|
987
|
-
end
|
|
967
|
+
_reassign_shareable_const(:AR_DB_CONFIG_HANDLERS_SHAREABLE, shareable)
|
|
988
968
|
rescue => e
|
|
989
969
|
# best-effort
|
|
990
970
|
end
|
|
@@ -1031,14 +1011,9 @@ module RactorRailsShim
|
|
|
1031
1011
|
if Ractor.main?
|
|
1032
1012
|
begin
|
|
1033
1013
|
transformers = ::ActiveRecord.query_transformers
|
|
1034
|
-
transformers.each { |t| Ractor.make_shareable(t)
|
|
1014
|
+
transformers.each { |t| _swallow("make ar query transformer shareable") { Ractor.make_shareable(t) } }
|
|
1035
1015
|
shareable = Ractor.make_shareable(transformers.dup)
|
|
1036
|
-
|
|
1037
|
-
begin
|
|
1038
|
-
const_set(:AR_QUERY_TRANSFORMERS_SHAREABLE, shareable)
|
|
1039
|
-
ensure
|
|
1040
|
-
$VERBOSE = verbose
|
|
1041
|
-
end
|
|
1016
|
+
_reassign_shareable_const(:AR_QUERY_TRANSFORMERS_SHAREABLE, shareable)
|
|
1042
1017
|
rescue => e
|
|
1043
1018
|
# best-effort
|
|
1044
1019
|
end
|
|
@@ -1085,12 +1060,7 @@ module RactorRailsShim
|
|
|
1085
1060
|
begin
|
|
1086
1061
|
val = ::ActiveRecord.public_send(method_name)
|
|
1087
1062
|
shareable = Ractor.make_shareable(val.is_a?(::Array) ? val.dup : val)
|
|
1088
|
-
|
|
1089
|
-
begin
|
|
1090
|
-
const_set(const_name, shareable)
|
|
1091
|
-
ensure
|
|
1092
|
-
$VERBOSE = verbose
|
|
1093
|
-
end
|
|
1063
|
+
_reassign_shareable_const(const_name, shareable)
|
|
1094
1064
|
rescue => e
|
|
1095
1065
|
# best-effort
|
|
1096
1066
|
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Callable / lock-replacement object model. These classes are shareable
|
|
4
|
+
# stand-ins for unshareable Rails internals (self-capturing Procs, Mutexes,
|
|
5
|
+
# IO-backed log devices) that make_app_shareable! swaps into the app graph
|
|
6
|
+
# before Ractor.make_shareable. Extracted from make_shareable.rb so the
|
|
7
|
+
# object model has its own home.
|
|
8
|
+
#
|
|
9
|
+
# Defined on RactorRailsShim's singleton class (the same access path the rest
|
|
10
|
+
# of the codebase uses: RactorRailsShim.singleton_class.const_get(:NoOpProc)).
|
|
11
|
+
|
|
12
|
+
module RactorRailsShim
|
|
13
|
+
class << self
|
|
14
|
+
class NoOpProc
|
|
15
|
+
def call(*_); nil; end
|
|
16
|
+
# A NoOpProc is a shareable stand-in for an arbitrary Proc in the app
|
|
17
|
+
# graph. Some Rails code passes such values through `&block`, which
|
|
18
|
+
# calls `#to_proc` and then requires the result to be a real Proc.
|
|
19
|
+
# Return a frozen no-op lambda so the implicit conversion succeeds and
|
|
20
|
+
# the (side-effect-free) call is a true no-op, matching `#call`.
|
|
21
|
+
#
|
|
22
|
+
# The lambda is a shareable constant (frozen at class load), NOT
|
|
23
|
+
# memoized on `@_to_proc`: NoOpProc instances are deep-frozen by
|
|
24
|
+
# `Ractor.make_shareable` during `make_app_shareable!`, so an
|
|
25
|
+
# `@_to_proc ||= ...` write would raise FrozenError on the frozen
|
|
26
|
+
# instance. A constant avoids the write entirely and is safe to share.
|
|
27
|
+
NO_OP_LAMBDA = ->(*) { nil }.freeze
|
|
28
|
+
Ractor.make_shareable(NO_OP_LAMBDA)
|
|
29
|
+
def to_proc
|
|
30
|
+
NO_OP_LAMBDA
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Callable
|
|
35
|
+
def initialize(target, method_name)
|
|
36
|
+
@target = target
|
|
37
|
+
@method_name = method_name
|
|
38
|
+
end
|
|
39
|
+
def call(*args)
|
|
40
|
+
@target.__send__(@method_name, *args)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class CallableConst
|
|
45
|
+
def initialize(value); @value = value; end
|
|
46
|
+
def call(*_); @value; end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Shareable snapshot of a Devise::Mapping. The real Mapping holds an
|
|
50
|
+
# unshareable lambda (failure_app) plus a default-proc Hash (controllers),
|
|
51
|
+
# so it can't be Ractor.make_shareable'd. Request-time code only reads a
|
|
52
|
+
# handful of attributes (name, to/class, router_name, controllers, ...),
|
|
53
|
+
# which are all shareable values. We copy those now (in main) into a
|
|
54
|
+
# frozen Plain Old Object that plays the role of the Mapping in workers.
|
|
55
|
+
class DeviseMappingSnapshot
|
|
56
|
+
def initialize(mapping)
|
|
57
|
+
@name = mapping.name
|
|
58
|
+
@klass = mapping.to
|
|
59
|
+
@router_name = mapping.instance_variable_get(:@router_name)
|
|
60
|
+
@singular = mapping.instance_variable_get(:@singular)
|
|
61
|
+
@scoped_path = mapping.instance_variable_get(:@scoped_path)
|
|
62
|
+
@path = mapping.instance_variable_get(:@path)
|
|
63
|
+
@path_prefix = mapping.instance_variable_get(:@path_prefix)
|
|
64
|
+
@format = mapping.instance_variable_get(:@format)
|
|
65
|
+
@sign_out_via = mapping.instance_variable_get(:@sign_out_via)
|
|
66
|
+
@modules = mapping.modules
|
|
67
|
+
@strategies = mapping.strategies
|
|
68
|
+
@routes = mapping.routes
|
|
69
|
+
@used_helpers = mapping.used_helpers
|
|
70
|
+
# controllers is a Hash with a default proc (unshareable) — copy the
|
|
71
|
+
# entries into a plain frozen Hash.
|
|
72
|
+
h = {}
|
|
73
|
+
RactorRailsShim._swallow("devise mapping controllers") do
|
|
74
|
+
mapping.controllers.each { |k, v| h[k] = v }
|
|
75
|
+
end
|
|
76
|
+
@controllers = h.freeze
|
|
77
|
+
# failure_app is either Devise::FailureApp (a shareable class) or a
|
|
78
|
+
# lambda (when configured as a String) — keep only the shareable class.
|
|
79
|
+
fa = mapping.instance_variable_get(:@failure_app)
|
|
80
|
+
fa = ::Devise::FailureApp unless fa.is_a?(Class)
|
|
81
|
+
@failure_app = fa
|
|
82
|
+
freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def name; @name; end
|
|
86
|
+
def to; @klass; end
|
|
87
|
+
def router_name; @router_name; end
|
|
88
|
+
def singular; @singular; end
|
|
89
|
+
def scoped_path; @scoped_path; end
|
|
90
|
+
def path; @path; end
|
|
91
|
+
def path_prefix; @path_prefix; end
|
|
92
|
+
def format; @format; end
|
|
93
|
+
def sign_out_via; @sign_out_via; end
|
|
94
|
+
def modules; @modules; end
|
|
95
|
+
def strategies; @strategies; end
|
|
96
|
+
def routes; @routes; end
|
|
97
|
+
def used_helpers; @used_helpers; end
|
|
98
|
+
def controllers; @controllers; end
|
|
99
|
+
def failure_app; @failure_app; end
|
|
100
|
+
def authenticatable?; @modules.any? { |m| m.to_s =~ /authenticatable/ }; end
|
|
101
|
+
def no_input_strategies; @strategies & Devise::NO_INPUT; end
|
|
102
|
+
def fullpath; "/#{@path_prefix}/#{@path}".squeeze("/"); end
|
|
103
|
+
# Devise::Mapping defines one `x?` predicate per Devise module
|
|
104
|
+
# (confirmable?, rememberable?, registerable?, ...) via `add_module`.
|
|
105
|
+
# Rather than enumerate them, fall back for any `x?` predicate to
|
|
106
|
+
# checking @modules — matching the generated behaviour.
|
|
107
|
+
def respond_to_missing?(method, _)
|
|
108
|
+
method.to_s.end_with?("?") || super
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def method_missing(method, *args)
|
|
112
|
+
s = method.to_s
|
|
113
|
+
if s.end_with?("?") && args.empty?
|
|
114
|
+
@modules.include?(s.chomp("?").to_sym)
|
|
115
|
+
else
|
|
116
|
+
super
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def _devise_mapping_snapshot(mapping)
|
|
122
|
+
_swallow("devise mapping snapshot") { DeviseMappingSnapshot.new(mapping) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class NoOpLock
|
|
126
|
+
def synchronize; yield; end
|
|
127
|
+
def mon_synchronize; yield; end
|
|
128
|
+
def lock; self; end
|
|
129
|
+
def unlock; self; end
|
|
130
|
+
def locked?; false; end
|
|
131
|
+
def mon_enter; end
|
|
132
|
+
def mon_exit; end
|
|
133
|
+
def mon_locked?; false; end
|
|
134
|
+
def try_lock; true; end
|
|
135
|
+
def new_cond; Struct.new(:wait, :signal, :broadcast).new(-> {}, -> {}, -> {}); end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# No-op log device sink: a frozen, shareable stand-in for an IO, swapped
|
|
139
|
+
# in for $stdout/$stderr in the app's logger before make_shareable so
|
|
140
|
+
# the real IOs aren't frozen. Responds to the write methods a
|
|
141
|
+
# Logger::LogDevice might call.
|
|
142
|
+
class NoOpLogDev
|
|
143
|
+
def write(*_); self; end
|
|
144
|
+
def <<(*_); self; end
|
|
145
|
+
def puts(*_); self; end
|
|
146
|
+
def print(*_); self; end
|
|
147
|
+
def flush; self; end
|
|
148
|
+
def close; self; end
|
|
149
|
+
def sync=(*_); self; end
|
|
150
|
+
def binmode; self; end
|
|
151
|
+
def tty?; false; end
|
|
152
|
+
def closed?; false; end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|