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: 515076a823c0767ce28d1bf35545a6ced10f2ef1f628f8e263cde654b58fb899
4
- data.tar.gz: df743b8da60645f80569b646301d7d0fd5c197756ffb928921144f7fac161c5e
3
+ metadata.gz: 49ca69607b89c8817a961657ddccd6a3c1a3e87084ef0ec34090b91bce396b22
4
+ data.tar.gz: cc35444b2078ea06545412a8c6e0d44dd03d2e07c3c2fd99f7a9e930a6fb897e
5
5
  SHA512:
6
- metadata.gz: c4c6fc83fc7fc61a0fecf603e26e2031d2876f20222c29717cc267fc99acfd7ed4e3e05cd98995099a62807bc37e37f34635c676c334caf2c99cc8d4acc50c0d
7
- data.tar.gz: 53bb7cf546753b3bc7c6dc7313dc9b0bbf86b616f1e8f7fc1f0f45d0428988efeee0308bf93c7cf127e9219ebcd747f76340a86b750853a5fad81eaae5da4a0e
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
- `NEXT_STEPS.md` "The wall — deep AR ractor-unsafety".
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 (from NEXT_STEPS.md):
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
- existing = ActiveSupport::IsolatedExecutionState[key]
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
- ActiveSupport::IsolatedExecutionState[key] = handler
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
- return if Ractor.current[:rrs_worker_ready]
785
- rebind_constants
786
- RactorRailsShim.init_worker_ar_connections! if defined?(RactorRailsShim)
787
- Ractor.current[:rrs_worker_ready] = true
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 (from NEXT_STEPS.md):
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RactorRailsShim
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
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.1
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.10
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).