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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f309da49240393f31079209d41e422a4f5fabde1428d344d6300ace7ffc5a94d
4
- data.tar.gz: d6e330ac46078ff31534d594d5c90f7ae1c739bf765223fda27f03d546350fcb
3
+ metadata.gz: 4a4b7ce968f90326f2b476d713b7fab6c524f8f7b111f60a2ae582d1e162de88
4
+ data.tar.gz: c144478be527ff98804db48add54d67edb4124514fba3670b970990ef09e36c0
5
5
  SHA512:
6
- metadata.gz: 147c5795a7bf89b07dc1ebc19503cc12fd37c556d727a05a2c4c776c4bf3822def6c37e289343345fa6e861ecdf9a2ef331b13f69dee377cde97f5364dcd42d5
7
- data.tar.gz: 3ef1bdce56de6ee989797f780c451f4a56d4ebd6427979a120cc25e90a16d3779e97fe83e2ef9fa48f2e011a7de21bd2df81e2731e77914c2b6d01dd7743460d
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
- - Improved the published gem summary and description (gemspec). Metadata-only
12
- release no code changes.
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, 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.
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`. It will not work (and refuses to install) on
27
- earlier Ruby versions.
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
- **Patched kino:** the shim was validated against a kino build carrying a
39
- per-ractor env-string cache fix that removes a cross-ractor SIGBUS during
40
- sustained writes. The patch is published at
41
- [DDKatch/kino](https://github.com/DDKatch/kino) on the
42
- `ractor-per-ractor-env-cache` branch. Use that fork (rather than upstream
43
- `yaroslav/kino`) if you hit write-path crashes. To build it: clone the fork,
44
- check out that branch, then `asdf local rust 1.85.0 && cargo build --release`
45
- (the native extension is compiled by the Rails app's `bundle install`).
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.5. Rails already uses this primitive for `ActiveRecord::ConnectionHandling.connection_handler` (for thread safety), so the pattern is proven in production.
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
- RactorRailsShim._abstract_registry[self] = true if Ractor.main?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 SKIPED. For
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
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 unless v.nil?
893
+ return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
894
894
  result = check.call
895
895
  ActiveSupport::IsolatedExecutionState[#{key_str}] = result
896
896
  result