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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2f462e8a604fac024cc13b3f1667ec9e9c79207cb675a473e7f2f536e7ac4c8d
|
|
4
|
+
data.tar.gz: 76652b85a06f3c7e6cc50b7619f4b28c52da420f6f946f09518f01b5143c4094
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 83964fbfa3a113e70ca8bdfd5f6f69ea3598cf028207bb43971a9b6ed135e28f4b2f2a5b1d0bbcdf574a9a12313ff9ab6664182d22b8a3e00ab959105bdb72ce
|
|
7
|
+
data.tar.gz: be2ef63949ea84ac7f3dfa72a65d57f2687b2b1dc53de6f85a8709d42b84fcec5588f429e298beb8aa76e682b593ebfeb49dd292191cc620bd847b9b84e78c78
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ractor-rails-shim are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added — ActiveRecord query-path ractor-safety (Blocker 1 deep work)
|
|
11
|
+
- `RactorRailsShim.worker_ar_init(app)` — a shareable Rack middleware that
|
|
12
|
+
calls `init_worker_ar_connections!` on each worker's first request. Kino's
|
|
13
|
+
`:ractor` mode has no worker-init hook, so `config_ractor.ru` (generated by
|
|
14
|
+
`make_full_test_app.sh`) now wraps the shareable app with it.
|
|
15
|
+
- `_share_relation_delegate_caches!` — deep-freezes each AR model class's
|
|
16
|
+
`@relation_delegate_cache` (mutable Hash of shareable delegate Classes) so a
|
|
17
|
+
worker Ractor can read it. (`patches/activerecord.rb`)
|
|
18
|
+
- `_share_model_classes!` — warms every AR model class in main (runs
|
|
19
|
+
`count`/`first`/`page`/`table_name`/...) to populate lazy `@ivar ||= ...`
|
|
20
|
+
class ivars, then makes each shareable (deep-freeze; Monitor/Mutex→
|
|
21
|
+
`NoOpLock`; `Concurrent::Map`→frozen Hash; unfreezable caches→frozen empty
|
|
22
|
+
container). Fixes `@table_name`, `@arel_table`, `@predicate_builder`, etc.
|
|
23
|
+
- `_share_active_record_internals!` — warms `.empty` + freezes class ivars on
|
|
24
|
+
the AR `*Clause` helper classes (`WhereClause`, `FromClause`).
|
|
25
|
+
- `_install_activerecord_configurations_patch` — routes the raw
|
|
26
|
+
`@@configurations` class var (`ActiveRecord::Base.configurations`) through
|
|
27
|
+
IES with a shareable deep-frozen `DatabaseConfigurations` fallback.
|
|
28
|
+
- ~30 non-shareable AR/Arel constants registered in `SHAREABLE_CONSTANTS`
|
|
29
|
+
(`VALID_UNSCOPING_VALUES`, `MULTI_VALUE_METHODS`, `STRING_OR_SYMBOL_CLASS`,
|
|
30
|
+
`NATIVE_DATABASE_TYPES`, ...). `make_constant_shareable` now special-cases
|
|
31
|
+
Monitor/Mutex constants → `NoOpLock` and rescues intrinsically-unshareable
|
|
32
|
+
values (Proc / `Concurrent::Map` / `TypeMap`) instead of crashing.
|
|
33
|
+
- `_capture_ar_configurations!` now reads
|
|
34
|
+
`Rails.application.config.database_configuration` (`DatabaseConfigurations`
|
|
35
|
+
has no `#each` in this Rails/Ruby).
|
|
36
|
+
- `verify_blockers.rb` (generated by `make_full_test_app.sh`) — end-to-end
|
|
37
|
+
data-layer + HTTP-dispatch check in a worker Ractor.
|
|
38
|
+
|
|
39
|
+
### Known limitations (DB queries from worker Ractors)
|
|
40
|
+
- `Post.count` / `Post.page(1)` from a worker Ractor still fail at
|
|
41
|
+
`ActiveRecord::DatabaseConfigurations.db_config_handlers` (an Array of
|
|
42
|
+
**Procs** registered by adapters), and downstream at the SQLite
|
|
43
|
+
`TYPE_MAP` (Procs) and `QUOTED_*` `Concurrent::Map` quoting caches.
|
|
44
|
+
These are intrinsically unshareable and require upstream Rails changes
|
|
45
|
+
(shareable callables instead of Procs; per-Ractor quoting caches). See
|
|
46
|
+
`NEXT_STEPS.md` "The wall — deep AR ractor-unsafety".
|
|
47
|
+
|
|
48
|
+
## [0.2.0] - 2026-07-09
|
|
49
|
+
|
|
50
|
+
### Added — productionization (Phase 6)
|
|
51
|
+
- **Version detection infrastructure.** New `RactorRailsShim::Version` module
|
|
52
|
+
with `Gem::Version`-based runtime checks (`ruby`, `rails`, `rails_segment`,
|
|
53
|
+
`supported_ruby?`, `supported_rails?`, `satisfies?`). Replaces the string-
|
|
54
|
+
prefix compare so pre-release and patch versions sort correctly.
|
|
55
|
+
(`lib/ractor_rails_shim/version_check.rb`)
|
|
56
|
+
- **Version policy switch.** `RactorRailsShim.version_policy = :warn | :strict |
|
|
57
|
+
:off`. The default `:warn` preserves backward compatibility; `:strict`
|
|
58
|
+
raises `RactorRailsShim::UnsupportedVersionError` on untested Rails/Ruby;
|
|
59
|
+
`:off` silences. (`patches.rb`)
|
|
60
|
+
- **Patch version registry.** Every `install_*` / `_install_*` method registers
|
|
61
|
+
its tested Rails versions in `RactorRailsShim::PATCH_VERSIONS`. Use
|
|
62
|
+
`RactorRailsShim.applicable_patches` to see which patches applied to the
|
|
63
|
+
runtime (and which were skipped as untested). This is the "load different
|
|
64
|
+
patches for different Rails versions" extension point — to add 7.x support,
|
|
65
|
+
write version-specific variants and tag them in the registry.
|
|
66
|
+
(`patches.rb`)
|
|
67
|
+
- **CI.** GitHub Actions workflow (`.github/workflows/ci.yml`): a fast unit
|
|
68
|
+
job (no Rails) plus an integration job that builds the minimal Rails 8.1
|
|
69
|
+
test app, makes it shareable, and dispatches `GET /up` in a worker Ractor —
|
|
70
|
+
asserting HTTP 200. Also runs `ractor-rails-check` against the test app.
|
|
71
|
+
- **Unit specs** for the version infrastructure and the callable/lock
|
|
72
|
+
replacement classes (`NoOpProc`, `Callable`, `CallableConst`,
|
|
73
|
+
`RequestCallable`, `NoOpLock`, `NoOpLogDev`) — including cross-Ractor
|
|
74
|
+
callability. (`spec/version_spec.rb`; 31 specs total, up from 8.)
|
|
75
|
+
- **CHANGELOG.md.**
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
- `ractor-rails-shim.gemspec`: real metadata (`changelog_uri`,
|
|
79
|
+
`bug_tracker_uri`, `rubygems_mfa_required`), canonical repo URL, CHANGELOG
|
|
80
|
+
included in the gem package.
|
|
81
|
+
- `script/make_test_app.sh`: portable `sed` (was macOS-only `sed -i ''`,
|
|
82
|
+
failed on Linux CI).
|
|
83
|
+
- Bumped version `0.1.0` → `0.2.0`.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- `.gitignore`: corrected `racker-rails-shim-*.gem` typo to
|
|
87
|
+
`ractor-rails-shim-*.gem`.
|
|
88
|
+
|
|
89
|
+
## [0.1.0] - 2026-07-09
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
- Initial proof-of-concept. Reroutes Rails class-level instance variables
|
|
93
|
+
(`Rails.application`, `Rails.cache`, `Rails.logger`, `mattr_accessor`,
|
|
94
|
+
`class_attribute`, `Zeitwerk::Registry`, unshareable constants) through
|
|
95
|
+
`IsolatedExecutionState` / `Ractor.make_shareable`.
|
|
96
|
+
- `make_app_shareable!` — replaces self-capturing Procs with callable objects,
|
|
97
|
+
Mutex/Monitor with no-op locks, `Concurrent::Map` with frozen Hashes, then
|
|
98
|
+
`Ractor.make_shareable(app)`. A worker Ractor dispatches `GET /up` → 200.
|
|
99
|
+
- `ractor-rails-check` CLI audit tool.
|
|
100
|
+
- Minimal unit specs (8) + an integration spec (self-skips without a test app).
|
|
101
|
+
- `VALIDATION.md`, `README.md`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# ractor-rails-shim
|
|
2
|
+
|
|
3
|
+
A monkey-patch shim that reroutes Rails' class-level instance variable accessors through `ActiveSupport::IsolatedExecutionState`, which is Ractor-safe. Lets a Rails app run in Ractor mode without forking Rails itself.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
**Current status:** on a full Rails 8.1 app (Devise 5, Propshaft, Kaminari,
|
|
8
|
+
PG) under Ruby 4.0.5, worker Ractors serve **every** routable action —
|
|
9
|
+
`GET /up`, full ERB view rendering, Devise sign-in/sign-out (CSRF issuance
|
|
10
|
+
**and** validation), and authenticated Devise **writes** (`POST /posts` → 302,
|
|
11
|
+
row persisted). A worker Ractor dispatches `GET /up` → **HTTP 200** via
|
|
12
|
+
`RactorRailsShim.make_app_shareable!`. The shim builds a shareable fallback
|
|
13
|
+
table for framework class config (`class_attribute` / `mattr_accessor` values)
|
|
14
|
+
and patches the raw class-ivar accessors Rails reads per-request
|
|
15
|
+
(`ExecutionWrapper.active_key`, `Notifications.notifier`, `Inflections`,
|
|
16
|
+
`PathRegistry`, `I18n`, `AbstractController` lazy ivars, `Rack::Request`/`Utils`,
|
|
17
|
+
`ExecutionContext`, etc.).
|
|
18
|
+
|
|
19
|
+
**Known limitation:** sustained *concurrent* writes in a worker Ractor can still
|
|
20
|
+
crash with a frozen-iseq SIGBUS (a Ruby 4.0 Ractor-model issue); reads and
|
|
21
|
+
single writes are stable.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **Ruby >= 4.0** — the shim relies on Ruby 4.0's Ractor semantics and
|
|
26
|
+
`Ractor.make_shareable`. It will not work (and refuses to install) on
|
|
27
|
+
earlier Ruby versions.
|
|
28
|
+
- **Rails ~> 8.1** — tested against Rails 8.1.x class layouts. Other versions
|
|
29
|
+
are not yet supported (see Version compatibility below).
|
|
30
|
+
|
|
31
|
+
**Ractor-mode server:** as of this writing, the only Ruby web server that can
|
|
32
|
+
run a Rails app in Ractor mode is
|
|
33
|
+
[kino](https://github.com/yaroslav/kino) (`kino -m ractor`). Other servers
|
|
34
|
+
(Puma, Falcon, …) run Rails in processes or threads, not Ractors. This shim was
|
|
35
|
+
developed and tested against a real Rails 8.1 app **served by kino**; that is
|
|
36
|
+
the configuration it is verified against.
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
Rails stores global state in class-level instance variables:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Rails
|
|
44
|
+
class << self
|
|
45
|
+
attr_accessor :app_class, :cache, :logger
|
|
46
|
+
def application; @application ||= ...; end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
From a non-main Ractor, these reads/writes raise `Ractor::IsolationError`:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
can not get unshareable values from instance variables of classes/modules
|
|
55
|
+
from non-main Ractors
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is the primary blocker preventing Rails from running in Ractor mode (see the [kino](https://github.com/yaroslav/kino) project's `doc/rails-on-ractors.md` for the full diagnosis). The same pattern blocks Hanami, Padrino, Sinatra, and most Ruby web frameworks.
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
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.5. Rails already uses this primitive for `ActiveRecord::ConnectionHandling.connection_handler` (for thread safety), so the pattern is proven in production.
|
|
63
|
+
|
|
64
|
+
Two storage paths, depending on the state's shape:
|
|
65
|
+
|
|
66
|
+
| path | primitive | sharing | mutability | use case |
|
|
67
|
+
|---|---|---|---|---|
|
|
68
|
+
| **per-Ractor** | `IsolatedExecutionState` / `Ractor.store_if_absent` | none (each Ractor own copy) | mutable | connection pools, per-Ractor config, logger |
|
|
69
|
+
| **shareable** | `Ractor.make_shareable` + constant | one copy, by reference | frozen (read-only) | route tables, frozen config, templates |
|
|
70
|
+
|
|
71
|
+
The shim uses the per-Ractor path by default. For shareable state, use `Ractor.make_shareable` directly — that's not this gem's job.
|
|
72
|
+
|
|
73
|
+
**Load order.** `install` may be called either before or after Rails is defined — the normal `config/boot.rb` path calls it before `require "rails"`. The `mattr_accessor` macro patch applies regardless; the Rails-module accessor patch (`Rails.application`, `Rails.env`, ...) defers via a `TracePoint(:class)` load hook that fires when `module Rails` opens.
|
|
74
|
+
|
|
75
|
+
## Version compatibility
|
|
76
|
+
|
|
77
|
+
The shim targets specific Rails class layouts (8.1.x) and Ruby Ractor semantics
|
|
78
|
+
(4.0.x). At install it runs a real `Gem::Version`-based check (not a string
|
|
79
|
+
compare) and applies a configurable policy on mismatch:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
RactorRailsShim.version_policy = :strict # raise on untested versions
|
|
83
|
+
RactorRailsShim.version_policy = :warn # default: warn to $stderr, proceed
|
|
84
|
+
RactorRailsShim.version_policy = :off # silent (experimentation)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Under `:strict` a mismatch raises `RactorRailsShim::UnsupportedVersionError`.
|
|
88
|
+
Each patch registers its tested Rails versions in `RactorRailsShim::PATCH_VERSIONS`;
|
|
89
|
+
query what applied to your runtime with:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
RactorRailsShim.applicable_patches
|
|
93
|
+
# => { applied: [:mattr_accessor, :rails_module, ...], skipped: [{name: ...}] }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Adding Rails 7.x support: the version-gated registry is the extension point —
|
|
97
|
+
write version-specific patch variants, tag them in `PATCH_VERSIONS`, and the
|
|
98
|
+
dispatcher applies only matching patches. (7.x is not yet supported; only 8.1
|
|
99
|
+
is tested today.)
|
|
100
|
+
|
|
101
|
+
## Install
|
|
102
|
+
|
|
103
|
+
Add to your Gemfile:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
gem "ractor-rails-shim", group: :production
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then install early in boot, before `Rails.application` is first accessed:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# config/boot.rb
|
|
113
|
+
require "bundler/setup"
|
|
114
|
+
require "ractor_rails_shim"
|
|
115
|
+
RactorRailsShim.install
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
After Rails is fully booted (after `Rails.application.initialize!`) and **before
|
|
119
|
+
spawning worker Ractors**, call `prepare_for_ractors!` to make the remaining
|
|
120
|
+
unshareable constants (e.g. `Rails::Railtie::ABSTRACT_RAILTIES`, which loads
|
|
121
|
+
after `module Rails` opens) shareable:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# config/environment.rb, or wherever you boot your app before spawning workers
|
|
125
|
+
Rails.application.initialize!
|
|
126
|
+
RactorRailsShim.prepare_for_ractors!
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
To share the whole app across worker Ractors (the `:ractor` mode path), call
|
|
130
|
+
`make_app_shareable!` — it replaces every self-capturing Proc in the app graph
|
|
131
|
+
with a callable object, every Mutex/Monitor with a no-op lock, and every
|
|
132
|
+
`Concurrent::Map` with a frozen Hash, then calls `Ractor.make_shareable`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
Rails.application.initialize!
|
|
136
|
+
app = RactorRailsShim.make_app_shareable!(Rails.application)
|
|
137
|
+
# app is now frozen and Ractor.shareable? — pass it to worker Ractors:
|
|
138
|
+
worker = Ractor.new(app) { |a| a.call(env) }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Note:** `make_app_shareable!` is production-only (the app becomes read-only).
|
|
142
|
+
It also **detaches the logger IO from the app graph**: `app.config.logger`
|
|
143
|
+
(the broadcast target holding the real `$stdout`/`$stderr` IO) is swapped for a
|
|
144
|
+
frozen no-op `BroadcastLogger` so `Ractor.make_shareable(app)` doesn't freeze
|
|
145
|
+
the process's real IOs, and the main ractor's `Rails.logger` (the per-Ractor
|
|
146
|
+
module accessor, not in the app graph) is re-pointed at a fresh live
|
|
147
|
+
`BroadcastLogger` → `$stderr` so main keeps logging. Worker ractors build their
|
|
148
|
+
own per-Ractor `Rails.logger` (a `BroadcastLogger` → `$stderr`). To fix
|
|
149
|
+
unshareable constants in your own app/gems, add them to the registry before
|
|
150
|
+
`prepare_for_ractors!`:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
RactorRailsShim.shareable_constants << "MyGem::MUTABLE_LIST"
|
|
154
|
+
RactorRailsShim.prepare_for_ractors!
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or from a Rails console / runner for a quick check:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "ractor_rails_shim"
|
|
161
|
+
RactorRailsShim.install
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Audit your app
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
bundle exec ractor-rails-check
|
|
168
|
+
# or scoped:
|
|
169
|
+
bundle exec ractor-rails-check --rails # only Rails framework modules
|
|
170
|
+
bundle exec ractor-rails-check --app # only app + gems
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Reports class ivars holding unshareable values — the ones that would raise `Ractor::IsolationError` from a worker Ractor. Example:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
ractor-rails-shim check: 23 class-ivar blocker(s) found
|
|
177
|
+
|
|
178
|
+
=== Rails framework (8) ===
|
|
179
|
+
Rails@application = nil
|
|
180
|
+
Rails@app_class = String
|
|
181
|
+
Rails@cache = NilClass
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
=== app + gems (15) ===
|
|
185
|
+
Devise@config = Devise::Config
|
|
186
|
+
Sidekiq@options = Hash
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
hints:
|
|
190
|
+
- require "ractor_rails_shim" and call RactorRailsShim.install before
|
|
191
|
+
Rails.application is first accessed (early in config/boot.rb)
|
|
192
|
+
- class-var (@@foo) blockers from mattr_accessor/cattr_accessor are rerouted
|
|
193
|
+
by the shim automatically once installed
|
|
194
|
+
- raw class-ivar (@foo) blockers are NOT fixed by the shim; patch the gem
|
|
195
|
+
or use Ractor.make_shareable + a constant for shareable state
|
|
196
|
+
- for per-Ractor mutable state use Ractor.store_if_absent(key) { default }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## What this fixes
|
|
200
|
+
|
|
201
|
+
- `Rails.application`, `Rails.app_class`, `Rails.cache`, `Rails.logger`, `Rails.env`, `Rails.backtrace_cleaner` — rerouted through `IsolatedExecutionState`.
|
|
202
|
+
- `Module.mattr_accessor` / `cattr_accessor` — the macro is rewritten so all ~150 call sites in Rails inherit the fix without individual edits. Pass `shareable: true` to opt an accessor into the shareable-by-reference path instead.
|
|
203
|
+
- `Class.class_attribute` (ActiveSupport) — the macro is rewritten so `executor`, `check`, and every other `class_attribute`-defined accessor routes through `IsolatedExecutionState` via string-eval'd methods (no captured binding). Without this, a worker Ractor calling `app.reloader.executor = ...` during boot raises "defined with an un-shareable Proc in a different Ractor".
|
|
204
|
+
- `Zeitwerk::Registry` class ivars (`@loaders`, `@mutex`, `@autoloads`, etc.) — routed through `IsolatedExecutionState` so a worker Ractor can create autoloaders (each Ractor gets its own registry).
|
|
205
|
+
- Unshareable constants (`Rails::Railtie::ABSTRACT_RAILTIES`, `ActiveSupport::EnvironmentInquirer::DEFAULT_ENVIRONMENTS`, etc.) — made shareable once at boot via `Ractor.make_shareable` + `const_set`. Add your own via `RactorRailsShim.shareable_constants << "MyGem::CONST"` then call `prepare_for_ractors!`.
|
|
206
|
+
- **Framework class config fallback** (`SHAREABLE_FALLBACK`) — at `make_app_shareable!` time the main ractor's live `class_attribute` / `mattr_accessor` values are captured, made shareable (callable-replacement for any Procs), and exposed as a read-only fallback. Worker readers return it when their per-Ractor IES slot is empty — this is what fixes `ActionController::Base.config` being nil in workers.
|
|
207
|
+
- **Raw class-ivar/cvar accessors Rails reads per-request** (the long tail beyond `mattr_accessor`) — patched individually: `ExecutionWrapper.active_key`, `ActiveSupport::Notifications.notifier`, `ActiveSupport.error_reporter`, `ActiveSupport::ExecutionContext` (after_change_callbacks / nestable), `ActiveSupport::Inflector::Inflections`, `ActionView::PathRegistry`, `ActionView::LookupContext` + `DetailsKey` (`view_context_class` built per-controller in main & shared via `VIEW_CONTEXT_REGISTRY`), `ActionView::Template::Handlers`, `AbstractController::Base` (`controller_path` / `action_methods` / `abstract` / `_prefixes` via per-Ractor Hash caches keyed by class), `AbstractController::UrlFor#action_methods` (sidesteps the unshareable `_routes` define_method-block), `ActionController::ParameterEncoding#action_encoding_template`, `Rack::Request` (`forwarded_priority` / `x_forwarded_proto_priority`), `Rack::Utils` (`default_query_parser` / multipart limits), `ActionDispatch::Request.parameter_parsers`, `I18n::Config` (`default_locale` / `locale`) + `I18n.fallbacks` + `I18n::Locale::Tag.implementation`.
|
|
208
|
+
- **`ActiveSupport::Callbacks#run_callbacks`** — made nil-safe so a worker Ractor whose `__callbacks` couldn't be shared (frozen, self-capturing-Proc callback chains) treats callbacks as empty. Correct for a read-only shared app where boot-time callbacks already ran in main.
|
|
209
|
+
|
|
210
|
+
## What this does NOT fix
|
|
211
|
+
|
|
212
|
+
- **Gems.** Every gem your app depends on (Devise, Sidekiq, redis-rb, pg, etc.) has its own class ivars. The shim's `mattr_accessor` rewrite helps gems that use that macro, but gems using raw `@ivar ||= ...` need their own patches. The `--check` script surfaces these.
|
|
213
|
+
- **App code** holding mutable state in closures (`cache = {}; ->(env){ cache[...] }`). The shim can't see into closures; `--check` finds class ivars only.
|
|
214
|
+
- **define_method-block methods.** A handful of Rails methods are defined via `define_method` with a block capturing the defining Ractor (e.g. `ActionDispatch::Routing::RouteSet`'s `_routes` singleton helper, `ActionView::Base.with_empty_template_cache`'s `compiled_method_container`). The shim works around these by building the affected classes (e.g. `view_context_class`) in the main Ractor and sharing them, or by reading the route set directly from the shared `Rails.application`. More complex apps may hit additional `define_method`-block call sites that need similar treatment.
|
|
215
|
+
|
|
216
|
+
## Limitations
|
|
217
|
+
|
|
218
|
+
- **Per-Ractor means N copies.** Each Ractor gets its own `Rails.application`, `Rails.cache`, etc. — same shape as forking N processes, but cheaper (no heap duplication). For large read-only state, use `Ractor.make_shareable` instead.
|
|
219
|
+
- **The mattr_accessor rewrite is broad.** It reroutes *all* `mattr_accessor`-defined accessors through `IsolatedExecutionState`, including ones that were legitimately class-global. This may change semantics for code that sets a value in main Ractor expecting worker Ractors to see it. Audit with `--check` and use `shareable: true` for accessors that should be shared.
|
|
220
|
+
- **Fragile across Rails versions.** This is a monkey-patch. Rails releases that touch `rails.rb` or `mattr_accessor` may break it. When upstream fixes it, delete the gem.
|
|
221
|
+
|
|
222
|
+
## When to delete this gem
|
|
223
|
+
|
|
224
|
+
This shim is a **stopgap**. Delete it (remove from the Gemfile) when Rails
|
|
225
|
+
natively supports Ractor mode — i.e. when **all** of these land upstream:
|
|
226
|
+
|
|
227
|
+
1. Class-level instance variables / class variables backing `mattr_accessor`
|
|
228
|
+
and `class_attribute` are migrated to `IsolatedExecutionState` (or
|
|
229
|
+
equivalent ractor-safe storage). Rails already does this for
|
|
230
|
+
`ActiveRecord::ConnectionHandling.connection_handler`, proving the pattern.
|
|
231
|
+
2. The `Zeitwerk::Registry` class ivars route through ractor-safe storage.
|
|
232
|
+
3. Unshareable constants (`EnvironmentInquirer::DEFAULT_ENVIRONMENTS`, etc.)
|
|
233
|
+
are made shareable (deep-frozen) at boot.
|
|
234
|
+
4. The **7 self-capturing Procs** in the app graph are restructured to not
|
|
235
|
+
capture `self` — e.g. the `Rack::Files` head lambda, `ActionDispatch::SSL`
|
|
236
|
+
exclude proc, `CookieStore` same-site proc, the message-verifier secret
|
|
237
|
+
generator, and the routes-reloader blocks. These block
|
|
238
|
+
`Ractor.make_shareable(Rails.application)` today.
|
|
239
|
+
5. Initializer blocks (`Rails::Initializable::Initializer#block`) are
|
|
240
|
+
shareable (defined as methods, not closures) so per-Ractor boot works.
|
|
241
|
+
|
|
242
|
+
Until then, the shim is required. A simple canary: with the gem removed,
|
|
243
|
+
`Ractor.make_shareable(Rails.application)` fails (it raises on a Mutex or a
|
|
244
|
+
"Proc's self is not shareable"). When that call succeeds unshimmed, the gem
|
|
245
|
+
is obsolete. See `UPSTREAM_ISSUE.md` for the full blocker map and the
|
|
246
|
+
proposed incremental upstream merge plan.
|
|
247
|
+
|
|
248
|
+
## Publishing (maintainers)
|
|
249
|
+
|
|
250
|
+
The gemspec is publish-ready (metadata, MFA required, CHANGELOG packaged).
|
|
251
|
+
To release a new version:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
# Bump lib/ractor_rails_shim/version.rb and add a CHANGELOG entry, then:
|
|
255
|
+
gem build ractor-rails-shim.gemspec
|
|
256
|
+
gem push ractor-rails-shim-<version>.gem
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
CI (`.github/workflows/ci.yml`) gates merges: unit specs (no Rails) + an
|
|
260
|
+
integration job that builds the minimal Rails 8.1 app and dispatches `GET /up`
|
|
261
|
+
in a worker Ractor. Don't publish from a red build.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ractor-rails-check — audit a loaded Rails app (or any Ruby app) for
|
|
5
|
+
# class-level instance variables that would block Ractor mode.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ractor-rails-check # scan loaded modules, print report
|
|
9
|
+
# ractor-rails-check --rails # only Rails framework modules
|
|
10
|
+
# ractor-rails-check --app # only app + gems (exclude Rails)
|
|
11
|
+
#
|
|
12
|
+
# Typically run from a Rails console or a boot script:
|
|
13
|
+
# bundle exec rails runner ractor-rails-check
|
|
14
|
+
|
|
15
|
+
require "ractor_rails_shim/check"
|
|
16
|
+
|
|
17
|
+
scope = ARGV.find { |a| a.start_with?("--") }&.sub(/^--/, "")&.to_sym || :all
|
|
18
|
+
|
|
19
|
+
case scope
|
|
20
|
+
when :rails
|
|
21
|
+
findings = RactorRailsShim::Check.scan_rails
|
|
22
|
+
when :app
|
|
23
|
+
findings = RactorRailsShim::Check.scan_app
|
|
24
|
+
else
|
|
25
|
+
RactorRailsShim::Check.report(print: true)
|
|
26
|
+
exit 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Scoped report
|
|
30
|
+
if findings.empty?
|
|
31
|
+
puts "no blockers found in scope :#{scope}"
|
|
32
|
+
exit 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts "=== scope :#{scope} (#{findings.size} blockers) ==="
|
|
36
|
+
findings.each do |f|
|
|
37
|
+
puts " #{f.owner}#{f.ivar} = #{f.value_class}"
|
|
38
|
+
puts " #{f.source}" if f.source
|
|
39
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
|
|
5
|
+
module RactorRailsShim
|
|
6
|
+
# Audit an app for Ractor blockers. Inspects loaded classes/modules for
|
|
7
|
+
# class-level instance variables and mattr_accessor usage that would
|
|
8
|
+
# raise Ractor::IsolationError from a non-main Ractor.
|
|
9
|
+
#
|
|
10
|
+
# Modeled on Kino's `kino --check` — tell the user *exactly* what blocks
|
|
11
|
+
# their app, instead of leaving them to decode IsolationError at runtime.
|
|
12
|
+
class Check
|
|
13
|
+
# kind: :ivar (class-level instance variable, @foo) or
|
|
14
|
+
# :cvar (class variable, @@foo — mattr_accessor/cattr_accessor)
|
|
15
|
+
Finding = Data.define(:owner, :ivar, :value_class, :shareable, :source, :kind)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Scan all loaded classes/modules and report class ivars AND class
|
|
19
|
+
# variables holding unshareable values. Returns an array of Finding.
|
|
20
|
+
def scan
|
|
21
|
+
findings = []
|
|
22
|
+
seen = {}
|
|
23
|
+
|
|
24
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
25
|
+
next if mod.name.nil? || mod.name.empty?
|
|
26
|
+
next if mod.name.start_with?("#", "Ractor::", "Thread::", "Fiber::", "ObjectSpace::")
|
|
27
|
+
|
|
28
|
+
# Inspect class-level instance variables.
|
|
29
|
+
mod.instance_variables.each do |ivar|
|
|
30
|
+
next if ivar == :@_sandbox # Rails internal, ignored
|
|
31
|
+
begin
|
|
32
|
+
val = mod.instance_variable_get(ivar)
|
|
33
|
+
rescue => e
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
shareable = begin
|
|
38
|
+
Ractor.shareable?(val)
|
|
39
|
+
rescue => e
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
next if shareable
|
|
43
|
+
|
|
44
|
+
key = "#{mod.name}#{ivar}"
|
|
45
|
+
next if seen[key]
|
|
46
|
+
seen[key] = true
|
|
47
|
+
|
|
48
|
+
source = locate(mod, ivar)
|
|
49
|
+
findings << Finding.new(
|
|
50
|
+
owner: mod.name,
|
|
51
|
+
ivar: ivar.to_s,
|
|
52
|
+
value_class: val.class.name || val.class.to_s,
|
|
53
|
+
shareable: shareable,
|
|
54
|
+
source: source,
|
|
55
|
+
kind: :ivar
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Inspect class variables (@@foo) — these back mattr_accessor /
|
|
60
|
+
# cattr_accessor and are ALSO subject to Ractor::IsolationError
|
|
61
|
+
# from non-main Ractors (verified on Ruby 4.0.5).
|
|
62
|
+
begin
|
|
63
|
+
mod.class_variables.each do |cvar|
|
|
64
|
+
begin
|
|
65
|
+
val = mod.class_variable_get(cvar)
|
|
66
|
+
rescue => e
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
shareable = begin
|
|
71
|
+
Ractor.shareable?(val)
|
|
72
|
+
rescue => e
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
next if shareable
|
|
76
|
+
|
|
77
|
+
key = "#{mod.name}#{cvar}"
|
|
78
|
+
next if seen[key]
|
|
79
|
+
seen[key] = true
|
|
80
|
+
|
|
81
|
+
source = locate(mod, cvar)
|
|
82
|
+
findings << Finding.new(
|
|
83
|
+
owner: mod.name,
|
|
84
|
+
ivar: cvar.to_s,
|
|
85
|
+
value_class: val.class.name || val.class.to_s,
|
|
86
|
+
shareable: shareable,
|
|
87
|
+
source: source,
|
|
88
|
+
kind: :cvar
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
rescue => e
|
|
92
|
+
# Some modules raise on class_variables enumeration; skip.
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
findings.sort_by { |f| [f.owner, f.ivar] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Scan only Rails framework modules (Railties, ActiveRecord, etc.)
|
|
101
|
+
# — the ones the shim targets. Useful for "is Rails itself clean?"
|
|
102
|
+
def scan_rails
|
|
103
|
+
scan.select { |f| f.owner.start_with?("Rails", "ActiveRecord", "ActiveSupport",
|
|
104
|
+
"ActionController", "ActionView", "ActionDispatch", "ActionMailer",
|
|
105
|
+
"ActiveJob", "ActionCable", "ActionText", "ActionMailbox", "ActiveStorage") }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Scan app + gem classes outside the Rails framework.
|
|
109
|
+
def scan_app
|
|
110
|
+
scan - scan_rails
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Human-readable report. Returns a string; also prints to $stderr
|
|
114
|
+
# if `print:` is true (default).
|
|
115
|
+
def report(print: true)
|
|
116
|
+
findings = scan
|
|
117
|
+
rails_findings = findings.select { |f| rails_namespace?(f.owner) }
|
|
118
|
+
app_findings = findings - rails_findings
|
|
119
|
+
|
|
120
|
+
lines = []
|
|
121
|
+
cvar_count = findings.count { |f| f.kind == :cvar }
|
|
122
|
+
ivar_count = findings.count { |f| f.kind == :ivar }
|
|
123
|
+
lines << "ractor-rails-shim check: #{findings.size} blocker(s) found" \
|
|
124
|
+
" (#{ivar_count} class-ivar, #{cvar_count} class-var)"
|
|
125
|
+
lines << " (unshareable values in @ivar and @@cvar; reads/writes from a non-main Ractor"
|
|
126
|
+
lines << " would raise Ractor::IsolationError)"
|
|
127
|
+
lines << ""
|
|
128
|
+
|
|
129
|
+
%i[rails app].each do |group|
|
|
130
|
+
grp = group == :rails ? rails_findings : app_findings
|
|
131
|
+
next if grp.empty?
|
|
132
|
+
|
|
133
|
+
label = group == :rails ? "Rails framework" : "app + gems"
|
|
134
|
+
lines << "=== #{label} (#{grp.size}) ==="
|
|
135
|
+
grp.first(50).each do |f|
|
|
136
|
+
tag = f.kind == :cvar ? " (mattr/cattr — shim targets)" : ""
|
|
137
|
+
lines << " #{f.owner}#{f.ivar} = #{f.value_class}#{tag}"
|
|
138
|
+
lines << " #{f.source}" if f.source
|
|
139
|
+
end
|
|
140
|
+
if grp.size > 50
|
|
141
|
+
lines << " ... and #{grp.size - 50} more (use Check.scan to see all)"
|
|
142
|
+
end
|
|
143
|
+
lines << ""
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if findings.empty?
|
|
147
|
+
lines << "no blockers found — app may be Ractor-compatible"
|
|
148
|
+
else
|
|
149
|
+
lines << "hints:"
|
|
150
|
+
lines << " - require \"ractor_rails_shim\" and call RactorRailsShim.install before"
|
|
151
|
+
lines << " Rails.application is first accessed (early in config/boot.rb)"
|
|
152
|
+
lines << " - class-var (@@foo) blockers from mattr_accessor/cattr_accessor are"
|
|
153
|
+
lines << " rerouted by the shim automatically once installed"
|
|
154
|
+
lines << " - raw class-ivar (@foo) blockers are NOT fixed by the shim; patch the"
|
|
155
|
+
lines << " gem or use Ractor.make_shareable + a constant for shareable state"
|
|
156
|
+
lines << " - for per-Ractor mutable state use Ractor.store_if_absent(key) { default }"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
out = lines.join("\n")
|
|
160
|
+
$stderr.puts(out) if print
|
|
161
|
+
out
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def rails_namespace?(name)
|
|
167
|
+
name.start_with?("Rails", "ActiveRecord", "ActiveSupport", "ActionController",
|
|
168
|
+
"ActionView", "ActionDispatch", "ActionMailer", "ActiveJob", "ActionCable",
|
|
169
|
+
"ActionText", "ActionMailbox", "ActiveStorage")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Best-effort: locate where the ivar is set in source. Not always
|
|
173
|
+
# possible (set via mattr_accessor macro), but useful when it is.
|
|
174
|
+
def locate(mod, ivar)
|
|
175
|
+
# Check if it's a method-defined accessor (mattr_accessor etc.)
|
|
176
|
+
reader = ivar.to_s.delete("@").to_sym
|
|
177
|
+
if mod.singleton_class.method_defined?(reader)
|
|
178
|
+
m = mod.singleton_class.instance_method(reader)
|
|
179
|
+
if m.source_location
|
|
180
|
+
file, line = m.source_location
|
|
181
|
+
return "#{file}:#{line}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
nil
|
|
185
|
+
rescue => e
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fallback IsolatedExecutionState when ActiveSupport is not available.
|
|
4
|
+
# This is a simple thread-local storage that mimics the ActiveSupport API
|
|
5
|
+
# enough for the shim to work. In production (with Rails loaded), the real
|
|
6
|
+
# ActiveSupport::IsolatedExecutionState is used instead.
|
|
7
|
+
module ActiveSupport
|
|
8
|
+
module IsolatedExecutionState
|
|
9
|
+
KEY = :active_support_execution_state_fallback
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def [](key)
|
|
13
|
+
Thread.current[KEY]&.[](key)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
(Thread.current[KEY] ||= {})[key] = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def key?(key)
|
|
21
|
+
Thread.current[KEY]&.key?(key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(key)
|
|
25
|
+
Thread.current[KEY]&.delete(key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear
|
|
29
|
+
Thread.current[KEY] = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end unless defined?(ActiveSupport::IsolatedExecutionState)
|