ractor-rails-shim 0.2.2 → 0.2.4

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: 49ca69607b89c8817a961657ddccd6a3c1a3e87084ef0ec34090b91bce396b22
4
- data.tar.gz: cc35444b2078ea06545412a8c6e0d44dd03d2e07c3c2fd99f7a9e930a6fb897e
3
+ metadata.gz: f309da49240393f31079209d41e422a4f5fabde1428d344d6300ace7ffc5a94d
4
+ data.tar.gz: d6e330ac46078ff31534d594d5c90f7ae1c739bf765223fda27f03d546350fcb
5
5
  SHA512:
6
- metadata.gz: 88badfc12887a028b39500559256df576e7317e2dbb9f9e4ee170f116279724a07aef6d3ab4a2bae848c12e04bbc56eb6ffc5c487a54f400aa60ff8b8ef3b0fe
7
- data.tar.gz: 95751eb76405f35d3fa1b2f53c89f2d459af19e7503156597d282f23a3c7da2ac71d6836dc54d80c7991b1e623d66c30fd72e67001530d4636fc7846177ed89d
6
+ metadata.gz: 147c5795a7bf89b07dc1ebc19503cc12fd37c556d727a05a2c4c776c4bf3822def6c37e289343345fa6e861ecdf9a2ef331b13f69dee377cde97f5364dcd42d5
7
+ data.tar.gz: 3ef1bdce56de6ee989797f780c451f4a56d4ebd6427979a120cc25e90a16d3779e97fe83e2ef9fa48f2e011a7de21bd2df81e2731e77914c2b6d01dd7743460d
data/CHANGELOG.md CHANGED
@@ -11,6 +11,37 @@ 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.4]
15
+
16
+ ### Performance
17
+ - **class_attribute reader (ractor mode) is now allocation-free.** The
18
+ generated `__class_attr_*` accessors previously walked `self.ancestors` and
19
+ built a fresh `Symbol` via string interpolation for *every* ancestor on
20
+ *every* read — the dominant allocation source for GET requests (a Rails class
21
+ has 20–40 ancestors, and class_attribute is read constantly: controller
22
+ filters, view partial paths, AR `strict_loading`, form builder, logger, …).
23
+ In ractor mode the writer already collapses every write to the defining
24
+ owner's single key, so the ancestor walk was dead code. The reader now does a
25
+ single literal-symbol lookup against `IsolatedExecutionState[key]` then
26
+ `SHAREABLE_FALLBACK[key]`, eliminating the per-read `Array` + `Symbol`
27
+ churn. This cuts request allocations substantially and, with them, the
28
+ garbage-collection share of CPU time (was ~33% of CPU on `GET /posts`).
29
+
30
+ ## [0.2.3]
31
+
32
+ ### Fixed
33
+ - **Cold `GET /up` `SystemStackError` in worker Ractors (kino `:ractor`).**
34
+ Rails' `ActionDispatch::Routing::RouteSet#generate_url_helpers` builds a
35
+ module whose `self.included(base)` hook re-dups the module and re-includes
36
+ it while `!base._routes.equal?(@_proxy._routes)`. Under the frozen,
37
+ Ractor-shareable app graph a worker Ractor's controller reports
38
+ `base._routes` as `nil`, so the equality never holds and the hook
39
+ re-includes forever (empty Ruby backtrace, first request only; respawns and
40
+ later requests are fine). The shim's `route_helpers.rb` `generate_url_helpers`
41
+ override now bounds the reinclude to once per base, preserving the
42
+ route-alignment intent without the infinite loop. Verified: cold `GET /up`
43
+ returns 200 in `kino -m ractor`; both `:ractor` and `:threaded` modes clean.
44
+
14
45
  ## [0.2.2]
15
46
 
16
47
  ### Fixed
@@ -22,8 +53,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
53
  `ActiveRecord::ConnectionNotEstablished` on the very first request. Setup is
23
54
  now serialized with a per-Ractor `Thread::Mutex`, and `rebind_constants`
24
55
  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.
