ractor-rails-shim 0.2.1 → 0.2.2
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49ca69607b89c8817a961657ddccd6a3c1a3e87084ef0ec34090b91bce396b22
|
|
4
|
+
data.tar.gz: cc35444b2078ea06545412a8c6e0d44dd03d2e07c3c2fd99f7a9e930a6fb897e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88badfc12887a028b39500559256df576e7317e2dbb9f9e4ee170f116279724a07aef6d3ab4a2bae848c12e04bbc56eb6ffc5c487a54f400aa60ff8b8ef3b0fe
|
|
7
|
+
data.tar.gz: 95751eb76405f35d3fa1b2f53c89f2d459af19e7503156597d282f23a3c7da2ac71d6836dc54d80c7991b1e623d66c30fd72e67001530d4636fc7846177ed89d
|
data/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
- Improved the published gem summary and description (gemspec). Metadata-only
|
|
12
12
|
release — no code changes.
|
|
13
13
|
|
|
14
|
+
## [0.2.2]
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Multi-threaded worker race in `WorkerApp#setup_once!` (kino `:ractor`,
|
|
18
|
+
`-wN -tM` with M > 1).** All threads inside a worker Ractor share
|
|
19
|
+
`Ractor.current`, so the previous `Ractor.current[:rrs_worker_ready]` guard
|
|
20
|
+
let multiple threads race through `rebind_constants` +
|
|
21
|
+
`init_worker_ar_connections!`, producing
|
|
22
|
+
`ActiveRecord::ConnectionNotEstablished` on the very first request. Setup is
|
|
23
|
+
now serialized with a per-Ractor `Thread::Mutex`, and `rebind_constants`
|
|
24
|
+
re-fetches each namespace parent so concurrent setup cannot clobber a module
|
|
25
|
+
out from under another thread. `kino :ractor (-w5 -t5)` now serves all
|
|
26
|
+
endpoints with 0 failures.
|
|
27
|
+
|
|
14
28
|
### Added — ActiveRecord query-path ractor-safety (Blocker 1 deep work)
|
|
15
29
|
- `RactorRailsShim.worker_ar_init(app)` — a shareable Rack middleware that
|
|
16
30
|
calls `init_worker_ar_connections!` on each worker's first request. Kino's
|
|
@@ -47,7 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
47
61
|
`TYPE_MAP` (Procs) and `QUOTED_*` `Concurrent::Map` quoting caches.
|
|
48
62
|
These are intrinsically unshareable and require upstream Rails changes
|
|
49
63
|
(shareable callables instead of Procs; per-Ractor quoting caches). See
|
|
50
|
-
|
|
64
|
+
the "deep AR ractor-unsafety" wall in the project notes.
|
|
51
65
|
|
|
52
66
|
## [0.2.0] - 2026-07-09
|
|
53
67
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Patch ActiveRecord's connection handler to work in per-Ractor mode.
|
|
4
4
|
#
|
|
5
|
-
# Blocker 1
|
|
5
|
+
# Blocker 1:
|
|
6
6
|
# ActiveRecord::Base.connection_pool calls
|
|
7
7
|
# default_connection_handler.retrieve_connection_pool(...)
|
|
8
8
|
# at connection_handling.rb:346. The default_connection_handler
|
|
@@ -229,7 +229,14 @@ module RactorRailsShim
|
|
|
229
229
|
return unless defined?(::ActiveRecord::Base)
|
|
230
230
|
|
|
231
231
|
key = :active_record_connection_handler
|
|
232
|
-
|
|
232
|
+
# Store the handler in Ractor-local storage (Ractor.current), NOT in
|
|
233
|
+
# ActiveSupport::IsolatedExecutionState. IES is per-THREAD (it is a
|
|
234
|
+
# Thread.attr_accessor), so a value set on the init thread is invisible
|
|
235
|
+
# to the other worker threads in the same worker Ractor ->
|
|
236
|
+
# ConnectionNotEstablished ("No connection handler for Ractor X").
|
|
237
|
+
# Ractor.current storage is per-Ractor and shared by every thread of the
|
|
238
|
+
# worker, so connection_handler resolves the same handler for all threads.
|
|
239
|
+
existing = Ractor.current[key]
|
|
233
240
|
return if existing
|
|
234
241
|
|
|
235
242
|
# Establish a fresh, per-Ractor connection handler + pool from the
|
|
@@ -256,7 +263,7 @@ module RactorRailsShim
|
|
|
256
263
|
end
|
|
257
264
|
end
|
|
258
265
|
|
|
259
|
-
|
|
266
|
+
Ractor.current[key] = handler
|
|
260
267
|
end
|
|
261
268
|
end
|
|
262
269
|
|
|
@@ -320,6 +327,35 @@ module RactorRailsShim
|
|
|
320
327
|
_share_relation_delegate_caches! if Ractor.main?
|
|
321
328
|
end
|
|
322
329
|
|
|
330
|
+
# ActiveModel::AttributeMethods::ClassMethods#attribute_method_patterns_cache
|
|
331
|
+
# stores a mutable Concurrent::Map in a CLASS instance variable
|
|
332
|
+
# (@attribute_method_patterns_cache). That ivar is unshareable, so reading
|
|
333
|
+
# it from a worker Ractor raises Ractor::IsolationError ("can not get
|
|
334
|
+
# unshareable values from instance variables of classes/modules from
|
|
335
|
+
# non-main Ractors") — hit on the write path via redirect_to @post ->
|
|
336
|
+
# respond_to? -> matched_attribute_method -> attribute_method_patterns_cache.
|
|
337
|
+
#
|
|
338
|
+
# Unlike @relation_delegate_cache (populated once, freezable), this map is
|
|
339
|
+
# mutated lazily per method_name (compute_if_absent) during request handling,
|
|
340
|
+
# so it cannot be frozen. Instead route it through Ractor-local storage:
|
|
341
|
+
# each Ractor gets its own Concurrent::Map, shared by all of its threads.
|
|
342
|
+
# The cache content is deterministic (a pure function of the class's
|
|
343
|
+
# attribute_method_patterns), so per-Ractor recomputation is correct.
|
|
344
|
+
def _install_active_model_attribute_method_patterns_patch
|
|
345
|
+
return if @am_amp_patched
|
|
346
|
+
@am_amp_patched = true
|
|
347
|
+
_register_patch :active_model_attribute_method_patterns, "8.1"
|
|
348
|
+
return unless defined?(::ActiveModel::AttributeMethods)
|
|
349
|
+
|
|
350
|
+
mod = ::ActiveModel::AttributeMethods::ClassMethods
|
|
351
|
+
mod.module_eval do
|
|
352
|
+
def attribute_method_patterns_cache
|
|
353
|
+
store = Ractor.current[:__am_attribute_method_patterns_cache__] ||= {}
|
|
354
|
+
store[object_id] ||= Concurrent::Map.new(initial_capacity: 4)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
323
359
|
# Make every loaded AR model class's @relation_delegate_cache shareable.
|
|
324
360
|
# Idempotent; must run in the main Ractor after eager_load so that all
|
|
325
361
|
# model classes (and their caches) exist.
|
|
@@ -1310,6 +1346,19 @@ module RactorRailsShim
|
|
|
1310
1346
|
def default_connection_handler=(val)
|
|
1311
1347
|
ActiveSupport::IsolatedExecutionState[#{dch_key_str}] = val
|
|
1312
1348
|
end
|
|
1349
|
+
# Route the per-Ractor handler through Ractor-local storage. IES is
|
|
1350
|
+
# per-thread, so a handler stored on the init thread is invisible to the
|
|
1351
|
+
# worker's other threads; Ractor.current is per-Ractor and shared by all
|
|
1352
|
+
# threads of the worker. Falls back to IES (legacy) then
|
|
1353
|
+
# default_connection_handler (main Ractor only).
|
|
1354
|
+
def connection_handler
|
|
1355
|
+
v = Ractor.current[:active_record_connection_handler]
|
|
1356
|
+
return v unless v.nil?
|
|
1357
|
+
ActiveSupport::IsolatedExecutionState[:active_record_connection_handler] || default_connection_handler
|
|
1358
|
+
end
|
|
1359
|
+
def connection_handler=(handler)
|
|
1360
|
+
Ractor.current[:active_record_connection_handler] = handler
|
|
1361
|
+
end
|
|
1313
1362
|
RUBY
|
|
1314
1363
|
|
|
1315
1364
|
# Also ensure connection_handler (which Rails already routes through IES
|
|
@@ -377,6 +377,7 @@ module RactorRailsShim
|
|
|
377
377
|
_install_activerecord_primary_key_patch
|
|
378
378
|
_install_activerecord_query_constraints_patch
|
|
379
379
|
_install_activerecord_relation_delegate_cache_patch
|
|
380
|
+
_install_active_model_attribute_method_patterns_patch
|
|
380
381
|
_install_activerecord_model_classes_patch
|
|
381
382
|
_install_active_model_naming_patch
|
|
382
383
|
_install_active_record_core_patch
|
|
@@ -781,10 +782,18 @@ module RactorRailsShim
|
|
|
781
782
|
private
|
|
782
783
|
|
|
783
784
|
def setup_once!
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
785
|
+
# All threads inside a worker Ractor share Ractor.current, so a single
|
|
786
|
+
# per-Ractor mutex serializes the one-time init across the worker's
|
|
787
|
+
# threads. (The mutex is created under ||= which is racy under extreme
|
|
788
|
+
# contention, but both init steps below are themselves idempotent, so a
|
|
789
|
+
# rare double-init is harmless.)
|
|
790
|
+
m = Ractor.current[:rrs_worker_mutex] ||= Thread::Mutex.new
|
|
791
|
+
m.synchronize do
|
|
792
|
+
return if Ractor.current[:rrs_worker_ready]
|
|
793
|
+
rebind_constants
|
|
794
|
+
RactorRailsShim.init_worker_ar_connections! if defined?(RactorRailsShim)
|
|
795
|
+
Ractor.current[:rrs_worker_ready] = true
|
|
796
|
+
end
|
|
788
797
|
end
|
|
789
798
|
|
|
790
799
|
def rebind_constants
|
|
@@ -792,6 +801,9 @@ module RactorRailsShim
|
|
|
792
801
|
parent = Object
|
|
793
802
|
parts = cpath.split("::")
|
|
794
803
|
parts[0...-1].each do |p|
|
|
804
|
+
# Re-fetch the parent each iteration so concurrent setup (multiple
|
|
805
|
+
# threads racing through setup_once! on the same worker) cannot
|
|
806
|
+
# clobber a namespace module out from under us.
|
|
795
807
|
parent = if parent.const_defined?(p, false)
|
|
796
808
|
parent.const_get(p, false)
|
|
797
809
|
else
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Patch Kaminari::config to not read the @_config class ivar from a worker
|
|
4
4
|
# Ractor.
|
|
5
5
|
#
|
|
6
|
-
# Blocker 2
|
|
6
|
+
# Blocker 2:
|
|
7
7
|
# Kaminari.config reads @_config directly at kaminari/config.rb:14:
|
|
8
8
|
# def self.config; @_config ||= Kaminari::Configuration.new end
|
|
9
9
|
# Not mattr_accessor-backed, so the shim's mattr rewrite doesn't catch it.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ractor-rails-shim
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev
|
|
@@ -106,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
106
|
- !ruby/object:Gem::Version
|
|
107
107
|
version: '0'
|
|
108
108
|
requirements: []
|
|
109
|
-
rubygems_version: 4.0.
|
|
109
|
+
rubygems_version: 4.0.16
|
|
110
110
|
specification_version: 4
|
|
111
111
|
summary: Make Rails apps Ractor-safe so they can run in Ractor mode (e.g. under the
|
|
112
112
|
kino web server).
|