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 +4 -4
- data/CHANGELOG.md +53 -2
- data/lib/ractor_rails_shim/patches/class_attribute.rb +44 -41
- data/lib/ractor_rails_shim/patches/route_helpers.rb +21 -0
- data/lib/ractor_rails_shim/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f309da49240393f31079209d41e422a4f5fabde1428d344d6300ace7ffc5a94d
|
|
4
|
+
data.tar.gz: d6e330ac46078ff31534d594d5c90f7ae1c739bf765223fda27f03d546350fcb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
26
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|