56
+ out from under another thread.
57
+ - **Per-thread ActiveRecord connection handler (intermittent
58
+ `ConnectionNotEstablished` on `kino :ractor -w5 -t5`).** `init_worker_ar_connections!`
59
+ stored the per-Ractor `ConnectionHandler` in `ActiveSupport::IsolatedExecutionState`,
60
+ which is **per-thread** (`Thread.attr_accessor`). The init thread set it, but
61
+ the other worker threads in the same Ractor saw `nil` →
62
+ `ConnectionNotEstablished: No connection handler for Ractor X` on a fraction of
63
+ requests. The handler now lives in `Ractor.current` (per-Ractor, shared by all
64
+ of the worker's threads), and `ActiveRecord::Base.connection_handler` reads it
65
+ there first.
66
+ - **Write-path `Ractor::IsolationError` on `redirect_to @post`
67
+ (`kino :ractor -w5 -t5`, `POST /posts`).** `ActiveModel::AttributeMethods::
68
+ ClassMethods#attribute_method_patterns_cache` stored a mutable `Concurrent::Map`
69
+ in a class instance variable (unshareable). Hit via `redirect_to @post` →
70
+ `respond_to?` → `matched_attribute_method`, raising from worker Ractors. The
71
+ cache is now routed through `Ractor.current` (per-Ractor, keyed per class) and
72
+ populated lazily per Ractor — content is deterministic, so per-Ractor
73
+ recomputation is correct.
74
+
75
+ With these three fixes, `kino :ractor (-w5 -t5)` serves `/up`, `GET /posts`,
76
+ and `POST /posts` (authenticated write + 302) with 0 transport failures and 0
77
+ server errors under sustained load.
27
78
 
28
79
  ### Added — ActiveRecord query-path ractor-safety (Blocker 1 deep work)
29
80
  - `RactorRailsShim.worker_ar_init(app)` — a shareable Rack middleware that
@@ -135,50 +135,53 @@ module RactorRailsShim
135
135
  end
136
136
  RUBY
137
137
  end
138
- else
139
- target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
140
- def #{namespaced_name}
141
- self.ancestors.each do |anc|
142
- k = :"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"
143
- v = ActiveSupport::IsolatedExecutionState[k]
144
- return v unless v.nil?
145
- fb = RactorRailsShim::SHAREABLE_FALLBACK[k]
146
- return fb unless fb.nil?
147
- end
148
- RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
149
- end
138
+ else
139
+ # Ractor mode: the writer (below) ALWAYS targets this owner's single
140
+ # `key` (#{key_str}), so there is no per-subclass copy-on-write to
141
+ # resolve by walking ancestors. The original `self.ancestors.each`
142
+ # + per-ancestor Symbol interpolation allocated a fresh Array AND a
143
+ # Symbol per ancestor on EVERY read — the dominant allocation source
144
+ # for GET requests. Replace it with a direct literal-key lookup
145
+ # (zero per-read allocation). The per-Ractor value already lives in
146
+ # ActiveSupport::IsolatedExecutionState[key]; SHAREABLE_FALLBACK[key]
147
+ # is the frozen, shared default built at prepare_for_ractors! time.
148
+ target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
149
+ def #{namespaced_name}
150
+ v = ActiveSupport::IsolatedExecutionState[#{key_str}]
151
+ return v unless v.nil?
152
+ fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
153
+ return fb unless fb.nil?
154
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
155
+ end
150
156
 
151
- def #{namespaced_name}=(new_value)
152
- ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
153
- RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
154
- new_value
155
- end
156
- RUBY
157
+ def #{namespaced_name}=(new_value)
158
+ ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
159
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
160
+ new_value
161
+ end
162
+ RUBY
157
163
 
158
- # When owner is a module's singleton class, the original also
159
- # defines a public reader `def #{name} { value }` on owner directly
160
- # (block-based). Override it with the IES-routed version + fallback.
161
- if owner.singleton_class? && owner.attached_object.is_a?(Module)
162
- owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
163
- def #{name}
164
- self.ancestors.each do |anc|
165
- k = :"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"
166
- v = ActiveSupport::IsolatedExecutionState[k]
167
- return v unless v.nil?
168
- fb = RactorRailsShim::SHAREABLE_FALLBACK[k]
169
- return fb unless fb.nil?
170
- end
171
- RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
172
- end
164
+ # When owner is a module's singleton class, the original also
165
+ # defines a public reader `def #{name} { value }` on owner directly
166
+ # (block-based). Override it with the IES-routed version + fallback.
167
+ if owner.singleton_class? && owner.attached_object.is_a?(Module)
168
+ owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
169
+ def #{name}
170
+ v = ActiveSupport::IsolatedExecutionState[#{key_str}]
171
+ return v unless v.nil?
172
+ fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
173
+ return fb unless fb.nil?
174
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
175
+ end
173
176
 
174
- def #{name}=(new_value)
175
- ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
176
- RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
177
- new_value
178
- end
179
- RUBY
180
- end
181
- end
177
+ def #{name}=(new_value)
178
+ ActiveSupport::IsolatedExecutionState[#{key_str}] = new_value
179
+ RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] = new_value if Ractor.main?
180
+ new_value
181
+ end
182
+ RUBY
183
+ end
184
+ end
182
185
  end
183
186
 
184
187
  # redefine_method is used by `redefine` internally and by other call
@@ -93,6 +93,27 @@ module RactorRailsShim
93
93
  mod = _rrs_orig_generate_url_helpers(supports_path)
94
94
  mod.module_eval("def _routes\n @_routes || ::Rails.application.routes\nend", __FILE__, __LINE__ + 1)
95
95
  mod.module_eval("def _generate_paths_by_default\n " + supports_path.inspect + "\nend", __FILE__, __LINE__ + 1)
96
+
97
+ # Bound the stock `self.included` reinclude (actionpack
98
+ # action_dispatch/routing/route_set.rb). That hook re-dups the module
99
+ # and re-includes it while `!base._routes.equal?(@_proxy._routes)`.
100
+ # Under the frozen, Ractor-shareable app graph a worker Ractor's
101
+ # controller can report `_routes` as nil, so the equality never holds
102
+ # and the reinclude loops forever (SystemStackError on the very first
103
+ # request, e.g. GET /up -> Rails::HealthController). The intent is to
104
+ # re-align `_routes` per base exactly once; bound it to one reinclude
105
+ # per base so the loop can never happen while preserving that intent.
106
+ seen = ::Set.new
107
+ mod.instance_variable_set(:@_rrs_reinclude_seen, seen)
108
+ mod.singleton_class.class_eval do
109
+ alias_method :_rrs_orig_self_included, :included
110
+ def included(base)
111
+ seen = instance_variable_get(:@_rrs_reinclude_seen)
112
+ return if seen.include?(base.object_id)
113
+ seen << base.object_id
114
+ _rrs_orig_self_included(base)
115
+ end
116
+ end
96
117
  mod
97
118
  end
98
119
  RUBY
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RactorRailsShim
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
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.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - dev