ractor-rails-shim 0.2.4 → 0.2.6
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 +140 -2
- data/README.md +90 -27
- data/lib/ractor_rails_shim/patches/action_controller.rb +10 -1
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +1 -1
- data/lib/ractor_rails_shim/patches/action_view.rb +2 -2
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +17 -0
- data/lib/ractor_rails_shim/patches/active_support.rb +20 -20
- data/lib/ractor_rails_shim/patches/activerecord.rb +102 -20
- data/lib/ractor_rails_shim/patches/class_attribute.rb +17 -4
- data/lib/ractor_rails_shim/patches/core.rb +121 -51
- data/lib/ractor_rails_shim/patches/devise.rb +1 -1
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +1 -1
- data/lib/ractor_rails_shim/patches/kaminari.rb +1 -1
- data/lib/ractor_rails_shim/patches/make_shareable.rb +126 -37
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +2 -2
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +1 -1
- data/lib/ractor_rails_shim/patches/rack.rb +5 -5
- data/lib/ractor_rails_shim/patches/rails_module.rb +13 -6
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +8 -1
- data/lib/ractor_rails_shim/version.rb +1 -1
- data/lib/ractor_rails_shim/version_check.rb +10 -6
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a4b7ce968f90326f2b476d713b7fab6c524f8f7b111f60a2ae582d1e162de88
|
|
4
|
+
data.tar.gz: c144478be527ff98804db48add54d67edb4124514fba3670b970990ef09e36c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55cc0fb00ebd523182cb66f8ae7a9f270bf33bcaaf928af79d291edf3eeb0248246f956e082200ab74563c7afceee8520336f53373ed9d54ff73f4db9ce05f40
|
|
7
|
+
data.tar.gz: b19280d1985a1e8b456a6a13750e277a49c5123998a4f7fab587518d3d6a717249d9490cfbe6d32751483d9c5e622373d861fbed9ce50d8daeabb09578b897e9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,9 +7,147 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.6]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`WorkerApp` from `worker_app` not `Ractor.shareable?`.** The factory
|
|
14
|
+
returned a bare `WorkerApp` instance without freezing or making it
|
|
15
|
+
shareable, so any caller following the README's
|
|
16
|
+
`Ractor.new(worker_app) { |a| a.call(env) }` example would raise
|
|
17
|
+
`Ractor::IsolationError` at spawn. `worker_app` now freezes +
|
|
18
|
+
`Ractor.make_shareable`s the instance, enforcing the shareability
|
|
19
|
+
contract at the boundary instead of pushing it onto every caller.
|
|
20
|
+
(`patches/core.rb`)
|
|
21
|
+
|
|
22
|
+
- **`Hash#compute_if_absent` nil-caching divergence from
|
|
23
|
+
`Concurrent::Map`.** The shim replaces `Concurrent::Map` instances in
|
|
24
|
+
the frozen app graph with plain `Hash`es (Concurrent::Map isn't
|
|
25
|
+
Ractor-shareable) and adds a compatible `compute_if_absent`. The
|
|
26
|
+
frozen-Hash branch used `IES[key] ||= yield`, which SKIPS nil results
|
|
27
|
+
— but `Concurrent::Map#compute_if_absent` STORES nil (verified: the
|
|
28
|
+
key is present after a nil-result block call), so the shim's
|
|
29
|
+
divergent semantics re-invoked the block on every subsequent lookup
|
|
30
|
+
for any cache slot whose computed value was nil. The frozen branch
|
|
31
|
+
now uses a two-level IES bucket (`slot[object_id][key]`) with a
|
|
32
|
+
proper `key?` presence check, matching `Concurrent::Map` exactly.
|
|
33
|
+
Also fixed: an existing key on a frozen Hash now returns without
|
|
34
|
+
invoking the block (was always treating the receiver as empty), and
|
|
35
|
+
per-Hash isolation is now keyed by `object_id` so two frozen hashes
|
|
36
|
+
with the same key don't collide. (`patches/core.rb`)
|
|
37
|
+
|
|
38
|
+
- **Magic `line == 32` strategy dispatch replaced with identity check.**
|
|
39
|
+
The Proc-replacement pass distinguished
|
|
40
|
+
`ActionDispatch::Routing::Mapper::Constraints::SERVE` from `CALL` by
|
|
41
|
+
hard-coding the `source_location` line number (32). Any Rails patch
|
|
42
|
+
release that shifted the constant definitions by even one line would
|
|
43
|
+
silently swap the two strategies and break routing (a dispatcher
|
|
44
|
+
endpoint called as `app.call(req.env)` instead of `app.serve(req)`).
|
|
45
|
+
Replaced with an `equal?` check against the actual constants — the
|
|
46
|
+
value stored in `@strategy` IS the constant object (Rails assigns it
|
|
47
|
+
by reference), so identity is the robust identifier, independent of
|
|
48
|
+
source layout. Falls through to `NoOpProc` for an unknown Proc
|
|
49
|
+
(defensive; never mis-routes). Extracted into
|
|
50
|
+
`_strategy_replacement_for`. (`patches/make_shareable.rb`)
|
|
51
|
+
|
|
52
|
+
- **`capture_app_constants` crash on non-Zeitwerk autoloaders.** The
|
|
53
|
+
method called `Rails.autoloaders.main` / `.once` directly, which
|
|
54
|
+
`NoMethodError`s for apps in classic-loader mode or with
|
|
55
|
+
`config.autoloaders = false` (the autoloaders object exists but
|
|
56
|
+
doesn't expose `main`/`once`). Now filters to the loaders that
|
|
57
|
+
actually expose `all_expected_cpaths` (the Zeitwerk introspection
|
|
58
|
+
API), and tolerates autoloaders objects that only respond to `each`.
|
|
59
|
+
Also fixed a pre-existing bug: the early-return path returned an
|
|
60
|
+
UNFROZEN map, violating the method's documented frozen-map contract.
|
|
61
|
+
(`patches/core.rb`)
|
|
62
|
+
|
|
10
63
|
### Changed
|
|
11
|
-
-
|
|
12
|
-
|
|
64
|
+
- **`_replace_unshareable_procs!` 3.times magic replaced with a
|
|
65
|
+
fixed-point loop.** The original `3.times` had no justification and
|
|
66
|
+
an arbitrary constant. Replaced with a loop-to-fixed-point (break
|
|
67
|
+
when `_collect_procs` returns empty) plus a safety cap of 8 passes
|
|
68
|
+
to guard against a pathological graph where replacement keeps
|
|
69
|
+
introducing new Procs (the replacements are `NoOpProc`/`Callable`,
|
|
70
|
+
not `Proc`, so this shouldn't happen, but the cap prevents an
|
|
71
|
+
infinite loop). Documented the multi-occurrence rationale (the same
|
|
72
|
+
Proc object can live in many containers, e.g. shared deprecation
|
|
73
|
+
behaviors). Observed real graphs converge in 2 passes.
|
|
74
|
+
(`patches/make_shareable.rb`)
|
|
75
|
+
|
|
76
|
+
- **Graph traversals now walk Set/Struct/Enumerable.**
|
|
77
|
+
`_collect_procs` and `_replace_locks_and_concurrent_maps!` only
|
|
78
|
+
handled `Array` and `Hash` as recursive containers. Rails uses `Set`
|
|
79
|
+
in several caches (e.g. `ActionDispatch::Journey::Routes`), and
|
|
80
|
+
`Struct`/`Enumerable` mixes appear in framework internals — a
|
|
81
|
+
`Mutex` or `Proc` nested inside one of those was missed and left
|
|
82
|
+
unshareable, breaking `make_app_shareable!` downstream. Extracted
|
|
83
|
+
`_each_ivar_and_child` (single responsibility: enumerate every child
|
|
84
|
+
reference of an object) shared by both traversals, handling `Hash`,
|
|
85
|
+
`Array`, `Set`, `Struct`, and a generic `Enumerable` fallback
|
|
86
|
+
(`Range`, `Enumerator`, custom mixins). The `Hash#default_proc`
|
|
87
|
+
branch is preserved so `_collect_procs` still flags default-proc
|
|
88
|
+
Procs. (`patches/make_shareable.rb`)
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- **Debug funnel for freeze-path silent rescues.** The
|
|
92
|
+
freeze/shareability paths (`_freeze_active_record_class_ivars!`,
|
|
93
|
+
`_freeze_global_class_ivars!`, `_make_value_shareable`) rescue all
|
|
94
|
+
exceptions silently with bare `rescue; nil`. Individual failures are
|
|
95
|
+
expected (some ivars hold intrinsically unshareable values like
|
|
96
|
+
Procs), but a worker Ractor that later crashes on the same
|
|
97
|
+
unshareable value had no traceable cause. New `_swallow(label) { ... }`
|
|
98
|
+
funnel is silent by default (preserves backward compat) and emits a
|
|
99
|
+
labeled `[ractor_rails_shim] <label>: <class>: <msg>` line to
|
|
100
|
+
`$stderr` when `RactorRailsShim.debug = true`. The three freeze-path
|
|
101
|
+
rescues are routed through it; the bare rescues in non-freeze paths
|
|
102
|
+
(require `LoadError`, optional `const_get`, etc.) are left as-is —
|
|
103
|
+
those are genuinely expected and a `$stderr` line would be noise.
|
|
104
|
+
(`patches/core.rb`)
|
|
105
|
+
|
|
106
|
+
### Documentation
|
|
107
|
+
- **`WorkerApp#setup_once!` race comment corrected.** The previous
|
|
108
|
+
comment claimed `Ractor.current[:key] ||= Thread::Mutex.new` was racy
|
|
109
|
+
under "extreme contention" and dismissed it as harmless because the
|
|
110
|
+
init steps are idempotent. Verified on Ruby 4.0.6 that
|
|
111
|
+
`Ractor.current[:key] ||= X` is atomic (100 concurrent threads
|
|
112
|
+
produce exactly one mutex object), so the TOCTOU race does not
|
|
113
|
+
occur. Replaced the misleading comment with the verified finding.
|
|
114
|
+
|
|
115
|
+
## [0.2.5]
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
- **Nil-sentinel storage bug in IES-routed accessors.** The shim's
|
|
119
|
+
`mattr_accessor` / `class_attribute` / Rails-module / AR / Rack / Devise /
|
|
120
|
+
Kaminari / I18n / Inflector / ActionView / ActionController /
|
|
121
|
+
ActionDispatch / Zeitwerk / ExecutionWrapper readers used
|
|
122
|
+
`return v unless v.nil?` to detect "no per-Ractor override has been set."
|
|
123
|
+
That made `Foo.x = nil` (or `= false`) indistinguishable from "never set,"
|
|
124
|
+
so the reader silently fell through to the default/fallback instead of the
|
|
125
|
+
user's explicit `nil`/`false` — a divergence from threaded Rails, where
|
|
126
|
+
class variables distinguish "undefined" (`class_variable_defined?` is
|
|
127
|
+
false) from "set to nil." Replaced the nil-sentinel with `<storage>.key?`
|
|
128
|
+
at every IES / `CLASS_ATTR_VALUES` / `SHAREABLE_FALLBACK` reader so any
|
|
129
|
+
explicit assignment — including `nil` and `false` — wins over the
|
|
130
|
+
fallback. The single `Ractor.current[:active_record_connection_handler]`
|
|
131
|
+
reader keeps the nil-sentinel (with an explanatory comment) because
|
|
132
|
+
`Ractor#[]` has no `key?` method and `connection_handler=` is never called
|
|
133
|
+
with nil in practice. 58 sites transformed across 13 patch files.
|
|
134
|
+
Regression specs in `spec/sentinel_spec.rb` cover the
|
|
135
|
+
`mattr_accessor :flag, default: true; Flag = nil; Flag # => nil` and
|
|
136
|
+
`class_attribute :setting, default: :on; Setting = nil; Setting # => nil`
|
|
137
|
+
cases.
|
|
138
|
+
|
|
139
|
+
- **`Ractor::IsolationError` in worker schema reload.** When a worker Ractor
|
|
140
|
+
hit a cold schema (e.g. first request after boot, or after
|
|
141
|
+
`ActiveRecord::Base.connection_handler.clear_all_connections!`),
|
|
142
|
+
`ModelSchema::ClassMethods#reload_schema_from_cache` and
|
|
143
|
+
`Timestamp::ClassMethods#reload_schema_from_cache` wrote to class instance
|
|
144
|
+
variables (`@columns`, `@columns_hash`, …) on the shared model classes,
|
|
145
|
+
raising `Ractor::IsolationError: can not set instance variables of
|
|
146
|
+
classes/modules by non-main Ractors`. Patched both methods (plus
|
|
147
|
+
`AttributeRegistration::ClassMethods#reset_default_attributes!`) to clear
|
|
148
|
+
the worker's IES slots instead of writing class ivars, so the next read
|
|
149
|
+
re-derives the schema in the worker's own IES. Without this, any worker
|
|
150
|
+
that reloaded its schema crashed the first request after the reload.
|
|
13
151
|
|
|
14
152
|
## [0.2.4]
|
|
15
153
|
|
data/README.md
CHANGED
|
@@ -4,27 +4,34 @@ A monkey-patch shim that reroutes Rails' class-level instance variable accessors
|
|
|
4
4
|
|
|
5
5
|
**Status:** proof-of-concept / stopgap. The goal is for Rails to do this upstream, at which point this gem becomes a no-op and can be removed.
|
|
6
6
|
|
|
7
|
-
**Current status:** on a full Rails 8.1 app (Devise 5, Propshaft,
|
|
8
|
-
PG) under Ruby 4.0.
|
|
9
|
-
`GET /up`, full ERB view rendering, Devise sign-in/sign-out
|
|
10
|
-
**and** validation), and authenticated Devise **writes**
|
|
11
|
-
row persisted). A worker Ractor dispatches `GET /up` →
|
|
12
|
-
`RactorRailsShim.make_app_shareable!`. The shim builds a
|
|
13
|
-
table for framework class config (`class_attribute` /
|
|
14
|
-
and patches the raw class-ivar accessors Rails reads
|
|
15
|
-
(`ExecutionWrapper.active_key`, `Notifications.notifier`,
|
|
16
|
-
`PathRegistry`, `I18n`, `AbstractController` lazy ivars,
|
|
17
|
-
`ExecutionContext`, etc.).
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
**Current status (v0.2.5):** on a full Rails 8.1 app (Devise 5, Propshaft,
|
|
8
|
+
Kaminari, PG) under **official Ruby 4.0.6**, worker Ractors serve **every**
|
|
9
|
+
routable action — `GET /up`, full ERB view rendering, Devise sign-in/sign-out
|
|
10
|
+
(CSRF issuance **and** validation), and authenticated Devise **writes**
|
|
11
|
+
(`POST /posts` → 302, row persisted). A worker Ractor dispatches `GET /up` →
|
|
12
|
+
**HTTP 200** via `RactorRailsShim.make_app_shareable!`. The shim builds a
|
|
13
|
+
shareable fallback table for framework class config (`class_attribute` /
|
|
14
|
+
`mattr_accessor` values) and patches the raw class-ivar accessors Rails reads
|
|
15
|
+
per-request (`ExecutionWrapper.active_key`, `Notifications.notifier`,
|
|
16
|
+
`Inflections`, `PathRegistry`, `I18n`, `AbstractController` lazy ivars,
|
|
17
|
+
`Rack::Request`/`Utils`, `ExecutionContext`, etc.). 61 unit specs across 7
|
|
18
|
+
spec files pass (no Rails dependency); an integration spec dispatches `GET /up`
|
|
19
|
+
→ 200 in a worker Ractor.
|
|
20
|
+
|
|
21
|
+
**No patched Ruby or kino required.** Official Ruby 4.0.6 ships the frozen-iseq
|
|
22
|
+
call-cache fix (#22075) and the cross-ractor env-string fix, so the SIGBUS crashes
|
|
23
|
+
that previously required the DDKatch patched Ruby/kino forks are resolved in core.
|
|
24
|
+
Both SIGBUS classes were the only reason a patched build was ever needed; on 4.0.6
|
|
25
|
+
the shim runs `kino -m ractor` on the official `kino` gem (0.1.3) under sustained
|
|
26
|
+
read **and** write load (verified: 0 transport failures / 0 server errors across
|
|
27
|
+
`/up`, `GET /posts`, `POST /posts` in the benchmark matrix).
|
|
22
28
|
|
|
23
29
|
## Requirements
|
|
24
30
|
|
|
25
|
-
- **Ruby >= 4.0** — the shim relies on Ruby 4.0's Ractor semantics and
|
|
26
|
-
`Ractor.make_shareable
|
|
27
|
-
|
|
31
|
+
- **Ruby >= 4.0.6** — the shim relies on Ruby 4.0's Ractor semantics and
|
|
32
|
+
`Ractor.make_shareable`, plus the frozen-iseq call-cache fix (#22075) and
|
|
33
|
+
the cross-ractor env-string fix that both shipped in 4.0.6. It will not
|
|
34
|
+
work (and refuses to install) on earlier Ruby versions.
|
|
28
35
|
- **Rails ~> 8.1** — tested against Rails 8.1.x class layouts. Other versions
|
|
29
36
|
are not yet supported (see Version compatibility below).
|
|
30
37
|
|
|
@@ -35,19 +42,75 @@ run a Rails app in Ractor mode is
|
|
|
35
42
|
developed and tested against a real Rails 8.1 app **served by kino**; that is
|
|
36
43
|
the configuration it is verified against.
|
|
37
44
|
|
|
38
|
-
**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
**kino:** the Ractor-mode server is
|
|
46
|
+
[kino](https://github.com/yaroslav/kino) (`kino -m ractor`). On official Ruby
|
|
47
|
+
4.0.6 the **upstream `kino` gem (0.1.3) works as-is** — no fork or patch is
|
|
48
|
+
required. (Historically a DDKatch/kino fork carried a per-ractor env-string
|
|
49
|
+
cache fix; that fix landed in official 4.0.6, so the fork is obsolete.)
|
|
50
|
+
|
|
51
|
+
**Threaded servers (Puma/Falcon/Thin/Webrick):** set
|
|
52
|
+
`RactorRailsShim.thread_mode = true` (or `ENV["SERVER"]` to one of
|
|
53
|
+
`puma|falcon|thin|webrick|thread*`) and the shim installs a **minimal**
|
|
54
|
+
patch set — only the `class_attribute` isolation fix + nil-safe callback
|
|
55
|
+
replay. The per-Ractor IES-routing patches are skipped because IES is empty
|
|
56
|
+
on a thread server's request threads and would break the app; Rails' own
|
|
57
|
+
class globals are thread-safe and used as-is. Use this when you want the
|
|
58
|
+
callback-correctness fixes without running in Ractor mode.
|
|
46
59
|
|
|
47
60
|
**Benchmarks:** throughput/latency/memory of this shim + the test app under
|
|
48
61
|
kino `:ractor` vs Puma vs Falcon are documented in
|
|
49
62
|
[ractor-rails-shim-test-app/BENCHMARKS.md](https://github.com/DDKatch/ractor-rails-shim-test-app/blob/main/BENCHMARKS.md).
|
|
50
63
|
|
|
64
|
+
## Repository layout
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
lib/ractor_rails_shim.rb # entry point — autoload-install if Rails is loaded
|
|
68
|
+
lib/ractor_rails_shim/
|
|
69
|
+
version.rb # VERSION constant (currently 0.2.5)
|
|
70
|
+
version_check.rb # Gem::Version-based Ruby/Rails detection + policy
|
|
71
|
+
fallback_ies.rb # thread-local IES shim when ActiveSupport is absent
|
|
72
|
+
check.rb # the ractor-rails-check audit (Check.scan / report)
|
|
73
|
+
patches.rb # requires all per-concern patch files in order
|
|
74
|
+
patches/
|
|
75
|
+
core.rb # module skeleton, registries, install, prepare_for_ractors!, WorkerApp
|
|
76
|
+
make_shareable.rb # make_app_shareable!, callable/lock replacement, shareable fallback
|
|
77
|
+
rails_module.rb # Rails.application / Rails.cache / Rails.logger / Rails.env …
|
|
78
|
+
mattr_accessor.rb # Module#mattr_accessor / cattr_accessor macro rewrite
|
|
79
|
+
class_attribute.rb # ActiveSupport class_attribute macro rewrite
|
|
80
|
+
zeitwerk_registry.rb # Zeitwerk::Registry class ivars
|
|
81
|
+
route_helpers.rb # ActionDispatch::Routing::RouteSet#generate_url_helpers
|
|
82
|
+
url_helpers.rb # URL helper singleton + module fixes
|
|
83
|
+
execution_wrapper.rb # ActiveSupport::ExecutionWrapper + callback replay
|
|
84
|
+
rack.rb # Rack::Request / Rack::Utils
|
|
85
|
+
action_view.rb # PathRegistry, LookupContext, Template handlers, compiled_method_container
|
|
86
|
+
action_controller.rb # AbstractController, ActionController name/encoding
|
|
87
|
+
action_dispatch.rb # ActionDispatch routing, http_url, journey, mounted helpers
|
|
88
|
+
polymorphic_routes.rb # polymorphic_path(s) URL helpers
|
|
89
|
+
active_support.rb # Inflector, error_reporter, ExecutionContext, I18n, JSON encoding, Reloader
|
|
90
|
+
warden.rb # Warden hooks / strategies / serializer
|
|
91
|
+
devise.rb # Devise url_helpers / authenticatable / failure_app
|
|
92
|
+
active_model_attribute.rb # ActiveModel::Attribute dup_or_share for frozen graphs
|
|
93
|
+
active_record_model_schema.rb # AR ModelSchema reload_schema_from_cache (worker-safe)
|
|
94
|
+
activerecord.rb # AR connection handler, configurations, query caches, …
|
|
95
|
+
kaminari.rb # Kaminari config
|
|
96
|
+
propshaft.rb # Propshaft asset server
|
|
97
|
+
orm_adapter.rb # orm_adapter
|
|
98
|
+
openssl.rb # OpenSSL digest cache
|
|
99
|
+
rubygems.rb # Rubygems msgpack pre-check
|
|
100
|
+
exe/ractor-rails-check # CLI audit tool
|
|
101
|
+
spec/ # 7 spec files, 61 unit tests (no Rails dep) + integration spec
|
|
102
|
+
script/make_test_app.sh # build the minimal Rails 8.1 test app (CI uses this)
|
|
103
|
+
script/make_full_test_app.sh # build the full-featured test app (Devise/PG/Kaminari)
|
|
104
|
+
.github/workflows/ci.yml # unit job + integration job (GET /up → 200 in a worker Ractor)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Per-concern patch files reopen `RactorRailsShim`'s singleton class to add
|
|
108
|
+
their `_install_*` methods. `patches.rb` requires them in dependency order
|
|
109
|
+
(`core.rb` first — it defines the module skeleton + registries the rest
|
|
110
|
+
reference). Each `_install_*` method is idempotent (guarded by its own
|
|
111
|
+
`@*_patched` flag), so `install`, `prepare_for_ractors!`, and
|
|
112
|
+
`make_app_shareable!` can all call the full set safely.
|
|
113
|
+
|
|
51
114
|
## Why
|
|
52
115
|
|
|
53
116
|
Rails stores global state in class-level instance variables:
|
|
@@ -72,7 +135,7 @@ This is the primary blocker preventing Rails from running in Ractor mode (see th
|
|
|
72
135
|
|
|
73
136
|
## How it works
|
|
74
137
|
|
|
75
|
-
The shim reroutes the accessor methods through `ActiveSupport::IsolatedExecutionState`, which is thread-local storage (`Thread.current[:key]`). Each Ractor has its own threads, so each Ractor gets its own slot automatically — verified on Ruby 4.0.
|
|
138
|
+
The shim reroutes the accessor methods through `ActiveSupport::IsolatedExecutionState`, which is thread-local storage (`Thread.current[:key]`). Each Ractor has its own threads, so each Ractor gets its own slot automatically — verified on Ruby 4.0.6. Rails already uses this primitive for `ActiveRecord::ConnectionHandling.connection_handler` (for thread safety), so the pattern is proven in production.
|
|
76
139
|
|
|
77
140
|
Two storage paths, depending on the state's shape:
|
|
78
141
|
|
|
@@ -118,8 +118,17 @@ module RactorRailsShim
|
|
|
118
118
|
# (Hash class→bool) built at prepare_for_ractors! time. Workers read
|
|
119
119
|
# the registry; main reads its live @abstract ivar (set by abstract!
|
|
120
120
|
# / inherited). `internal_methods` loops on abstract?.
|
|
121
|
+
#
|
|
122
|
+
# The registry is frozen with `Ractor.make_shareable` at install time so
|
|
123
|
+
# it can travel the shared app graph. Mutating a frozen Hash raises
|
|
124
|
+
# FrozenError, so `abstract!` only writes the live ivar (main) and
|
|
125
|
+
# guards the registry write behind a mutability check. `abstract!`
|
|
126
|
+
# after install in main is a no-op on the registry (already captured);
|
|
127
|
+
# callers that need to flip a class to abstract post-install should
|
|
128
|
+
# rebuild the registry (rare — abstract! is a boot-time declaration).
|
|
121
129
|
def abstract!
|
|
122
|
-
|
|
130
|
+
reg = RactorRailsShim._abstract_registry
|
|
131
|
+
reg[self] = true if reg && !reg.frozen? && Ractor.main?
|
|
123
132
|
@abstract = true if Ractor.main?
|
|
124
133
|
end
|
|
125
134
|
|
|
@@ -101,7 +101,7 @@ module RactorRailsShim
|
|
|
101
101
|
req.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
102
102
|
def parameter_parsers
|
|
103
103
|
v = ActiveSupport::IsolatedExecutionState[#{pp_key_str}]
|
|
104
|
-
return v
|
|
104
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{pp_key_str})
|
|
105
105
|
if Ractor.main? && instance_variable_defined?(:@parameter_parsers)
|
|
106
106
|
v = @parameter_parsers
|
|
107
107
|
ActiveSupport::IsolatedExecutionState[#{pp_key_str}] = v
|
|
@@ -85,7 +85,7 @@ module RactorRailsShim
|
|
|
85
85
|
lc.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
86
86
|
def registered_details
|
|
87
87
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
88
|
-
return v
|
|
88
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
89
89
|
if Ractor.main? && instance_variable_defined?(:@registered_details)
|
|
90
90
|
@registered_details
|
|
91
91
|
else
|
|
@@ -257,7 +257,7 @@ module RactorRailsShim
|
|
|
257
257
|
dk.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
258
258
|
def view_context_class
|
|
259
259
|
v = ActiveSupport::IsolatedExecutionState[#{vcc_key_str2}]
|
|
260
|
-
return v
|
|
260
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{vcc_key_str2})
|
|
261
261
|
if Ractor.main? && instance_variable_defined?(:@view_context_class)
|
|
262
262
|
v = @view_context_class
|
|
263
263
|
ActiveSupport::IsolatedExecutionState[#{vcc_key_str2}] = v
|
|
@@ -64,6 +64,23 @@ module RactorRailsShim
|
|
|
64
64
|
types
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
# `reset_default_attributes!` (called by `reload_schema_from_cache` via
|
|
69
|
+
# the Attributes → Timestamp → ModelSchema super chain) writes
|
|
70
|
+
# `@default_attributes = nil` and `@attribute_types = nil` class ivars.
|
|
71
|
+
# In a worker Ractor those writes raise Ractor::IsolationError. The
|
|
72
|
+
# shim routes the *readers* above through IES, so in a worker we clear
|
|
73
|
+
# the IES slots instead (the next read rebuilds lazily). In main we keep
|
|
74
|
+
# the original class-ivar-clearing behavior.
|
|
75
|
+
def reset_default_attributes!
|
|
76
|
+
if Ractor.main?
|
|
77
|
+
@default_attributes = nil
|
|
78
|
+
@attribute_types = nil
|
|
79
|
+
else
|
|
80
|
+
ActiveSupport::IsolatedExecutionState.delete(:"rrs_default_attributes_#{object_id}")
|
|
81
|
+
ActiveSupport::IsolatedExecutionState.delete(:"rrs_attribute_types_#{object_id}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
67
84
|
end
|
|
68
85
|
end
|
|
69
86
|
|
|
@@ -98,7 +98,7 @@ module RactorRailsShim
|
|
|
98
98
|
# In a worker Ractor, class_attribute-backed `__callbacks`
|
|
99
99
|
# falls back to the empty default (see class_attribute.rb /
|
|
100
100
|
# make_shareable.rb), so controller `before_action` /
|
|
101
|
-
# `after_action` filters are normally
|
|
101
|
+
# `after_action` filters are normally SKIPPED. For
|
|
102
102
|
# `:process_action` we replay the captured SYMBOL filters
|
|
103
103
|
# (see make_shareable!#_capture_controller_callbacks!)
|
|
104
104
|
# so actions that depend on a before_action (e.g. `set_post`
|
|
@@ -186,7 +186,7 @@ module RactorRailsShim
|
|
|
186
186
|
notif.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
187
187
|
def notifier
|
|
188
188
|
v = ActiveSupport::IsolatedExecutionState[#{nkey_str}]
|
|
189
|
-
return v
|
|
189
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{nkey_str})
|
|
190
190
|
if Ractor.main? && instance_variable_defined?(:@notifier)
|
|
191
191
|
@notifier
|
|
192
192
|
else
|
|
@@ -212,7 +212,7 @@ module RactorRailsShim
|
|
|
212
212
|
def instance(locale = :en)
|
|
213
213
|
if locale == :en
|
|
214
214
|
v = ActiveSupport::IsolatedExecutionState[#{en_key_str}]
|
|
215
|
-
return v
|
|
215
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{en_key_str})
|
|
216
216
|
if Ractor.main?
|
|
217
217
|
existing = instance_variable_get(:@__en_instance__) if instance_variable_defined?(:@__en_instance__)
|
|
218
218
|
ActiveSupport::IsolatedExecutionState[#{en_key_str}] = existing
|
|
@@ -302,7 +302,7 @@ module RactorRailsShim
|
|
|
302
302
|
::ActiveSupport.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
303
303
|
def error_reporter
|
|
304
304
|
v = ActiveSupport::IsolatedExecutionState[#{er_key_str}]
|
|
305
|
-
return v
|
|
305
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{er_key_str})
|
|
306
306
|
if Ractor.main? && instance_variable_defined?(:@error_reporter)
|
|
307
307
|
@error_reporter
|
|
308
308
|
else
|
|
@@ -343,7 +343,7 @@ module RactorRailsShim
|
|
|
343
343
|
cfg.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
344
344
|
def default_locale
|
|
345
345
|
v = ActiveSupport::IsolatedExecutionState[#{dl_key_str}]
|
|
346
|
-
return v
|
|
346
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{dl_key_str})
|
|
347
347
|
if Ractor.main? && defined?(@@default_locale)
|
|
348
348
|
cv = @@default_locale
|
|
349
349
|
ActiveSupport::IsolatedExecutionState[#{dl_key_str}] = cv
|
|
@@ -360,7 +360,7 @@ module RactorRailsShim
|
|
|
360
360
|
end
|
|
361
361
|
def locale
|
|
362
362
|
v = ActiveSupport::IsolatedExecutionState[#{l_key_str}]
|
|
363
|
-
return v
|
|
363
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{l_key_str})
|
|
364
364
|
default_locale
|
|
365
365
|
end
|
|
366
366
|
def locale=(locale)
|
|
@@ -380,7 +380,7 @@ module RactorRailsShim
|
|
|
380
380
|
# set config.i18n.available_locales explicitly.
|
|
381
381
|
def available_locales
|
|
382
382
|
v = ActiveSupport::IsolatedExecutionState[#{av_key_str}]
|
|
383
|
-
return v
|
|
383
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{av_key_str})
|
|
384
384
|
if Ractor.main?
|
|
385
385
|
if defined?(@@available_locales) && (cv = @@available_locales)
|
|
386
386
|
ActiveSupport::IsolatedExecutionState[#{av_key_str}] = cv
|
|
@@ -404,7 +404,7 @@ module RactorRailsShim
|
|
|
404
404
|
end
|
|
405
405
|
def available_locales_set
|
|
406
406
|
v = ActiveSupport::IsolatedExecutionState[#{avs_key_str}]
|
|
407
|
-
return v
|
|
407
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{avs_key_str})
|
|
408
408
|
if Ractor.main? && defined?(@@available_locales_set) && (cv = @@available_locales_set)
|
|
409
409
|
ActiveSupport::IsolatedExecutionState[#{avs_key_str}] = cv
|
|
410
410
|
return cv
|
|
@@ -424,7 +424,7 @@ module RactorRailsShim
|
|
|
424
424
|
# documented I18n default).
|
|
425
425
|
def enforce_available_locales
|
|
426
426
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_enforce]
|
|
427
|
-
return v
|
|
427
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_enforce)
|
|
428
428
|
if Ractor.main? && defined?(@@enforce_available_locales)
|
|
429
429
|
cv = @@enforce_available_locales
|
|
430
430
|
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_enforce] = cv
|
|
@@ -469,7 +469,7 @@ module RactorRailsShim
|
|
|
469
469
|
# main; workers reload translations from disk via these paths.
|
|
470
470
|
def load_path
|
|
471
471
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_load_path]
|
|
472
|
-
return v
|
|
472
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_load_path)
|
|
473
473
|
if Ractor.main?
|
|
474
474
|
lp = (defined?(@@load_path) && @@load_path) || []
|
|
475
475
|
lp = lp.dup.freeze if lp.respond_to?(:freeze) && !lp.frozen?
|
|
@@ -493,7 +493,7 @@ module RactorRailsShim
|
|
|
493
493
|
# DEFAULT_INTERPOLATION_PATTERNS constant).
|
|
494
494
|
def default_separator
|
|
495
495
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_sep]
|
|
496
|
-
return v
|
|
496
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_sep)
|
|
497
497
|
if Ractor.main?
|
|
498
498
|
cv = defined?(@@default_separator) ? @@default_separator : "."
|
|
499
499
|
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_sep] = cv
|
|
@@ -509,7 +509,7 @@ module RactorRailsShim
|
|
|
509
509
|
end
|
|
510
510
|
def exception_handler
|
|
511
511
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_exc]
|
|
512
|
-
return v
|
|
512
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_exc)
|
|
513
513
|
if Ractor.main?
|
|
514
514
|
cv = defined?(@@exception_handler) ? @@exception_handler : ::I18n::ExceptionHandler.new
|
|
515
515
|
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_exc] = cv
|
|
@@ -525,7 +525,7 @@ module RactorRailsShim
|
|
|
525
525
|
end
|
|
526
526
|
def missing_interpolation_argument_handler
|
|
527
527
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_miss]
|
|
528
|
-
return v
|
|
528
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_miss)
|
|
529
529
|
if Ractor.main?
|
|
530
530
|
cv = defined?(@@missing_interpolation_argument_handler) ? @@missing_interpolation_argument_handler : lambda do |missing_key, provided_hash, string|
|
|
531
531
|
raise ::I18n::MissingInterpolationArgument.new(missing_key, provided_hash, string)
|
|
@@ -545,7 +545,7 @@ module RactorRailsShim
|
|
|
545
545
|
end
|
|
546
546
|
def interpolation_patterns
|
|
547
547
|
v = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_ip]
|
|
548
|
-
return v
|
|
548
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(:ractor_rails_shim_i18n_ip)
|
|
549
549
|
if Ractor.main?
|
|
550
550
|
cv = defined?(@@interpolation_patterns) ? @@interpolation_patterns : ::I18n::DEFAULT_INTERPOLATION_PATTERNS.dup
|
|
551
551
|
ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_i18n_ip] = cv
|
|
@@ -599,7 +599,7 @@ module RactorRailsShim
|
|
|
599
599
|
i18n.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
600
600
|
def fallbacks
|
|
601
601
|
v = ActiveSupport::IsolatedExecutionState[#{fb_key_str}]
|
|
602
|
-
return v
|
|
602
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{fb_key_str})
|
|
603
603
|
if Ractor.main? && defined?(@@fallbacks)
|
|
604
604
|
cv = @@fallbacks
|
|
605
605
|
if cv
|
|
@@ -622,7 +622,7 @@ module RactorRailsShim
|
|
|
622
622
|
tag.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
623
623
|
def implementation
|
|
624
624
|
v = ActiveSupport::IsolatedExecutionState[#{tag_key_str}]
|
|
625
|
-
return v
|
|
625
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{tag_key_str})
|
|
626
626
|
if Ractor.main? && defined?(@@implementation)
|
|
627
627
|
cv = @@implementation
|
|
628
628
|
ActiveSupport::IsolatedExecutionState[#{tag_key_str}] = cv
|
|
@@ -781,7 +781,7 @@ module RactorRailsShim
|
|
|
781
781
|
ec.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
782
782
|
def after_change_callbacks
|
|
783
783
|
v = ActiveSupport::IsolatedExecutionState[#{acb_key_str}]
|
|
784
|
-
return v
|
|
784
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{acb_key_str})
|
|
785
785
|
if Ractor.main? && instance_variable_defined?(:@after_change_callbacks)
|
|
786
786
|
v = @after_change_callbacks
|
|
787
787
|
ActiveSupport::IsolatedExecutionState[#{acb_key_str}] = v
|
|
@@ -797,7 +797,7 @@ module RactorRailsShim
|
|
|
797
797
|
end
|
|
798
798
|
def nestable
|
|
799
799
|
v = ActiveSupport::IsolatedExecutionState[#{nest_key_str}]
|
|
800
|
-
return v
|
|
800
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{nest_key_str})
|
|
801
801
|
if Ractor.main? && instance_variable_defined?(:@nestable)
|
|
802
802
|
v = @nestable
|
|
803
803
|
ActiveSupport::IsolatedExecutionState[#{nest_key_str}] = v
|
|
@@ -855,7 +855,7 @@ module RactorRailsShim
|
|
|
855
855
|
ls.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
856
856
|
def logger
|
|
857
857
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
858
|
-
return v
|
|
858
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
859
859
|
if Ractor.main? && instance_variable_defined?(:@logger)
|
|
860
860
|
@logger
|
|
861
861
|
elsif defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
@@ -890,7 +890,7 @@ module RactorRailsShim
|
|
|
890
890
|
rl.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
891
891
|
def check!
|
|
892
892
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
893
|
-
return v
|
|
893
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
894
894
|
result = check.call
|
|
895
895
|
ActiveSupport::IsolatedExecutionState[#{key_str}] = result
|
|
896
896
|
result
|