ractor-rails-shim 0.4.0 → 0.5.1
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 +645 -0
- data/LICENSE.txt +2 -2
- data/README.md +48 -6
- data/lib/ractor_rails_shim/callbacks/dependent_association_transport.rb +62 -5
- data/lib/ractor_rails_shim/callbacks/registry.rb +61 -7
- data/lib/ractor_rails_shim/callbacks/symbolic_transport.rb +271 -32
- data/lib/ractor_rails_shim/foundation/storage_strategy.rb +29 -6
- data/lib/ractor_rails_shim/loader.rb +14 -0
- data/lib/ractor_rails_shim/patches/action_cable.rb +165 -0
- data/lib/ractor_rails_shim/patches/action_controller.rb +51 -2
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +11 -1
- data/lib/ractor_rails_shim/patches/action_view.rb +60 -2
- data/lib/ractor_rails_shim/patches/active_job.rb +113 -0
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +10 -0
- data/lib/ractor_rails_shim/patches/active_storage.rb +101 -2
- data/lib/ractor_rails_shim/patches/active_support.rb +173 -8
- data/lib/ractor_rails_shim/patches/activerecord.rb +960 -149
- data/lib/ractor_rails_shim/patches/callables.rb +60 -0
- data/lib/ractor_rails_shim/patches/cgi.rb +93 -0
- data/lib/ractor_rails_shim/patches/core.rb +125 -0
- data/lib/ractor_rails_shim/patches/dependencies_interlock.rb +62 -0
- data/lib/ractor_rails_shim/patches/devise.rb +2 -1
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +2 -1
- data/lib/ractor_rails_shim/patches/i18n.rb +24 -12
- data/lib/ractor_rails_shim/patches/kaminari.rb +2 -1
- data/lib/ractor_rails_shim/patches/live_streaming.rb +71 -0
- data/lib/ractor_rails_shim/patches/mail.rb +39 -5
- data/lib/ractor_rails_shim/patches/make_shareable.rb +22 -0
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +8 -4
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +2 -1
- data/lib/ractor_rails_shim/patches/rack.rb +62 -5
- data/lib/ractor_rails_shim/patches/rails_module.rb +20 -7
- data/lib/ractor_rails_shim/patches/railtie_configuration.rb +132 -0
- data/lib/ractor_rails_shim/patches/secure_token.rb +131 -0
- data/lib/ractor_rails_shim/patches/solid_cache.rb +85 -0
- data/lib/ractor_rails_shim/patches/solid_queue.rb +117 -0
- data/lib/ractor_rails_shim/patches/thread_mattr.rb +112 -0
- data/lib/ractor_rails_shim/patches/time_zone_lookup.rb +94 -0
- data/lib/ractor_rails_shim/patches/turbo.rb +173 -0
- data/lib/ractor_rails_shim/patches/tzinfo_data_source.rb +107 -0
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +2 -1
- data/lib/ractor_rails_shim/roles/app_shareabilizer.rb +81 -0
- data/lib/ractor_rails_shim/roles/callback_capture.rb +221 -32
- data/lib/ractor_rails_shim/roles/fallback_builder.rb +113 -2
- data/lib/ractor_rails_shim/roles/install_strategy.rb +26 -0
- data/lib/ractor_rails_shim/roles/lifecycle.rb +63 -1
- data/lib/ractor_rails_shim/roles/per_ractor_cache.rb +288 -0
- data/lib/ractor_rails_shim/roles/shareability_traversal.rb +158 -29
- data/lib/ractor_rails_shim/roles/worker_app.rb +21 -0
- data/lib/ractor_rails_shim/version.rb +2 -2
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abdcca0b7262caf6686c80535a98635edcb4a6f02e373d1ab1cedde7d9ee8867
|
|
4
|
+
data.tar.gz: 021d39390668e696131e221d1a767a763ebc0765fa37ff3b5a4dbd082609b010
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 977596297e62a5743b33c7afcfdec8b3cb4f3be66aca0e477f7de4acc03f3213cfe04138bc99dbfb5bbe4982e8852cd47d145fce7e0536f60d9b5216cf7e4709
|
|
7
|
+
data.tar.gz: 6a46bc0e33bca9f70752effbfb6c5ba90e85b71cff16259b828787a1a9d539c1a93bbd32f491cbb681f7246527ffcb6065ba69cb2ae3ba7f92e9ef95c3c9359f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,651 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] — 2026-10-25
|
|
11
|
+
|
|
12
|
+
### Release housekeeping
|
|
13
|
+
|
|
14
|
+
- Version bump only. No functional changes.
|
|
15
|
+
|
|
16
|
+
## [0.5.0] — 2026-10-24
|
|
17
|
+
|
|
18
|
+
### Performance — read-path hot-loop optimization (measured)
|
|
19
|
+
|
|
20
|
+
Four changes targeting the per-request class-attribute/config read path,
|
|
21
|
+
benchmarked after each against the test app under kino `:ractor` (-w5 -t1,
|
|
22
|
+
Ruby 4.0.7, ab c=64):
|
|
23
|
+
|
|
24
|
+
- **Single-probe IES hit** (`StorageStrategy::Ractor` and all ~53 inline
|
|
25
|
+
eval'd readers): the non-nil IES hit no longer pays a second
|
|
26
|
+
`storage.key?` probe — the stored-nil disambiguation moved to the nil
|
|
27
|
+
path. Semantics identical (a stored nil is still a hit). Micro-bench
|
|
28
|
+
exact-key reader 2.51 → 3.52 Mops/s.
|
|
29
|
+
- **Worker IES prefill** (`WorkerApp#setup_once!`): each worker copies the
|
|
30
|
+
frozen shareable fallback table's non-nil slots into its own IES on
|
|
31
|
+
first request, so every later read of fallback-backed config is a
|
|
32
|
+
single first-tier hit. Per-ractor overrides are preserved (key? guard).
|
|
33
|
+
- **Boot-time GC.compact** (`prepare_for_ractors!`): one compaction in
|
|
34
|
+
main after the shareabilization passes, before workers spawn.
|
|
35
|
+
Explicitly NOT `GC.auto_compact` (which hangs kino :ractor under load).
|
|
36
|
+
Opt-outs: `RRAILS_SHIM_DISABLE_BOOT_COMPACT=1` / `RUBY_GC_DISABLE_COMPACTION=1`.
|
|
37
|
+
- New `bench/accessor_read_bench.rb` micro-benchmark (raw ivar floor vs
|
|
38
|
+
each lookup shape).
|
|
39
|
+
- **Compiled callback chains** (`SymbolicTransport` + `Registry`): chains
|
|
40
|
+
compile once per (class, kind) into frozen phase arrays — one ancestor
|
|
41
|
+
walk instead of three per replay, no per-replay `const_get` or
|
|
42
|
+
transport select. Micro-bench: 0.07 → 0.21 Mops/s (3×). E2E POST /posts
|
|
43
|
+
3,000 → 3,445 rps on top of the other changes; `/up` 14,991 → 16,031.
|
|
44
|
+
|
|
45
|
+
End-to-end (req/s, baseline → full stack; two independent full-stack runs
|
|
46
|
+
agreed within noise): `/up` 13,986 → ~15,400 (+7-13%), `/posts_plain`
|
|
47
|
+
4,245 → 4,743-4,900 (+12-15%), `POST /posts` 2,442 → 2,728-3,000
|
|
48
|
+
(+12-23%), `/posts` (Kaminari, DB-bound) 527 → 548-574 (+4-9%). Memory
|
|
49
|
+
flat (~270 MB cold). All 832 unit specs + strategy_proc + integration
|
|
50
|
+
spec green.
|
|
51
|
+
|
|
52
|
+
### Added — multi-database (`connects_to` / `connected_to`) in worker Ractors
|
|
53
|
+
|
|
54
|
+
Test-app row 81 now runs a real second database, which exposed three gaps in
|
|
55
|
+
the worker connection setup:
|
|
56
|
+
|
|
57
|
+
- `init_worker_ar_connections!` established EVERY configured database on the
|
|
58
|
+
`:default` shard, so in a multi-database app the LAST config won — workers
|
|
59
|
+
queried the secondary database and raised `PG::UndefinedTable` for every
|
|
60
|
+
primary table. The config named `primary` (Rails' convention) now owns the
|
|
61
|
+
default shard; single-database apps fall back to the first entry.
|
|
62
|
+
- Each named database is registered for the writing AND the reading role;
|
|
63
|
+
`connected_to(role: :reading)` otherwise raised "calling `connected_to` is
|
|
64
|
+
not allowed when there is no connection defined for that role".
|
|
65
|
+
- New `_capture_ar_connection_classes!` (main, prepare time) records every
|
|
66
|
+
model that owns a pool via `connects_to` (Solid Cache/Queue/Cable records,
|
|
67
|
+
app multi-database models) together with the db config main resolved.
|
|
68
|
+
Workers re-establish those pools from the snapshot under the model's own
|
|
69
|
+
`connection_specification_name` — a lazily autoloaded class re-resolved
|
|
70
|
+
`connects_to` to the PRIMARY config (observed: `AuditEntry` queried the
|
|
71
|
+
primary database and its table was "missing").
|
|
72
|
+
|
|
73
|
+
Evidence: test app `/multi_db_probe` in a worker — secondary write + re-read,
|
|
74
|
+
a primary read in the same request, and
|
|
75
|
+
`SecondaryRecord.connected_to(role: :reading)`. Suite: 830 runs, 0 failures.
|
|
76
|
+
|
|
77
|
+
### Added — turbo-rails worker support (test-app rows 105 + 121)
|
|
78
|
+
|
|
79
|
+
Adopting turbo-rails and exercising ActiveStorage direct uploads in worker
|
|
80
|
+
Ractors surfaced five framework-level walls, all fixed here:
|
|
81
|
+
|
|
82
|
+
- `patches/thread_mattr.rb` (new, install-time): `Module#thread_mattr_reader/
|
|
83
|
+
writer` generate methods that memoize their IES key in a class/module ivar
|
|
84
|
+
(`@__thread_mattr_#{sym} ||=`) — a write that raises IsolationError in
|
|
85
|
+
workers. turbo-rails' `Turbo.current_request_id` (around_action on EVERY
|
|
86
|
+
request) and `ActiveRecord::Base.suppressed_turbo_broadcasts` hit this, so
|
|
87
|
+
every worker request died with a 500. The patched macros emit the
|
|
88
|
+
deterministic key without the ivar memo (same per-class isolation).
|
|
89
|
+
- `patches/turbo.rb` (new, prepare-time): re-emits turbo's
|
|
90
|
+
`_render_with_renderer_turbo_stream` renderer as a string-eval'd `def`
|
|
91
|
+
(upstream `define_method`s a main-compiled Proc), and routes PROC layouts
|
|
92
|
+
(`Turbo::Frames::FrameRequest`'s conditional frame layout — re-emitted per
|
|
93
|
+
controller by the Layouts `inherited` hook) through a frozen
|
|
94
|
+
`PROC_LAYOUT_PROCS` registry read by a real `def _layout_from_proc`
|
|
95
|
+
(`instance_exec` preserves the define_method self-rebinding contract).
|
|
96
|
+
- `patches/secure_token.rb` (new, install-time): `has_secure_token` registers
|
|
97
|
+
a BLOCK callback — skipped in workers, so worker-created rows had NULL
|
|
98
|
+
tokens (`ActiveStorage::Blob#key` → direct uploads died with
|
|
99
|
+
PG::NotNullViolation). The macro wrapper appends an equivalent Symbol
|
|
100
|
+
callback (and re-applies it to models loaded before the patch, e.g.
|
|
101
|
+
ActiveStorage::Blob).
|
|
102
|
+
- `SymbolicTransport::DEFAULT_KINDS` now includes `:initialize` (Rails 8.1's
|
|
103
|
+
`generate_secure_token_on` default) — declared symbolic after_initialize
|
|
104
|
+
filters replay in workers.
|
|
105
|
+
- `patches/active_storage.rb`: prebuilds every configured service +
|
|
106
|
+
warms each service's memoized `url_helpers` in main (DiskService#url wrote
|
|
107
|
+
to a frozen instance), registers the blob `service_name` default
|
|
108
|
+
symbolically (seeded `ACTIVE_STORAGE_DEFAULT_SERVICE`), injects that default
|
|
109
|
+
in `create_before_direct_upload!` (the endpoint passes an explicit nil),
|
|
110
|
+
and re-registers `ActiveStorage::SetCurrent` as a Symbol before_action
|
|
111
|
+
(the upstream block never replays → "Missing host to link to!").
|
|
112
|
+
- `patches/active_controller.rb`-adjacent constants:
|
|
113
|
+
`ActionController::Parameters::EMPTY_ARRAY/EMPTY_HASH` registered shareable.
|
|
114
|
+
|
|
115
|
+
Evidence: test app rows 105 (turbo stream responses, `.turbo_stream.erb`
|
|
116
|
+
templates, model broadcast enqueued from a worker + performed by `bin/jobs`
|
|
117
|
+
with a solid_cable message) and 121 (rich_text_area; full direct-upload
|
|
118
|
+
endpoint cycle: POST → signed id/URL → disk PUT → blob redirect → service
|
|
119
|
+
URL bytes). Embed parsing remains the documented Nokogiri wall (row 50).
|
|
120
|
+
|
|
121
|
+
### Fixed — worker-side `:memory_store` / `:null_store` (`Rails.cache` raised FrozenError on the first call)
|
|
122
|
+
|
|
123
|
+
`Rails.cache` is captured into `CLASS_ATTRIBUTES`. `FallbackBuilder` therefore
|
|
124
|
+
deep-froze the store at prepare time.
|
|
125
|
+
|
|
126
|
+
- `MemoryStore` refreshes LRU order with `@data.delete` / `@data[key] =` on
|
|
127
|
+
every read.
|
|
128
|
+
- `NullStore` memoizes `@local_cache_key` on the instance.
|
|
129
|
+
|
|
130
|
+
Both raised `FrozenError` on the first cache call in every Ractor. The failed
|
|
131
|
+
`make_shareable` also froze the live main-Ractor store, so main broke too.
|
|
132
|
+
Solid Cache and FileStore were unaffected: they hold no mutable hot-path state.
|
|
133
|
+
|
|
134
|
+
- `roles/per_ractor_cache.rb` (new): process-local stores are swapped for a
|
|
135
|
+
same-class shareable proxy. `klass.allocate` plus a singleton delegator keeps
|
|
136
|
+
`class` and `is_a?` exact. The proxy holds only a shareable spec (class name
|
|
137
|
+
plus option values that survive `Ractor.make_shareable`). Every store method
|
|
138
|
+
delegates to a `Ractor.current`-memoized instance, so each Ractor gets its own
|
|
139
|
+
`MemoryStore` — the same shape as forked processes. The main Ractor keeps the
|
|
140
|
+
original instance and its boot-warmed entries. `Rails.cache` and
|
|
141
|
+
`ActionController::Base.config.cache_store` are re-pointed at the proxy from
|
|
142
|
+
both `prepare_for_ractors!` and `make_app_shareable!`.
|
|
143
|
+
- `patches/active_support.rb`: `SerializerWithFallback::SERIALIZERS` added to
|
|
144
|
+
`SHAREABLE_CONSTANTS`. Worker-side store construction reads it through
|
|
145
|
+
`Store#initialize`. New `_install_cache_store_cattr_patch` re-routes
|
|
146
|
+
`Store.logger` / `raise_on_invalid_cache_expiration_time` through
|
|
147
|
+
`mattr_accessor` when `active_support/cache` loaded before the shim. Workers
|
|
148
|
+
cannot read `@@logger` at all.
|
|
149
|
+
- `patches/rails_module.rb`: the worker `Rails.cache` reader returns the proxy
|
|
150
|
+
constant first, then the shareable fallback.
|
|
151
|
+
- Redis/MemCache stores are not wrapped. Their connection config is not
|
|
152
|
+
reconstructible from `store.options`. A boot warning tells the operator to
|
|
153
|
+
use `:solid_cache_store` or `:file_store`.
|
|
154
|
+
- Spec: `spec/per_ractor_cache_spec.rb` (7 cases: classification, same-class
|
|
155
|
+
shareable proxy, main keeps the original, worker write/read/fetch, per-Ractor
|
|
156
|
+
isolation, NullStore, FileStore unwrapped).
|
|
157
|
+
|
|
158
|
+
### Fixed — worker-side `serialize :arguments, coder: JSON` (Solid Queue enqueue from a worker, row 110)
|
|
159
|
+
|
|
160
|
+
Four coordinated fixes, all proven by the test app's `:ractor` suite plus an
|
|
161
|
+
end-to-end kino + `bin/jobs` dispatcher run (worker enqueues; the separate
|
|
162
|
+
dispatcher performs):
|
|
163
|
+
|
|
164
|
+
- `patches/solid_queue.rb` (new): prewarms `SolidQueue::Batch.migrated?`
|
|
165
|
+
(a lazily-memoized boolean class ivar on a frozen engine-model class —
|
|
166
|
+
the first worker enqueue died on the `||=` write) and re-registers the
|
|
167
|
+
execution models' `before_create -> { assume_attributes_from_job }`
|
|
168
|
+
BLOCK callback symbolically (`before_create :assume_attributes_from_job`
|
|
169
|
+
via a string-eval'd redefinition of the `JobAttributes` macro). Without
|
|
170
|
+
the latter, SolidQueue's `ScheduledExecution` INSERT died with
|
|
171
|
+
PG::NotNullViolation (null `queue_name`) because SymbolicTransport
|
|
172
|
+
fail-safe skips block filters. Each execution model's current assumable
|
|
173
|
+
list is re-applied (ReadyExecution none/default, ScheduledExecution
|
|
174
|
+
`:scheduled_at`, BlockedExecution `:concurrency_key`).
|
|
175
|
+
- `FallbackBuilder.try_make_shareable` no longer blanket-skips
|
|
176
|
+
`__class_attr__queue_adapter`: stateless DB-write adapters (SolidQueueAdapter)
|
|
177
|
+
freeze into the worker fallback so workers enqueue through the REAL
|
|
178
|
+
adapter; unshareable ones (AsyncAdapter's live scheduler) still fall back
|
|
179
|
+
to the declared default.
|
|
180
|
+
- `_share_ar_pending_attribute_modifications!` gains a binding-locals
|
|
181
|
+
retry pass + `SerializedTypeDecorator` (callables.rb): decoration Procs
|
|
182
|
+
whose captured locals are unfrozen value objects (e.g. the `serialize`
|
|
183
|
+
macro's `ActiveRecord::Coders::JSON` instance) now freeze those locals in
|
|
184
|
+
place and retry; serialization.rb-sourced decorations that can never be
|
|
185
|
+
shared (`coder ||=` reassignment taints the proc) are reconstructed as a
|
|
186
|
+
shareable callable that rebuilds `Type::Serialized` per apply. WITHOUT
|
|
187
|
+
this, workers rebuilt the `arguments` attribute as a plain Text type and
|
|
188
|
+
stored Ruby hash-inspect instead of JSON (the dispatcher then died with
|
|
189
|
+
JSON::ParserError). SCOPE GUARD: only value-object coders
|
|
190
|
+
(ActiveRecord::Coders::* instances) are reconstructed — coder CLASSES
|
|
191
|
+
(ActionText::Content, whose load path needs Nokogiri — the permanent
|
|
192
|
+
worker wall) keep the pre-fix no-op behavior, preserving row 120's
|
|
193
|
+
raw-HTML round-trip.
|
|
194
|
+
- `JsonCoderFreezer` (in patches/active_job.rb): freezes the
|
|
195
|
+
`JSONGemCoderEncoder::CODER` constant's value in main at prepare time so
|
|
196
|
+
the worker-side encode path (called by `ActiveRecord::Coders::JSON#dump`
|
|
197
|
+
through `Type::Serialized#serialize`) can read the constant legally.
|
|
198
|
+
- `prepare_for_ractors!` pipeline gains two steps (wired through
|
|
199
|
+
`AppShareabilizer`'s configure seam): `prewarm_solid_queue` and
|
|
200
|
+
`freeze_json_coder`.
|
|
201
|
+
|
|
202
|
+
### Added — worker-side `around_action` / `:around` callback replay (wrap-style)
|
|
203
|
+
|
|
204
|
+
Symbolic method-name `:around` filters (`around_action :method`,
|
|
205
|
+
`around_save :method`, …) now replay in worker Ractors. `SymbolicTransport`
|
|
206
|
+
gains `around(context, kind, inner)` and `Registry#replay` builds a wrap
|
|
207
|
+
chain around the single yield. Nesting is pinned against the real
|
|
208
|
+
ActiveSupport compiled chain: first-declared `:around` filter outermost, the
|
|
209
|
+
after-half runs inside the innermost `yield`, a filter that never yields
|
|
210
|
+
suppresses the block and the after-half, and the chain evaluates to the
|
|
211
|
+
block's result (a yielding filter's own return value is discarded; a
|
|
212
|
+
non-yielding one evaluates to nil). Lambda/Proc around filters remain
|
|
213
|
+
fail-safe skipped (un-shareable Procs — same policy as every other Proc
|
|
214
|
+
filter). Specs: `spec/callbacks/around_transport_spec.rb`. This resolves the
|
|
215
|
+
test-app TODO #90 decision (`around_action`) in favor of shim support.
|
|
216
|
+
|
|
217
|
+
## [0.4.2]
|
|
218
|
+
|
|
219
|
+
### Fixed — worker time-zone creation dies on `TZInfo::DataSource`'s class variables
|
|
220
|
+
|
|
221
|
+
Surfaced by the same durations probe (the layer under the
|
|
222
|
+
`ActiveSupport::TimeZone.[]` fix): every `ActiveSupport::TimeZone`
|
|
223
|
+
creation resolves its TZInfo data through `TZInfo::DataSource.get`, which
|
|
224
|
+
memoizes into the class variables `@@instance` / `@@default_mutex`. Ruby
|
|
225
|
+
4.0 forbids ANY class-variable access from non-main Ractors
|
|
226
|
+
(`Ractor::IsolationError: can not access class variables from non-main
|
|
227
|
+
Ractors`).
|
|
228
|
+
|
|
229
|
+
- `get` is now Ractor-aware: main keeps the upstream body verbatim
|
|
230
|
+
(class-variable memoize + mutex); workers get a per-Ractor DataSource
|
|
231
|
+
from IsolatedExecutionState built by the upstream private
|
|
232
|
+
`create_default_data_source` (same ruby-data/zoneinfo choice; per-
|
|
233
|
+
instance lookup caches make per-Ractor caching semantically
|
|
234
|
+
equivalent). The upstream `set` (manual source selection) stays
|
|
235
|
+
main-only.
|
|
236
|
+
|
|
237
|
+
### Fixed — worker `Time.zone` resolution dies on `TimeZone.[]`'s Concurrent::Map class ivar
|
|
238
|
+
|
|
239
|
+
Surfaced by the test app's durations probe (row 131: `Time.zone` /
|
|
240
|
+
`Time.use_zone` from a worker).
|
|
241
|
+
|
|
242
|
+
- `Time.zone` (and `in_time_zone` / `use_zone`) resolves zones through
|
|
243
|
+
`ActiveSupport::TimeZone.[]`, whose String branch memoizes into the class
|
|
244
|
+
ivar `@lazy_zones_map` — a `Concurrent::Map` set at load time. Both
|
|
245
|
+
halves are forbidden in workers: reading the unshareable Concurrent::Map
|
|
246
|
+
class ivar raises `Ractor::IsolationError` (can not get unshareable
|
|
247
|
+
values from instance variables of classes/modules), and a worker-side
|
|
248
|
+
memoize would try to set it.
|
|
249
|
+
- The patched `[]` keeps the upstream body verbatim on main; workers use a
|
|
250
|
+
per-Ractor Hash cache from IsolatedExecutionState. Zone instances are
|
|
251
|
+
plain data and TZInfo lookups are worker-safe, so worker-local caches are
|
|
252
|
+
semantically equivalent. The numeric-offset branch (`all.find`) stays
|
|
253
|
+
main-only (`all` also reads class ivars).
|
|
254
|
+
|
|
255
|
+
### Fixed — worker-side streaming (`ActionController::Live`) hangs / dies
|
|
256
|
+
|
|
257
|
+
Surfaced by the test app's blob proxy dispatch (row 127:
|
|
258
|
+
`GET /rails/active_storage/blobs/proxy/:signed_id/:filename` in a worker).
|
|
259
|
+
|
|
260
|
+
- The Active Storage blob proxy controller includes `ActionController::Live`,
|
|
261
|
+
which processes the action on a pool thread: a lazy class-method reader
|
|
262
|
+
sets a class ivar (`@live_thread_pool_executor ||=
|
|
263
|
+
Concurrent::CachedThreadPool.new` — Ruby 4.0 forbids class-ivar writes
|
|
264
|
+
from non-main Ractors, `Ractor::IsolationError`), and `Live#process` then
|
|
265
|
+
blocks on `await_commit` until the spawned thread commits. Even with a
|
|
266
|
+
worker-local pool, the thread hop deadlocks in practice: any exception
|
|
267
|
+
before `commit!` kills the pooled thread (`abort_on_exception = true`)
|
|
268
|
+
and the request hangs forever.
|
|
269
|
+
- The patch runs the streaming action INLINE in worker Ractors:
|
|
270
|
+
`new_controller_thread` yields on the request thread (main keeps the
|
|
271
|
+
upstream pool-thread behavior verbatim). The bytes still stream through
|
|
272
|
+
the Rack response body; only true streaming concurrency (early header
|
|
273
|
+
flush) is lost worker-side, which is documented in the row note.
|
|
274
|
+
|
|
275
|
+
### Fixed — worker `Dependencies.interlock` read dies on the unshareable class ivar
|
|
276
|
+
|
|
277
|
+
Surfaced by the same blob proxy dispatch (the next frame after the
|
|
278
|
+
streaming fix): `ActionController::Live#process` runs the action inside
|
|
279
|
+
`ActiveSupport::Dependencies.interlock.running { ... }`, and upstream sets
|
|
280
|
+
the `interlock` class ivar at load time to an Interlock wrapping an
|
|
281
|
+
unshareable `Concurrency::ShareLock` — reading it from a worker is a
|
|
282
|
+
`Ractor::IsolationError` (can not get unshareable values from instance
|
|
283
|
+
variables of classes/modules). Regular worker requests don't hit it: the
|
|
284
|
+
shim's production stack drops the `ActionDispatch::Executor` middleware,
|
|
285
|
+
the only other interlock consumer.
|
|
286
|
+
|
|
287
|
+
- The reader is replaced with a Ractor-aware method: main returns the
|
|
288
|
+
upstream class ivar (100% upstream semantics); workers get a per-Ractor
|
|
289
|
+
Interlock from IsolatedExecutionState. Workers never reload (the graph
|
|
290
|
+
is frozen), so the per-Ractor lock never contends. The upstream writer
|
|
291
|
+
(`interlock=`) is untouched.
|
|
292
|
+
|
|
293
|
+
### Fixed — `config_for` / method_missing-backed config reads die in workers (`@@options` class variable)
|
|
294
|
+
|
|
295
|
+
Surfaced by the test app's `GET /features/config_probe` (rows 115/134/138:
|
|
296
|
+
read the production SMTP shape, `config.x`, and `config_for(:audit)` from a
|
|
297
|
+
worker).
|
|
298
|
+
|
|
299
|
+
- **`Rails::Railtie::Configuration` stores its method_missing option hash in
|
|
300
|
+
the class variable `@@options`**, and Ruby 4.0 forbids ANY class-variable
|
|
301
|
+
access from a non-main Ractor (`Ractor::IsolationError: can not access
|
|
302
|
+
class variables from non-main Ractors`) — so any worker-side read of an
|
|
303
|
+
option that is not an explicit method (e.g. `config.x`) died.
|
|
304
|
+
- The patch is installed with `class_eval` (NOT prepend): Ruby forbids
|
|
305
|
+
class-variable access from a prepended module ("class variable @@options
|
|
306
|
+
... is overtaken by <module>"), and even a non-accessing worker branch
|
|
307
|
+
would be compiled into the module. Methods defined via class_eval on the
|
|
308
|
+
class itself legally own the cvar, so the main branch delegates to the
|
|
309
|
+
aliased originals — 100% upstream semantics on main.
|
|
310
|
+
- Workers read through a **frozen snapshot** of the main-side `@@options`,
|
|
311
|
+
taken by the AppShareabilizer freeze pipeline (after Proc/Map/Lock
|
|
312
|
+
replacement, before the final make_shareable) and stored as the frozen
|
|
313
|
+
constant `RRS_FROZEN_OPTIONS` on the class (constants are shareable; the
|
|
314
|
+
class variable is not). Per-value `make_shareable` with rescue:
|
|
315
|
+
non-shareable leftovers (lock objects) are skipped. A per-Ractor IES
|
|
316
|
+
overlay (keyed by the config object's identity) covers worker-local
|
|
317
|
+
writes. Unknown names raise NoMethodError in workers instead of falling
|
|
318
|
+
through to the original (whose @@options access is forbidden).
|
|
319
|
+
|
|
320
|
+
### Fixed — intermittent worker `NoMethodError: undefined method 'owned?'` on NoOpLock
|
|
321
|
+
|
|
322
|
+
Observed ~1-in-7 kino runs on `GET /`: a worker hit
|
|
323
|
+
`Concurrent::Synchronization::MutexLockableObject#synchronize`, which (since
|
|
324
|
+
concurrent-ruby 1.3.8) checks `@__Lock__.owned?` before locking — and the
|
|
325
|
+
shim's `NoOpLock` (the shareable stand-in swapped for `Mutex`/`Monitor`
|
|
326
|
+
ivars during the graph freeze) didn't define `owned?`. Added
|
|
327
|
+
`def owned?; true; end`: callers take the direct-yield path, which is the
|
|
328
|
+
correct semantics for a never-contended post-boot lock.
|
|
329
|
+
|
|
330
|
+
### Fixed — `normalizes` breaks every User-touching worker route (DelegateClass types poison `instance_method` cross-Ractor)
|
|
331
|
+
|
|
332
|
+
Surfaced by the test app's `GET /features/normalizes_probe` (TODO P2 #3,
|
|
333
|
+
row 74: `User normalizes :email, with: ->(v) { v.strip.downcase }`).
|
|
334
|
+
|
|
335
|
+
- **`SerializeCastValue::ClassMethods#serialize_cast_value_compatible?` is not
|
|
336
|
+
DelegateClass-safe in workers** (`patches/activerecord.rb`): adding
|
|
337
|
+
`normalizes` to User wraps the attribute type in
|
|
338
|
+
`ActiveModel::Attributes::Normalization::NormalizedValueType`, a
|
|
339
|
+
`DelegateClass` subclass. DelegateClass defines its delegator methods via
|
|
340
|
+
`define_method` with **Proc bodies created in the defining Ractor**, and
|
|
341
|
+
Ruby 4.0 refuses `instance_method` / `instance_methods` on such classes
|
|
342
|
+
from another Ractor (`RuntimeError: defined with an un-shareable Proc in
|
|
343
|
+
a different Ractor`) — even for plain `def` methods on the same class.
|
|
344
|
+
The shim's IES-backed patch of `serialize_cast_value_compatible?` calls
|
|
345
|
+
`instance_method(...)` for every type construction, so ANY worker touching
|
|
346
|
+
User attributes boomed (Devise before_actions, mail probes, attach probes,
|
|
347
|
+
the root page, POST /posts...). The patch now rescues the cross-Ractor
|
|
348
|
+
error and recomputes with table snapshots only —
|
|
349
|
+
`method_defined?(sym, false)` per ancestor (verified safe cross-Ractor) —
|
|
350
|
+
reproducing the same first-owner comparison as upstream without Method
|
|
351
|
+
objects. Per-class IES key unchanged.
|
|
352
|
+
|
|
353
|
+
### Fixed — worker-side ActiveRecord enums (row 65)
|
|
354
|
+
|
|
355
|
+
Four worker-hostile method families generated by AR enums:
|
|
356
|
+
|
|
357
|
+
- predicates/bangs + class-level values reader + per-value scopes are
|
|
358
|
+
define_method'd with Procs compiled inside `_enum_methods_module` (a
|
|
359
|
+
Module the proc-replacement traversal cannot reach — it walks ivars,
|
|
360
|
+
not method tables) or singleton define_method Procs capturing
|
|
361
|
+
name/value locals that workers replay with a lost binding (NameError).
|
|
362
|
+
- Fix: the enum methods are re-defined as string-eval'd REAL defs (the
|
|
363
|
+
predicate compares `#{attr}_for_database`, bangs write via `update!`,
|
|
364
|
+
scopes build `all.where`/`where.not`, the pluralized reader returns a
|
|
365
|
+
const-backed frozen HWIA). Safe because Rails' `detect_enum_conflict!`
|
|
366
|
+
guarantees no user method occupies these names.
|
|
367
|
+
|
|
368
|
+
### Fixed — worker-side validations silently passed everything (rows 1+98)
|
|
369
|
+
|
|
370
|
+
Worker-side `valid?` returned TRUE with zero errors — invalid records
|
|
371
|
+
could save in workers (data corruption). Root causes, all fixed:
|
|
372
|
+
|
|
373
|
+
- The set_callback interceptor only recorded Symbol filters;
|
|
374
|
+
Validator-object filters (ActiveModel::Validator instances registered
|
|
375
|
+
by `validates` / `validates_with`) were never captured. The shim now
|
|
376
|
+
builds a shareable descriptor `{validator-class-name, attributes,
|
|
377
|
+
options, declaring-class}` and the SymbolicTransport rebuilds a fresh
|
|
378
|
+
validator in the worker and calls `.validate(record)` (return value
|
|
379
|
+
ignored — validators signal via errors, never halt).
|
|
380
|
+
- Phase parsing: Rails passes the phase as filter_list[0] only when
|
|
381
|
+
explicit; no-explicit-phase declarations (`validate :sym`) were
|
|
382
|
+
silently missed (parsed per normalize_callback_params).
|
|
383
|
+
- DEFAULT_KINDS was missing `:validate` (the ActiveModel validations
|
|
384
|
+
chain), and the interceptor predicate now includes ActiveRecord::Base
|
|
385
|
+
itself so Base-level declarations replay.
|
|
386
|
+
|
|
387
|
+
### Fixed — DelegateClass "special" methods, ActionView date selectors, CurrentAttributes keys in workers
|
|
388
|
+
|
|
389
|
+
- Ruby's delegate.rb defines every DelegateClass(X)'s "special" methods
|
|
390
|
+
(incl. `<<`) via `define_method(Delegator.delegating_block(m))` —
|
|
391
|
+
main-compiled lambdas that workers cannot call. They are detected by
|
|
392
|
+
shared source_location with a delegating-block marker and redefined as
|
|
393
|
+
string-eval'd defs. Fixes multipart parsing in workers
|
|
394
|
+
(Collector#on_mime_body calls Tempfile#<<).
|
|
395
|
+
- ActionView's DateTimeSelector#sec/min/hour/day/month/year (upstream
|
|
396
|
+
`define_method(&block)`) redefined as real defs — `date_select`
|
|
397
|
+
rendered NoMethodError in workers; plus a SelectType patch for select
|
|
398
|
+
helpers.
|
|
399
|
+
- CurrentAttributes' key-generated methods raised on frozen module reads
|
|
400
|
+
in workers — `Current.request_id` now round-trips.
|
|
401
|
+
|
|
402
|
+
### Fixed — worker-side Solid Cache (:solid_cache_store) round-trip; connects_to pool fallback; broken `reset_table_name` super
|
|
403
|
+
|
|
404
|
+
Surfaced by the test app's `GET /solid_cache_probe` (TODO #27: verify rows
|
|
405
|
+
45–47 against the REAL production store instead of the kino's historical
|
|
406
|
+
`:null_store` no-op path).
|
|
407
|
+
|
|
408
|
+
- **Broken `super` in the `reset_table_name` patch** (`patches/activerecord.rb`):
|
|
409
|
+
the shim's ModelSchema patch was installed via
|
|
410
|
+
`ModelSchema::ClassMethods.module_eval`, which REPLACES the original
|
|
411
|
+
`reset_table_name` in place — so the override's `super` skipped past the
|
|
412
|
+
module and found nothing. Every lazy main-side `reset_table_name` after
|
|
413
|
+
patch install raised `NoMethodError: super: no superclass method
|
|
414
|
+
'reset_table_name'` (silently swallowed by the warm passes' rescues). The
|
|
415
|
+
visible victims were HABTM join models, whose table names resolve lazily
|
|
416
|
+
through the builder's `table_name_resolver` AFTER patch install: their
|
|
417
|
+
`@table_name` never materialized and the resolver lambda survived the
|
|
418
|
+
graph freeze unfrozen, so worker-side HABTM writes died with
|
|
419
|
+
`IsolationError ... (@table_name_resolver from Post::HABTM_Tags)` —
|
|
420
|
+
with a heap-layout-sensitive failure rate that made the kino flaky across
|
|
421
|
+
shim changes. The override now captures the original via `alias_method`
|
|
422
|
+
and calls it explicitly; `ShareabilityTraversal.harden_ar_model_ivars!`
|
|
423
|
+
(called right before the graph freeze) additionally materializes join-model
|
|
424
|
+
`@table_name` and freezes the resolver lambda, with loud warnings instead
|
|
425
|
+
of silent skips.
|
|
426
|
+
|
|
427
|
+
- **connects_to pool fallback** (`patches/activerecord.rb`):
|
|
428
|
+
`init_worker_ar_connections!` + the patched `connection_pool` /
|
|
429
|
+
`retrieve_connection` / `connected?`. connects_to-style models (Solid
|
|
430
|
+
Cache / Solid Queue / Solid Cable) own their pool in the MAIN handler under
|
|
431
|
+
their CLASS name (`connects_to` sets `@connection_specification_name`) and
|
|
432
|
+
carry their own `default_shard` class_attribute (e.g. SolidCache::Record
|
|
433
|
+
gets `default_shard == :primary` from config/cache.yml `database: primary`),
|
|
434
|
+
while the worker handler only seeded Base-level (role, :default) pools —
|
|
435
|
+
workers querying such a model died with
|
|
436
|
+
`ConnectionNotDefined: No database connection defined for
|
|
437
|
+
SolidCache::Record with 'primary' shard`. Two fixes: (1) the worker seed
|
|
438
|
+
also registers each named env database config under its own name as a
|
|
439
|
+
shard key (the standard `connects_to database: <name>` convention), and
|
|
440
|
+
(2) the patched readers fall back to the Base-level pool for the same
|
|
441
|
+
role/shard when the class-owned pool is not present in the worker.
|
|
442
|
+
- **Per-Ractor `SolidCache::Entry.select_sql` memo**
|
|
443
|
+
(`patches/solid_cache.rb`, new): the read path memoizes per-keys-count SQL
|
|
444
|
+
in the class ivar `@select_sql` — a worker cannot write class ivars
|
|
445
|
+
(IsolationError). The singleton method now routes the memo through
|
|
446
|
+
Ractor.current in workers; main keeps the original lazy-memo semantics.
|
|
447
|
+
|
|
448
|
+
### Fixed — worker-Ractor mail hooks + ActiveJob `set(wait:)` enqueue; deterministic HABTM shareability
|
|
449
|
+
|
|
450
|
+
Surfaced by the test app's worker-Ractor audit of mail hooks (matrix rows
|
|
451
|
+
113/116) and ActiveJob enqueue options (rows 107/108). Verified by
|
|
452
|
+
`GET /stats/mail_hooks_probe` (interceptor + mailer-callback headers stamped on
|
|
453
|
+
worker-delivered mail) and `GET /features/job_resilience_probe`
|
|
454
|
+
(`set(queue:, wait:)` enqueue + rescue-handler config readable from a worker).
|
|
455
|
+
|
|
456
|
+
- **Mail hook registry snapshot** (`patches/mail.rb` +
|
|
457
|
+
`AppShareabilizer.make_shareable!`): the mail gem's
|
|
458
|
+
`inform_interceptors` / `inform_observers` read raw class variables, and the
|
|
459
|
+
shim's worker branch historically returned a per-worker EMPTY list —
|
|
460
|
+
interceptors/observers registered in main during boot silently never ran in
|
|
461
|
+
workers. The registries are now snapshotted as a frozen list of hook NAMES
|
|
462
|
+
(`RactorRailsShim::MAIL_INTERCEPTORS` / `MAIL_OBSERVERS`) AFTER the app-graph
|
|
463
|
+
freeze; workers resolve the constants and run the same hook objects main
|
|
464
|
+
registered. Non-module hooks (stateful instances) are skipped with a warning.
|
|
465
|
+
Placement note: the snapshot deliberately runs post-freeze — earlier
|
|
466
|
+
placements perturbed the heap-layout-sensitive make_shareable walk.
|
|
467
|
+
- **Deterministic HABTM join-model freeze**
|
|
468
|
+
(`ShareabilityTraversal.harden_ar_model_ivars!`, called right before the
|
|
469
|
+
graph freeze): HABTM join models resolve their table name through a lazy
|
|
470
|
+
resolver lambda (`@table_name_resolver = -> { table_name }`). Whether the
|
|
471
|
+
make_shareable walk reached it was heap-layout sensitive — some
|
|
472
|
+
configurations left it unfrozen and every worker-side HABTM write
|
|
473
|
+
(`post.tags << tag`, dependent destroys) died with
|
|
474
|
+
`IsolationError ... (@table_name_resolver from Post::HABTM_Tags)`. The
|
|
475
|
+
explicit freeze (run after Proc/Map replacement cleaned the graph, so it
|
|
476
|
+
succeeds) makes worker-side HABTM deterministic.
|
|
477
|
+
- **Per-Ractor Concurrent global executors**
|
|
478
|
+
(`_install_concurrent_global_timer_set_patch`): a worker-side
|
|
479
|
+
`set(wait:)` enqueue schedules via `Concurrent::ScheduledTask`, whose default
|
|
480
|
+
timer/notification path reads the unshareable `GLOBAL_TIMER_SET` /
|
|
481
|
+
`GLOBAL_IO_EXECUTOR` / `GLOBAL_FAST_EXECUTOR` constants —
|
|
482
|
+
`IsolationError: can not access non-shareable objects in constant
|
|
483
|
+
Concurrent::GLOBAL_TIMER_SET by non-main ractor`. The readers now give each
|
|
484
|
+
worker its own TimerSet / ThreadPoolExecutor through IES; main keeps the
|
|
485
|
+
global instances.
|
|
486
|
+
|
|
487
|
+
### Fixed — Rails 8.1 `signed_id` / `find_signed` / `token_for` from a worker Ractor
|
|
488
|
+
|
|
489
|
+
`User.find_signed(params[:token])` from a worker Ractor died with
|
|
490
|
+
`NoMethodError: undefined method '[]' for ... NoOpProc`: the signed-id salt
|
|
491
|
+
("active_record/signed_id") builds its verifier codec LAZILY on first use, and
|
|
492
|
+
by the time a worker asks, the coordinator's rotation Proc / secret generator
|
|
493
|
+
have become NoOpProcs (Proc replacement) on a frozen graph. Verified by the
|
|
494
|
+
test app's `GET /features/signed_id_probe` → `generate` / `find_signed` round
|
|
495
|
+
trip from a kino `:ractor` worker.
|
|
496
|
+
|
|
497
|
+
- **Codec pre-warm** (`ShareabilityTraversal.prewarm_message_verifier_codecs!`,
|
|
498
|
+
called from `prepare_for_ractors!` right after the ActiveStorage prefix
|
|
499
|
+
seed): main builds the lazily-created codecs ("active_record/signed_id",
|
|
500
|
+
"active_record/token_for") up front so workers hit the frozen `@codecs`
|
|
501
|
+
cache. The salt list is deliberately method-local — load-time allocations in
|
|
502
|
+
the shim used to perturb the order-sensitive attribute-method generation
|
|
503
|
+
downstream.
|
|
504
|
+
- **`SerializerWithFallback.serializer_for` NameError fix**
|
|
505
|
+
(`patches/active_support.rb`): the patch is string-eval'd on the singleton
|
|
506
|
+
class, where a bare `SERIALIZERS` reference raises NameError (cref ancestors
|
|
507
|
+
exclude the module). Now fully qualified:
|
|
508
|
+
`::ActiveSupport::Messages::SerializerWithFallback::SERIALIZERS.fetch(format)`.
|
|
509
|
+
- **stdlib `JSON` default-option ivars** (`patches/make_shareable.rb`): the
|
|
510
|
+
legacy signed-id rotation passes `serializer: JSON` (stdlib), and JSON
|
|
511
|
+
memoizes `@dump_default_options` / `@load_default_options` / etc. as class
|
|
512
|
+
ivars — workers could not even read them (`IsolationError ... from JSON`).
|
|
513
|
+
Registered in `SHAREABLE_CLASS_IVARS` so they deep-freeze at prepare time.
|
|
514
|
+
- **`MessageVerifier#digest_length_in_hex` memo patch**
|
|
515
|
+
(`patches/active_support.rb`): the lazily memoized `@digest_length_in_hex
|
|
516
|
+
||=` writes to the deep-frozen verifier at first generate/verify —
|
|
517
|
+
`FrozenError: can't modify frozen ActiveSupport::MessageVerifier`. Replaced
|
|
518
|
+
with a direct `OpenSSL::Digest` computation (allocation is negligible next
|
|
519
|
+
to the HMAC itself).
|
|
520
|
+
- **`ActiveRecord` attribute-state robustness** (`patches/active_model_attribute.rb`):
|
|
521
|
+
in MAIN, `attribute_types` / `_default_attributes` no longer memoize through
|
|
522
|
+
`RactorRailsShim.storage` — that memo had no invalidation path in main
|
|
523
|
+
(`reset_column_information` cannot see it), so an empty type-set built
|
|
524
|
+
before a model's schema settled poisoned `attribute_names` permanently.
|
|
525
|
+
Main now uses the upstream class-ivar memo (which every reset path clears);
|
|
526
|
+
workers keep the per-Ractor IES path.
|
|
527
|
+
- **Prefix-seed ordering fix** (`roles/lifecycle.rb`): `_seed_active_storage_prefix!`
|
|
528
|
+
now runs BEFORE `install_framework_patches`. The AR model-classes patch runs
|
|
529
|
+
`load_schema` on every concrete model at install time; with the prefix
|
|
530
|
+
unseeded, ActiveStorage models computed table names like `"attachments"` and
|
|
531
|
+
the failing attempts poisoned memoized attribute state —
|
|
532
|
+
`generate_ar_attribute_methods!` then generated modules missing column
|
|
533
|
+
writers, and workers lost `ActiveStorage::Attachment#name=` (DelegationError
|
|
534
|
+
on `Attachment.new`). Seeding first closes that window.
|
|
535
|
+
- Dispatch list updated in `spec/framework_patch_dispatch_spec.rb` for the new
|
|
536
|
+
`_install_message_verifier_digest_memo_patch`.
|
|
537
|
+
|
|
538
|
+
### Fixed — shim TODO #5: ActiveJob `perform_later` / `deliver_later` from a worker Ractor
|
|
539
|
+
|
|
540
|
+
The last code-level ❌ in `FEATURES.md`. `WelcomeJob.perform_later(user)` from
|
|
541
|
+
a worker Ractor crashed with two different walls; both fixed. Regression specs
|
|
542
|
+
in `spec/cgi_escape_patch_spec.rb`; verified end-to-end by the test app's
|
|
543
|
+
`GET /job_enqueue_probe` → 200 `{enqueued: true, job_class: "WelcomeJob"}`
|
|
544
|
+
from real kino `:ractor` workers.
|
|
545
|
+
|
|
546
|
+
- **`GlobalID.app` captured via `SHAREABLE_CLASS_IVARS`** (`["GlobalID",
|
|
547
|
+
:@app]`, `patches/make_shareable.rb`). The railtie sets it to an *unfrozen*
|
|
548
|
+
String, and workers cannot even READ an unfrozen class ivar —
|
|
549
|
+
`GlobalID.create` (any ActiveJob argument serialization) died with
|
|
550
|
+
`IsolationError: can not get unshareable values from instance variables
|
|
551
|
+
... (@app from GlobalID)` before GlobalID's own "An app is required" check
|
|
552
|
+
even ran. Deep-frozen at prepare time like the other captured ivars.
|
|
553
|
+
- **New `patches/cgi.rb`** — `CGI::Escape`/`CGI::EscapeExt` default their
|
|
554
|
+
`unescape` / `unescapeURIComponent` encoding argument to the
|
|
555
|
+
`@@accept_charset` CLASS VARIABLE (the C ext impl from `cgi/escape.so`
|
|
556
|
+
reads it internally too). Ruby forbids reading class variables from
|
|
557
|
+
non-main Ractors outright — even shareable ones. globalid's URI::GID
|
|
558
|
+
parsing calls `CGI.unescape`, putting this on the enqueue path. Both
|
|
559
|
+
methods are re-defined (string eval, shareable) defaulting to a frozen
|
|
560
|
+
snapshot constant; wired into the Ractor install strategy before eager
|
|
561
|
+
load. Requires no behavior change: explicit-encoding calls keep working,
|
|
562
|
+
and the class variable storage is untouched for main-Ractor callers.
|
|
563
|
+
- **`queue_adapter` needed no shim change**: ActiveJob's reader lazily
|
|
564
|
+
instantiates a per-worker adapter (`self.queue_adapter = :async if
|
|
565
|
+
_queue_adapter.nil?`) through the class_attribute IES writer, which
|
|
566
|
+
already works in workers.
|
|
567
|
+
|
|
568
|
+
### Fixed — worker-Ractor cache access (low-level Rails.cache + fragment caching)
|
|
569
|
+
|
|
570
|
+
Exercising the caching audit in the test app surfaced three isolation walls
|
|
571
|
+
that made ANY cache call (`Rails.cache.fetch`, view fragment caching) die in a
|
|
572
|
+
worker Ractor. All fixed; regression specs in
|
|
573
|
+
`spec/cache_option_aliases_shareable_spec.rb`.
|
|
574
|
+
|
|
575
|
+
- **`ActiveSupport::Cache::OPTION_ALIASES` registered in
|
|
576
|
+
`SHAREABLE_CONSTANTS`** (`patches/active_support.rb`). Rails freezes the
|
|
577
|
+
Hash shallowly — its Array values stay unfrozen — so workers could not even
|
|
578
|
+
READ the constant (`IsolationError` in `Store#normalize_options` →
|
|
579
|
+
`merged_options`), which broke every `fetch`/`read`/`write` with an options
|
|
580
|
+
hash. Deep-frozen at boot like the other read-only alias maps.
|
|
581
|
+
- **`ActiveSupport::Cache::Coder` constants registered** (`PACKED_TYPE_
|
|
582
|
+
TEMPLATE`, `PACKED_EXPIRES_AT_TEMPLATE`, `PACKED_VERSION_LENGTH_TEMPLATE`,
|
|
583
|
+
`STRING_DESERIALIZERS`, `STRING_ENCODINGS`) — read during cache-entry
|
|
584
|
+
deserialization (`Coder#load`); the next wall behind OPTION_ALIASES.
|
|
585
|
+
- **Cache serializer availability probes warmed in main**
|
|
586
|
+
(`_warm_cache_serializer_fallbacks!`, new step in the `AppShareabilizer`
|
|
587
|
+
pipeline between `_warm_attribute_method_patterns!` and
|
|
588
|
+
`_freeze_declared_callbacks!`). The `SerializerWithFallback` fallbacks
|
|
589
|
+
(Rails 8.1: `module ... extend self`) memoize `available?` as an ivar on
|
|
590
|
+
the module object; a worker making the first probe crashed on the ivar
|
|
591
|
+
WRITE (`can not set instance variables of classes/modules by non-main
|
|
592
|
+
Ractors`) during `Coder#load` → `dumped?`. Warming the probes before the
|
|
593
|
+
freeze leaves `defined?(@available)` true with a shareable boolean.
|
|
594
|
+
- **`ActiveSupport::NumberHelper` converter constants registered**
|
|
595
|
+
(`NumberConverter::DEFAULTS`, `NumberToDelimitedConverter::
|
|
596
|
+
DEFAULT_DELIMITER_REGEX`, `NumberToHumanConverter::DECIMAL_UNITS` /
|
|
597
|
+
`INVERTED_DECIMAL_UNITS`, `NumberToHumanSizeConverter::STORAGE_UNITS`) —
|
|
598
|
+
read on every `number_with_delimiter` / `number_to_currency` / human-size
|
|
599
|
+
call; `DEFAULTS` was entirely unfrozen. Needed once views render number
|
|
600
|
+
helpers from worker Ractors.
|
|
601
|
+
|
|
602
|
+
Verified end-to-end: the test app's fragment / russian-doll / low-level
|
|
603
|
+
caching and number-helper views now serve from real kino `:ractor` workers
|
|
604
|
+
(`root_load_test.rb` passes; full suite 111 runs / 0 failures / 0 skips).
|
|
605
|
+
|
|
606
|
+
Verified end-to-end: the test app's kino (`:ractor` kino, production
|
|
607
|
+
semantics) exercises streaming blob proxy serving, worker-side time
|
|
608
|
+
zones, the error reporter, config reads, the public Active Storage
|
|
609
|
+
service, the analyzer chain, and Solid Cache writes with expiry; the
|
|
610
|
+
full test suite is 180 runs / 787 assertions / 0 failures and the shim
|
|
611
|
+
spec suite is 804 runs / 1699 assertions / 0 failures / 4 skips.
|
|
612
|
+
|
|
613
|
+
## [0.4.1]
|
|
614
|
+
|
|
615
|
+
### Fixed — community-reported crash trio (GitHub issues #1, #2, #3)
|
|
616
|
+
|
|
617
|
+
All three were triggered by the same docs gap: thread-mode users calling
|
|
618
|
+
`prepare_for_ractors!` (Ractor-mode-only) because the README presented it
|
|
619
|
+
unconditionally. Two real code defects were also exposed and fixed.
|
|
620
|
+
|
|
621
|
+
- **`prepare_for_ractors!` now skips itself in thread mode** with a loud
|
|
622
|
+
warning (`roles/lifecycle.rb`). It is Ractor-mode-only: it dispatches the
|
|
623
|
+
full per-Ractor patch set and deep-freezes live Rails objects, which
|
|
624
|
+
breaks thread servers (Puma/Falcon/Thin/Webrick). Running it in thread
|
|
625
|
+
mode froze the main Ractor's live `DatabaseConfigurations::HashConfig`
|
|
626
|
+
objects → `FrozenError` in `Migration::CheckPending` on every dev HTTP
|
|
627
|
+
request. (Fixes #2; root cause of the trigger conditions in #1 and #3.)
|
|
628
|
+
|
|
629
|
+
- **`SHAREABLE_AUTOSAVE_REFLECTIONS` registry swap made atomic**
|
|
630
|
+
(`patches/activerecord.rb`). `_rrs_store_autosave_reflection` removed the
|
|
631
|
+
constant before calling `Ractor.make_shareable`, so an unshareable
|
|
632
|
+
reflection (e.g. one capturing a Proc) left the constant permanently
|
|
633
|
+
missing and every later association declaration raised
|
|
634
|
+
`NameError: uninitialized constant SHAREABLE_AUTOSAVE_REFLECTIONS` on
|
|
635
|
+
boot for any app with ActiveStorage attachments. Now compute-then-swap
|
|
636
|
+
(a failure leaves the previous registry intact) plus self-healing (a
|
|
637
|
+
missing constant is re-seeded on the next store). (Fixes #1.)
|
|
638
|
+
|
|
639
|
+
- **`action_methods` nil cache read fixed** in both patched copies
|
|
640
|
+
(`AbstractController::Base` and `AbstractController::UrlFor::ClassMethods`,
|
|
641
|
+
`patches/action_controller.rb`). `instance_variable_defined?` is true for
|
|
642
|
+
defined-but-nil ivars, and `clear_action_methods!` (Rails' invalidation
|
|
643
|
+
hook, hit while a mailer/controller class body is evaluated) sets
|
|
644
|
+
`@action_methods = nil` — so the next call read nil, cached nil, and
|
|
645
|
+
returned it, raising `NoMethodError: undefined method 'include?' for nil`
|
|
646
|
+
in `ActionMailer::Base#respond_to_missing?` (crashed `bin/rails
|
|
647
|
+
db:migrate` during a kamal release phase). (Fixes #3.)
|
|
648
|
+
|
|
649
|
+
### Changed — docs
|
|
650
|
+
- README Install section split into two explicit tracks (Ractor mode with
|
|
651
|
+
kino vs Thread mode with Puma/Falcon/Thin/Webrick), with warnings that
|
|
652
|
+
`prepare_for_ractors!` / `make_app_shareable!` must never be called in
|
|
653
|
+
thread mode.
|
|
654
|
+
|
|
10
655
|
## [0.4.0]
|
|
11
656
|
|
|
12
657
|
### Added — Rails feature support for worker Ractors
|