ractor-rails-shim 0.2.6 → 0.3.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 +157 -0
- data/README.md +113 -39
- data/lib/ractor_rails_shim/foundation/const_reassign.rb +26 -0
- data/lib/ractor_rails_shim/foundation/funnel.rb +47 -0
- data/lib/ractor_rails_shim/foundation/ies_accessor.rb +58 -0
- data/lib/ractor_rails_shim/foundation/registry.rb +77 -0
- data/lib/ractor_rails_shim/foundation/role_defaults.rb +58 -0
- data/lib/ractor_rails_shim/foundation/run_mode.rb +76 -0
- data/lib/ractor_rails_shim/foundation/storage.rb +99 -0
- data/lib/ractor_rails_shim/foundation/storage_strategy.rb +173 -0
- data/lib/ractor_rails_shim/foundation/version_policy.rb +147 -0
- data/lib/ractor_rails_shim/loader.rb +79 -0
- data/lib/ractor_rails_shim/patches/action_controller.rb +9 -9
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +85 -30
- data/lib/ractor_rails_shim/patches/action_view.rb +27 -27
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +62 -5
- data/lib/ractor_rails_shim/patches/active_record_model_schema.rb +5 -5
- data/lib/ractor_rails_shim/patches/active_support.rb +49 -587
- data/lib/ractor_rails_shim/patches/activerecord.rb +97 -127
- data/lib/ractor_rails_shim/patches/callables.rb +209 -0
- data/lib/ractor_rails_shim/patches/class_attribute.rb +51 -101
- data/lib/ractor_rails_shim/patches/core.rb +160 -705
- data/lib/ractor_rails_shim/patches/devise.rb +11 -6
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +3 -3
- data/lib/ractor_rails_shim/patches/hash_compute_if_absent.rb +98 -0
- data/lib/ractor_rails_shim/patches/i18n.rb +455 -0
- data/lib/ractor_rails_shim/patches/kaminari.rb +6 -11
- data/lib/ractor_rails_shim/patches/make_shareable.rb +21 -907
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +49 -33
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +3 -3
- data/lib/ractor_rails_shim/patches/propshaft.rb +4 -4
- data/lib/ractor_rails_shim/patches/rack.rb +15 -15
- data/lib/ractor_rails_shim/patches/rails_module.rb +24 -24
- data/lib/ractor_rails_shim/patches/route_helpers.rb +4 -4
- data/lib/ractor_rails_shim/patches/url_helpers.rb +2 -2
- data/lib/ractor_rails_shim/patches/warden.rb +2 -2
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +4 -4
- data/lib/ractor_rails_shim/patches.rb +4 -64
- data/lib/ractor_rails_shim/roles/app_shareabilizer.rb +207 -0
- data/lib/ractor_rails_shim/roles/ar_model_walker.rb +93 -0
- data/lib/ractor_rails_shim/roles/callback_capture.rb +226 -0
- data/lib/ractor_rails_shim/roles/check.rb +210 -0
- data/lib/ractor_rails_shim/roles/constant_shareabilizer.rb +217 -0
- data/lib/ractor_rails_shim/roles/fallback_builder.rb +333 -0
- data/lib/ractor_rails_shim/roles/fallback_ies.rb +25 -0
- data/lib/ractor_rails_shim/roles/freezers.rb +341 -0
- data/lib/ractor_rails_shim/roles/install_strategy.rb +68 -0
- data/lib/ractor_rails_shim/roles/installer.rb +89 -0
- data/lib/ractor_rails_shim/roles/lifecycle.rb +39 -0
- data/lib/ractor_rails_shim/roles/logger_io_neutralizer.rb +146 -0
- data/lib/ractor_rails_shim/roles/pre_spawn_steps.rb +36 -0
- data/lib/ractor_rails_shim/roles/shareability_traversal.rb +488 -0
- data/lib/ractor_rails_shim/roles/worker_app.rb +77 -0
- data/lib/ractor_rails_shim/roles/worker_app_factory.rb +124 -0
- data/lib/ractor_rails_shim/version.rb +1 -1
- data/lib/ractor_rails_shim.rb +1 -2
- metadata +33 -6
- data/lib/ractor_rails_shim/check.rb +0 -200
- data/lib/ractor_rails_shim/fallback_ies.rb +0 -33
- /data/lib/ractor_rails_shim/{version_check.rb → foundation/version_check.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ebcbda319623aa7003caf170774074e19fdf2d6452ed086fbc5edbfec037fa5
|
|
4
|
+
data.tar.gz: 7ab60400652444ccbc5ca39ac403d387c156cc8a6178700f10c9d744db9c3fad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69433fe039a2e2bea0302e13117a0ba701b786eaaec6e2e636d1d4786d74428e11701db2c703bc6fdb10c698febb6d5bce646adba24011513802d76de237349f
|
|
7
|
+
data.tar.gz: 615e99053521f65e7aa013d8a5c23e97991bd8b3f8593a92c9c2cf57e2ab245796bde664731ac943fe6b52681a8416c4bddac9635a7c5be6b2c3cc6339ecbdd2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,163 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0]
|
|
11
|
+
|
|
12
|
+
### Changed — breaking (public API)
|
|
13
|
+
- **`worker_app` renamed to `worker_app!`.** The factory freezes +
|
|
14
|
+
`Ractor.make_shareable`s the `WorkerApp` — a destructive build — so it
|
|
15
|
+
now carries the bang the naming convention reserves for "mutates /
|
|
16
|
+
produces a frozen shareable." Callers following the README's
|
|
17
|
+
`Ractor.new(worker_app) { |a| a.call(env) }` example must update to
|
|
18
|
+
`worker_app!`. The old name is gone. (`patches/core.rb`)
|
|
19
|
+
|
|
20
|
+
- **`fix_url_helpers_singleton_routes` renamed to
|
|
21
|
+
`fix_url_helpers_singleton_routes!`.** Mutates the global routes
|
|
22
|
+
singleton; now bang-suffixed per the convention documented at the top
|
|
23
|
+
of `patches.rb`. (`patches/route_helpers.rb`)
|
|
24
|
+
|
|
25
|
+
- **Tightened `activesupport` dependency to `>= 8.1`.** The gemspec
|
|
26
|
+
previously declared `>= 7.0`, but the shim only supports Rails 8.1
|
|
27
|
+
(per `Version::TESTED_RAILS` / `SUPPORTED_RAILS`). The looser bound let
|
|
28
|
+
Bundler resolve against AS 7.x where the class-layout patches
|
|
29
|
+
(`class_attribute`, `Callbacks`, `PathRegistry`, …) would silently miss
|
|
30
|
+
blockers or redefine the wrong methods. Bound at `>= 8.1` so Bundler
|
|
31
|
+
fails fast on an unsupported Rails. (`ractor-rails-shim.gemspec`)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **`_apply_shareable_constants!` set the done-flag on undefined
|
|
35
|
+
constants.** It set `@shareable_constants_done = true` after the first
|
|
36
|
+
run unconditionally, even when registered constants didn't exist yet
|
|
37
|
+
(`make_constant_shareable` returned `false`). A later call (from
|
|
38
|
+
`make_app_shareable!` or `prepare_for_ractors!`) short-circuited on the
|
|
39
|
+
flag and never retried the now-loadable constants — workers then hit
|
|
40
|
+
`Ractor::IsolationError` on unshareable values (e.g.
|
|
41
|
+
`Rack::Utils::PATH_SEPS`, a Regexp not deep-frozen until
|
|
42
|
+
`make_shareable` runs on it). The flag now sets only when every
|
|
43
|
+
registered constant was made shareable; any `false` return leaves it
|
|
44
|
+
unset so the next call retries. Caught by the integration spec (boots
|
|
45
|
+
a real Rails 8.1 app, dispatches `GET /up` in a worker Ractor); pinned
|
|
46
|
+
by `apply_shareable_constants_retry_spec.rb`.
|
|
47
|
+
|
|
48
|
+
- **ActionFilter private ivar reads now gated through the `_swallow`
|
|
49
|
+
debug funnel.** The eval'd `set_callback` interceptor inlined
|
|
50
|
+
`af.instance_variable_get(:@conditional_key) rescue nil`, but
|
|
51
|
+
`instance_variable_get` returns nil for a missing ivar *without*
|
|
52
|
+
raising — the rescue never fired, and a silent Rails internal rename
|
|
53
|
+
(`@conditional_key` / `@actions`) would leave `only`/`except` nil,
|
|
54
|
+
making callbacks run for actions they shouldn't (security-relevant)
|
|
55
|
+
with no visible cause. New `_read_action_filter_constraints(af)` checks
|
|
56
|
+
`instance_variable_defined?` and emits a labeled
|
|
57
|
+
`[ractor_rails_shim] action filter constraints: missing ivar …`
|
|
58
|
+
warning under `debug=true`. Returns `[nil, nil]` for a bare object,
|
|
59
|
+
`[key, symbols]` when the ivars are present.
|
|
60
|
+
|
|
61
|
+
- **Dead ternary in thread-mode `class_attribute` reader.** The reader
|
|
62
|
+
carried `:__class_attr___callbacks == :__callbacks ? {} : nil`, but
|
|
63
|
+
the namespaced name is always `__class_attr_<name>` (never `<name>`),
|
|
64
|
+
so the check was always false — dead code that allocated a `Symbol`
|
|
65
|
+
via `inspect` on every read AND never delivered the intended `{}`
|
|
66
|
+
default for `__callbacks` (callers index the result, so `nil` would
|
|
67
|
+
`NoMethodError`). Replaced with a static decision based on the public
|
|
68
|
+
name, inlining the shared frozen `EMPTY_CALLBACKS_HASH` constant.
|
|
69
|
+
|
|
70
|
+
- **Redundant `class_variable_set` pair in `mattr_accessor` writer.**
|
|
71
|
+
The shim-generated writer had two complementary guarded lines
|
|
72
|
+
(`set if defined?` + `set unless defined?`) that together were one
|
|
73
|
+
unconditional set, with a wasted `class_variable_defined?` per write.
|
|
74
|
+
Replaced with a single `class_variable_set(cv, val) if Ractor.main?`.
|
|
75
|
+
|
|
76
|
+
- **`WorkerApp#setup_once!` race-spec cleanup leak.** The race spec's
|
|
77
|
+
`ensure` block restored `Thread::Mutex.new` via
|
|
78
|
+
`define_method { orig_new.call }`, leaving a block that captures a
|
|
79
|
+
main-Ractor-bound `Method`. When a later spec's worker Ractor called
|
|
80
|
+
`Thread::Mutex.new` it raised `IsolationError` (flaky
|
|
81
|
+
`WorkerAppSpec#test_0002`). Replaced with `remove_method(:new)` so the
|
|
82
|
+
call falls back to the class-defined `new` (no captured binding).
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- **Centralized `const_set`-with-suppressed-`$VERBOSE` into
|
|
86
|
+
`_reassign_shareable_const(name, value)`.** `SHAREABLE_FALLBACK`,
|
|
87
|
+
`SHAREABLE_MATTR_DEFAULTS`, `SHAREABLE_APP`, and
|
|
88
|
+
`SHAREABLE_DECLARED_CALLBACKS` were each rebuilt + reassigned via an
|
|
89
|
+
inlined `$VERBOSE = nil … const_set … ensure $VERBOSE = …` dance. The
|
|
90
|
+
dance now lives in one helper on `RactorRailsShim`; all rebuild sites
|
|
91
|
+
(including the 6 `activerecord.rb` sites + 1 `kaminari.rb` site) call
|
|
92
|
+
it. The constants stay frozen shareable `Hash`es (always readable from
|
|
93
|
+
any Ractor) — a mutable-then-frozen registry would not be shareable
|
|
94
|
+
until `freeze!`, breaking workers that read early.
|
|
95
|
+
|
|
96
|
+
- **Callable/lock object model extracted to `patches/callables.rb`.**
|
|
97
|
+
`NoOpProc` / `Callable` / `CallableConst` / `DeviseMappingSnapshot` /
|
|
98
|
+
`NoOpLock` / `NoOpLogDev` + the `_devise_mapping_snapshot` helper moved
|
|
99
|
+
out of the 964-line `make_shareable.rb` into their own file. Plain
|
|
100
|
+
class definitions replace the string-eval indirection (the eval was
|
|
101
|
+
stylistic, not behavioral). `make_shareable.rb`: 964 → 822 lines.
|
|
102
|
+
|
|
103
|
+
- **`VersionPolicy` module extracted from the `RactorRailsShim` god
|
|
104
|
+
module.** The version-policy + patch-registry concern (`:warn` /
|
|
105
|
+
`:strict` / `:off` switch, `PATCH_VERSIONS`, `_register_patch` /
|
|
106
|
+
`applicable_patches` / `_version_mismatch`) moved from the
|
|
107
|
+
`RactorRailsShim` singleton in `core.rb` to
|
|
108
|
+
`RactorRailsShim::VersionPolicy` (`version_policy.rb`). `core.rb` keeps
|
|
109
|
+
the public facade (`RactorRailsShim.version_policy`, `.applicable_patches`,
|
|
110
|
+
`::PATCH_VERSIONS`, `::UnsupportedVersionError`) delegating to the
|
|
111
|
+
module. Orthogonal to the already-extracted `RactorRailsShim::Version`
|
|
112
|
+
(detection).
|
|
113
|
+
|
|
114
|
+
- **`mattr_accessor` split into single-responsibility helpers.** The
|
|
115
|
+
per-symbol body did four things (call super, push to `CLASS_ATTRIBUTES`,
|
|
116
|
+
seed the default + rebuild the shareable constant, redefine the
|
|
117
|
+
reader/writer). Extracted `_seed_mattr_default(key, default)` and
|
|
118
|
+
`_register_for_fallback(mod_name, sym, key, default)`; `mattr_accessor`
|
|
119
|
+
now reads `super, register, seed, redefine`.
|
|
120
|
+
|
|
121
|
+
- **Thread-mode vs Ractor-mode `class_attribute` heredocs de-duplicated.**
|
|
122
|
+
Each branch inlined a near-identical ~25-line reader/writer heredoc,
|
|
123
|
+
differing only in the method name. Extracted
|
|
124
|
+
`_class_attr_thread_methods` / `_class_attr_ractor_methods` (each builds
|
|
125
|
+
one reader+writer pair); `redefine` calls the helper twice. One source
|
|
126
|
+
of truth per mode. Also fixed a misaligned `else` (12-space indent in a
|
|
127
|
+
10-space block). `class_attribute.rb`: 232 → 224 lines.
|
|
128
|
+
|
|
129
|
+
- **Freeze-path bare rescues routed through the `_swallow` debug funnel.**
|
|
130
|
+
`_freeze_shareable_class_ivars!`, `_freeze_declared_callbacks!`,
|
|
131
|
+
`_collect_controller_classes`, `_replace_locks_and_concurrent_maps!`,
|
|
132
|
+
`_replace_one_proc`, `_neutralize_logger_io!`,
|
|
133
|
+
`DeviseMappingSnapshot`, `_devise_mapping_snapshot`, `_warm_journey_routes`,
|
|
134
|
+
url-helper freeze, `DUMMY_END_NODE`, `MimeNegotiation`, AR db-config
|
|
135
|
+
handlers, AR query transformers, devise mappings, and the view-context
|
|
136
|
+
fallback now emit a labeled `[ractor_rails_shim] <label>: …` line to
|
|
137
|
+
`$stderr` under `debug=true` instead of bare `rescue; nil`. Silent by
|
|
138
|
+
default (backward compatible).
|
|
139
|
+
|
|
140
|
+
- **`fallback_ies.rb` moved into the `RactorRailsShim` namespace.** It
|
|
141
|
+
previously defined `ActiveSupport::IsolatedExecutionState` directly
|
|
142
|
+
inside the `ActiveSupport` namespace (guarded by `defined?`). Defining
|
|
143
|
+
an upstream-namespaced constant from a third-party gem is a
|
|
144
|
+
namespace-patch smell — if a future AS lazy-loads IES, load order
|
|
145
|
+
decides which definition wins. The fallback now lives at
|
|
146
|
+
`RactorRailsShim::FallbackIES` and is aliased onto
|
|
147
|
+
`ActiveSupport::IsolatedExecutionState` only when the real AS IES is
|
|
148
|
+
absent. The ~270 string-eval'd references across the patch files
|
|
149
|
+
resolve to the alias when AS is absent and to the real one when present.
|
|
150
|
+
|
|
151
|
+
- **Stream-of-consciousness comments trimmed to one-line invariants.**
|
|
152
|
+
Working-notes-style comments that narrated history ("actually it IS a
|
|
153
|
+
constant holding…", "Pre-fix the traversals only handled…") replaced
|
|
154
|
+
with one-line invariant statements. The history is in git.
|
|
155
|
+
|
|
156
|
+
### Documentation
|
|
157
|
+
- **`WorkerApp#setup_once!` race comment corrected (again).** A prior
|
|
158
|
+
0.2.6 entry claimed `Ractor.current[:key] ||= Thread::Mutex.new` is
|
|
159
|
+
atomic in Ruby 4.0.6. It is NOT — `||=` is a read-then-write, and N
|
|
160
|
+
racing threads produce N distinct mutexes (verified by spec under a
|
|
161
|
+
widened window). The code is saved by (1) MRI's GIL serializing the
|
|
162
|
+
flag check inside `synchronize` and (2) `rebind_constants` /
|
|
163
|
+
`init_worker_ar_connections!` both being idempotent. Comment now states
|
|
164
|
+
the real contract + the escape hatch if either property changes. Two
|
|
165
|
+
specs pin the idempotency contract.
|
|
166
|
+
|
|
10
167
|
## [0.2.6]
|
|
11
168
|
|
|
12
169
|
### Fixed
|
data/README.md
CHANGED
|
@@ -64,49 +64,123 @@ kino `:ractor` vs Puma vs Falcon are documented in
|
|
|
64
64
|
## Repository layout
|
|
65
65
|
|
|
66
66
|
```
|
|
67
|
-
lib/ractor_rails_shim.rb
|
|
67
|
+
lib/ractor_rails_shim.rb # entry point — autoload-install if Rails is loaded
|
|
68
68
|
lib/ractor_rails_shim/
|
|
69
|
-
version.rb
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
69
|
+
version.rb # VERSION constant (currently 0.3.0)
|
|
70
|
+
loader.rb # pure require hub — one file, one job (POODR §1)
|
|
71
|
+
patches.rb # backward-compat redirect → loader.rb
|
|
72
|
+
|
|
73
|
+
foundation/ # Layer 2: no internal deps
|
|
74
|
+
funnel.rb # debug-aware exception-swallowing role
|
|
75
|
+
registry.rb # canonical home for the nine shared registries
|
|
76
|
+
storage.rb # pluggable key-value store contract (IES / ThreadLocal)
|
|
77
|
+
storage_strategy.rb # composed Ractor/Thread strategy for class_attribute
|
|
78
|
+
ies_accessor.rb # 3-tier IES lookup pattern generator
|
|
79
|
+
const_reassign.rb # $VERBOSE-suppressed const_set utility
|
|
80
|
+
version_check.rb # Gem::Version-based Ruby/Rails detection
|
|
81
|
+
version_policy.rb # mismatch policy (:warn/:strict/:off) + patch registry
|
|
82
|
+
run_mode.rb # thread vs Ractor mode decision
|
|
83
|
+
role_defaults.rb # DRY mixin for shared default-proc patterns
|
|
84
|
+
|
|
85
|
+
roles/ # Layer 3: depend on foundation
|
|
86
|
+
lifecycle.rb # prepare_for_ractors! orchestration
|
|
87
|
+
installer.rb # install orchestrator + framework-patch dispatcher
|
|
88
|
+
install_strategy.rb # per-mode install bodies (Ractor / Thread)
|
|
89
|
+
pre_spawn_steps.rb # shared orchestration steps
|
|
90
|
+
ar_model_walker.rb # ActiveRecord model enumeration
|
|
91
|
+
constant_shareabilizer.rb # constant shareability
|
|
92
|
+
shareability_traversal.rb # app-graph traversal
|
|
93
|
+
callback_capture.rb # callback-declaration capture
|
|
94
|
+
fallback_builder.rb # shareable-fallback builder
|
|
95
|
+
worker_app.rb # shareable Rack wrapper (per-worker init)
|
|
96
|
+
worker_app_factory.rb # shareable-Rack-app factory
|
|
97
|
+
app_shareabilizer.rb # make_app_shareable! orchestrator
|
|
98
|
+
freezers.rb # freeze/warm sub-domain (6 freezer roles)
|
|
99
|
+
logger_io_neutralizer.rb # logger IO detachment
|
|
100
|
+
fallback_ies.rb # backward-compat alias → Storage::ThreadLocal
|
|
101
|
+
check.rb # ractor-rails-check audit (Check.scan / report)
|
|
102
|
+
|
|
103
|
+
patches/ # Layer 4: monkey-patches (depend on roles + foundation)
|
|
104
|
+
core.rb # module skeleton, registries, facade delegations
|
|
105
|
+
callables.rb # NoOpProc, Callable, CallableConst, RequestCallable, …
|
|
106
|
+
make_shareable.rb # make_app_shareable!, proc/lock replacement
|
|
107
|
+
rails_module.rb # Rails.application / Rails.cache / Rails.logger / Rails.env …
|
|
108
|
+
mattr_accessor.rb # Module#mattr_accessor / cattr_accessor macro rewrite
|
|
109
|
+
class_attribute.rb # ActiveSupport class_attribute macro rewrite
|
|
110
|
+
zeitwerk_registry.rb # Zeitwerk::Registry class ivars
|
|
111
|
+
route_helpers.rb # ActionDispatch::Routing::RouteSet#generate_url_helpers
|
|
112
|
+
url_helpers.rb # URL helper singleton + module fixes
|
|
113
|
+
execution_wrapper.rb # ActiveSupport::ExecutionWrapper + callback replay
|
|
114
|
+
rack.rb # Rack::Request / Rack::Utils
|
|
115
|
+
action_view.rb # PathRegistry, LookupContext, Template handlers
|
|
116
|
+
action_controller.rb # AbstractController, ActionController name/encoding
|
|
117
|
+
action_dispatch.rb # ActionDispatch routing, http_url, journey
|
|
118
|
+
polymorphic_routes.rb # polymorphic_path(s) URL helpers
|
|
119
|
+
active_support.rb # Inflector, error_reporter, ExecutionContext, I18n, JSON
|
|
120
|
+
i18n.rb # I18n locale/fallback patches
|
|
121
|
+
warden.rb # Warden hooks / strategies / serializer
|
|
122
|
+
devise.rb # Devise url_helpers / authenticatable / failure_app
|
|
123
|
+
active_model_attribute.rb # ActiveModel::Attribute dup_or_share for frozen graphs
|
|
124
|
+
active_record_model_schema.rb # AR ModelSchema reload_schema_from_cache
|
|
125
|
+
activerecord.rb # AR connection handler, configurations, query caches
|
|
126
|
+
kaminari.rb # Kaminari config
|
|
127
|
+
propshaft.rb # Propshaft asset server
|
|
128
|
+
orm_adapter.rb # orm_adapter
|
|
129
|
+
openssl.rb # OpenSSL digest cache
|
|
130
|
+
rubygems.rb # Rubygems msgpack pre-check
|
|
131
|
+
hash_compute_if_absent.rb # Hash#compute_if_absent for Concurrent::Map replacement
|
|
132
|
+
|
|
133
|
+
exe/ractor-rails-check # CLI audit tool
|
|
134
|
+
spec/ # 724 unit tests (no Rails dep) + integration spec
|
|
135
|
+
script/make_test_app.sh # build the minimal Rails 8.1 test app (CI uses this)
|
|
136
|
+
script/make_full_test_app.sh # build the full-featured test app (Devise/PG/Kaminari)
|
|
137
|
+
.github/workflows/ci.yml # unit job + integration job (GET /up → 200 in a worker Ractor)
|
|
105
138
|
```
|
|
106
139
|
|
|
140
|
+
### Architecture: layered, POODR-aligned design
|
|
141
|
+
|
|
142
|
+
The directory structure mirrors the dependency layers:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Layer 1: Entry ractor_rails_shim.rb, loader.rb
|
|
146
|
+
Layer 2: Foundation foundation/ (no internal deps)
|
|
147
|
+
Layer 3: Roles roles/ (depend on foundation)
|
|
148
|
+
Layer 4: Patches patches/ (depend on roles + foundation)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The gem follows **Practical Object-Oriented Design in Ruby** (POODR) principles:
|
|
152
|
+
|
|
153
|
+
- **SRP (§1):** Every file defines exactly one concern. `loader.rb` is a pure
|
|
154
|
+
require hub (no logic). `lifecycle.rb` owns `prepare_for_ractors!` orchestration.
|
|
155
|
+
Role objects (`Funnel`, `Registry`, `CallbackCapture`, etc.) each own one
|
|
156
|
+
responsibility.
|
|
157
|
+
|
|
158
|
+
- **Dependencies (§2):** Role objects receive collaborators via `configure` seams
|
|
159
|
+
(keyword arguments), defaulting to facade lookups. No role reaches past its
|
|
160
|
+
own namespace by name — the composition root (`Installer`) is the one place
|
|
161
|
+
allowed to resolve by name.
|
|
162
|
+
|
|
163
|
+
- **Interfaces (§3):** The `Storage` contract (`[]`, `[]=`, `key?`, `delete`)
|
|
164
|
+
is implemented by `Storage::IES` and `Storage::ThreadLocal`. The
|
|
165
|
+
`StorageStrategy` contract (`lookup`, `store`, `replay_callbacks?`,
|
|
166
|
+
`replay_callbacks!`) is implemented by `StorageStrategy::Ractor` and
|
|
167
|
+
`StorageStrategy::Thread`.
|
|
168
|
+
|
|
169
|
+
- **Duck Typing (§4):** `ShareabilityTraversal` uses a `CONTAINER_WALKERS`
|
|
170
|
+
dispatch table (Hash → lambda) instead of `is_a?` chains. Lock detection
|
|
171
|
+
uses `respond_to?(:synchronize)` instead of `is_a?(Mutex)`.
|
|
172
|
+
|
|
173
|
+
- **Composition (§5):** `AppShareabilizer` composes 15 collaborators via the
|
|
174
|
+
configure seam. `FallbackBuilder` uses a `ValueLookup` chain of responsibility.
|
|
175
|
+
`Installer` selects between `InstallStrategy::Ractor` and `InstallStrategy::Thread`.
|
|
176
|
+
|
|
177
|
+
- **Roles (§6):** Modules as roles (`Funnel`, `ARModelWalker`,
|
|
178
|
+
`ConstantShareabilizer`, `CallbackCapture`, etc.) — each `extend RoleDefaults`
|
|
179
|
+
for DRY default-proc patterns.
|
|
180
|
+
|
|
107
181
|
Per-concern patch files reopen `RactorRailsShim`'s singleton class to add
|
|
108
|
-
their `_install_*` methods. `
|
|
109
|
-
(`core.rb` first — it defines the module skeleton +
|
|
182
|
+
their `_install_*` methods. `loader.rb` requires them in dependency order
|
|
183
|
+
(`core.rb` first — it defines the module skeleton + constants that others
|
|
110
184
|
reference). Each `_install_*` method is idempotent (guarded by its own
|
|
111
185
|
`@*_patched` flag), so `install`, `prepare_for_ractors!`, and
|
|
112
186
|
`make_app_shareable!` can all call the full set safely.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ConstReassign (Issue #46 Step 46.2): the $VERBOSE-suppressed const_set
|
|
4
|
+
# utility extracted from the facade's _reassign_shareable_const.
|
|
5
|
+
#
|
|
6
|
+
# The facade's _reassign_shareable_const calls ConstReassign.call, then
|
|
7
|
+
# updates Registry for the four known registries (SHAREABLE_FALLBACK,
|
|
8
|
+
# SHAREABLE_MATTR_DEFAULTS, ABSTRACT_REGISTRY, VIEW_CONTEXT_REGISTRY).
|
|
9
|
+
# Role objects (CallbackCapture, FallbackBuilder, AppShareabilizer) inject
|
|
10
|
+
# ConstReassign.method(:call) directly via the configure seam.
|
|
11
|
+
|
|
12
|
+
module RactorRailsShim
|
|
13
|
+
module ConstReassign
|
|
14
|
+
# Set a constant on target_module with $VERBOSE suppressed.
|
|
15
|
+
# Returns the new value.
|
|
16
|
+
def self.call(target_module, name, value)
|
|
17
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
18
|
+
begin
|
|
19
|
+
target_module.const_set(name, value)
|
|
20
|
+
ensure
|
|
21
|
+
$VERBOSE = verbose
|
|
22
|
+
end
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Funnel: the debug-aware exception-swallowing role extracted from the
|
|
4
|
+
# RactorRailsShim god module (Issue #31, step 31.1d; POODR §1 SRP).
|
|
5
|
+
#
|
|
6
|
+
# Owns the `swallow(label) { block }` contract: run the block, rescue
|
|
7
|
+
# StandardError, emit a labeled $stderr line when `debug?` is true,
|
|
8
|
+
# return nil on failure. Used by freeze/shareability paths where
|
|
9
|
+
# individual failures are expected (some ivars hold intrinsically
|
|
10
|
+
# unshareable values like Procs) but a worker crash on the same value
|
|
11
|
+
# later has no visible cause.
|
|
12
|
+
#
|
|
13
|
+
# `debug?` / `debug=` are the configuration seam. The facade
|
|
14
|
+
# `RactorRailsShim.debug?` / `debug=` delegate here so existing specs
|
|
15
|
+
# keep passing; role objects (ARModelWalker, LoggerIONeutralizer) reach
|
|
16
|
+
# the funnel via `configure(funnel:)`, defaulting to `Funnel.method(:swallow)`.
|
|
17
|
+
|
|
18
|
+
module RactorRailsShim
|
|
19
|
+
module Funnel
|
|
20
|
+
@debug = nil
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# When true, swallowed exceptions are reported to $stderr so a worker
|
|
24
|
+
# Ractor that later crashes on an unshareable value has a traceable
|
|
25
|
+
# cause. Default false (silent, the historical behaviour).
|
|
26
|
+
def debug?
|
|
27
|
+
defined?(@debug) ? @debug : false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def debug=(value)
|
|
31
|
+
@debug = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Swallow an exception raised by the block, optionally logging it
|
|
35
|
+
# when `debug?` is on. `label` identifies the call site (e.g.
|
|
36
|
+
# "freeze AR ivar Post@column_defaults"). Keep the label short —
|
|
37
|
+
# it's only for grepping. Returns the block's value on success,
|
|
38
|
+
# nil on rescued StandardError.
|
|
39
|
+
def swallow(label)
|
|
40
|
+
yield
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
warn "[ractor_rails_shim] #{label}: #{e.class}: #{e.message[0, 120]}" if debug?
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# IESAccessor (Issue #45): generates reader/writer methods via the 3-tier
|
|
4
|
+
# IES lookup pattern. Extracts the repeated accessor boilerplate from
|
|
5
|
+
# patches/active_support.rb (20+ accessor patches that all follow the
|
|
6
|
+
# same shape).
|
|
7
|
+
#
|
|
8
|
+
# The generated methods use StorageStrategy for the actual lookup/store,
|
|
9
|
+
# so they inherit both Ractor-mode and Thread-mode behavior without
|
|
10
|
+
# duplicating the lookup logic.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# RactorRailsShim::IESAccessor.define_accessor(
|
|
14
|
+
# ActiveSupport::Inflector::Inflections,
|
|
15
|
+
# :instance,
|
|
16
|
+
# :ractor_rails_shim_inflections_en
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# This generates a reader (`instance`) and writer (`instance=`) on the
|
|
20
|
+
# target class that do the 3-tier lookup:
|
|
21
|
+
# 1. IES (per-Ractor IsolatedExecutionState)
|
|
22
|
+
# 2. SHAREABLE_FALLBACK (frozen config captured at prepare time)
|
|
23
|
+
# 3. main-Ractor default (CLASS_ATTR_VALUES or nil)
|
|
24
|
+
|
|
25
|
+
module RactorRailsShim
|
|
26
|
+
module IESAccessor
|
|
27
|
+
# Generate a reader method on `target` that does the 3-tier lookup.
|
|
28
|
+
# `target` is a Class or Module. `attr_name` is the method name (Symbol).
|
|
29
|
+
# `ies_key` is the storage key (Symbol).
|
|
30
|
+
def self.define_reader(target, attr_name, ies_key)
|
|
31
|
+
key_str = ies_key.inspect
|
|
32
|
+
target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
33
|
+
def #{attr_name}
|
|
34
|
+
strategy = RactorRailsShim.storage_strategy
|
|
35
|
+
strategy.lookup(self, #{key_str}, nil)
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Generate a writer method on `target` that stores via storage_strategy.
|
|
41
|
+
def self.define_writer(target, attr_name, ies_key)
|
|
42
|
+
key_str = ies_key.inspect
|
|
43
|
+
target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
44
|
+
def #{attr_name}=(val)
|
|
45
|
+
strategy = RactorRailsShim.storage_strategy
|
|
46
|
+
strategy.store(self, #{key_str}, val)
|
|
47
|
+
val
|
|
48
|
+
end
|
|
49
|
+
RUBY
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate both reader and writer on `target`.
|
|
53
|
+
def self.define_accessor(target, attr_name, ies_key)
|
|
54
|
+
define_reader(target, attr_name, ies_key)
|
|
55
|
+
define_writer(target, attr_name, ies_key)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Registry: the canonical home for the shared registries that every role
|
|
4
|
+
# object reads/writes (Issue #25, POODR §1 SRP + §2 Dependencies).
|
|
5
|
+
#
|
|
6
|
+
# Issue #35 (Round 4): Registry OWNS the nine registries as instance
|
|
7
|
+
# variables. The facade constants in patches/core.rb are aliases that
|
|
8
|
+
# delegate to Registry. Reassignment of the frozen (shareable) registries
|
|
9
|
+
# goes through Registry.reassign_* so the facade alias tracks the new
|
|
10
|
+
# value without a $VERBOSE-suppressed const_set on the facade.
|
|
11
|
+
#
|
|
12
|
+
# Types:
|
|
13
|
+
# - Mutable Array: class_attributes, shareable_constants, shareable_class_ivars
|
|
14
|
+
# - Mutable Hash: mattr_defaults, class_attr_values, shareable_mattr_defaults
|
|
15
|
+
# - Frozen (Ractor.make_shareable): abstract_registry, view_context_registry,
|
|
16
|
+
# shareable_fallback
|
|
17
|
+
#
|
|
18
|
+
# The frozen registries start as empty shareable Hashes and are swapped
|
|
19
|
+
# (via reassign_*) at prepare_for_ractors! / make_app_shareable! time with
|
|
20
|
+
# the populated, frozen, shareable versions. The mutable registries are
|
|
21
|
+
# appended at boot (main Ractor) and read at request time (worker Ractors).
|
|
22
|
+
|
|
23
|
+
module RactorRailsShim
|
|
24
|
+
module Registry
|
|
25
|
+
@class_attributes = []
|
|
26
|
+
@mattr_defaults = {}
|
|
27
|
+
@class_attr_values = {}
|
|
28
|
+
@shareable_mattr_defaults = {}
|
|
29
|
+
@shareable_constants = []
|
|
30
|
+
@shareable_class_ivars = []
|
|
31
|
+
@abstract_registry = Ractor.make_shareable({})
|
|
32
|
+
@view_context_registry = Ractor.make_shareable({})
|
|
33
|
+
@shareable_fallback = Ractor.make_shareable({})
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
attr_reader :class_attributes, :mattr_defaults, :class_attr_values,
|
|
37
|
+
:shareable_mattr_defaults, :shareable_constants,
|
|
38
|
+
:shareable_class_ivars, :abstract_registry,
|
|
39
|
+
:view_context_registry, :shareable_fallback
|
|
40
|
+
|
|
41
|
+
# Swap the frozen (shareable) registries. The value MUST already be
|
|
42
|
+
# frozen + Ractor.make_shareable. Each reassign_* updates the
|
|
43
|
+
# instance variable; the facade alias (defined in patches/core.rb)
|
|
44
|
+
# re-reads it on every access, so the alias tracks the new value
|
|
45
|
+
# without a const_set on the facade singleton.
|
|
46
|
+
def reassign_shareable_fallback(value)
|
|
47
|
+
@shareable_fallback = value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reassign_shareable_mattr_defaults(value)
|
|
51
|
+
@shareable_mattr_defaults = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reassign_abstract_registry(value)
|
|
55
|
+
@abstract_registry = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reassign_view_context_registry(value)
|
|
59
|
+
@view_context_registry = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Test seam: reset all nine registries to their boot defaults. Used
|
|
63
|
+
# by specs that mutate the registries and need a clean slate.
|
|
64
|
+
def reset_all
|
|
65
|
+
@class_attributes = []
|
|
66
|
+
@mattr_defaults = {}
|
|
67
|
+
@class_attr_values = {}
|
|
68
|
+
@shareable_mattr_defaults = {}
|
|
69
|
+
@shareable_constants = []
|
|
70
|
+
@shareable_class_ivars = []
|
|
71
|
+
@abstract_registry = Ractor.make_shareable({})
|
|
72
|
+
@view_context_registry = Ractor.make_shareable({})
|
|
73
|
+
@shareable_fallback = Ractor.make_shareable({})
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RoleDefaults — DRY mixin for the three shared default-proc patterns
|
|
4
|
+
# used by every role object with a configure seam (Issue #44, POODR §8e).
|
|
5
|
+
#
|
|
6
|
+
# Every role with a `funnel` seam copies:
|
|
7
|
+
# @funnel || RactorRailsShim::Funnel.method(:swallow)
|
|
8
|
+
# Every role with a `safe_const_get` seam copies:
|
|
9
|
+
# @safe_const_get || RactorRailsShim::ConstantShareabilizer.method(:safe_const_get)
|
|
10
|
+
# Every role with a `reassign_shareable_const` seam copies:
|
|
11
|
+
# @reassign_shareable_const || RactorRailsShim.method(:_reassign_shareable_const)
|
|
12
|
+
#
|
|
13
|
+
# `RoleDefaults` centralises the three defaults; each role `extend`s it
|
|
14
|
+
# and its `funnel` / `safe_const_get` / `reassign_shareable_const` reader
|
|
15
|
+
# calls `default_funnel` etc. The boilerplate collapses to one definition.
|
|
16
|
+
#
|
|
17
|
+
# Usage in a role module:
|
|
18
|
+
#
|
|
19
|
+
# module MyRole
|
|
20
|
+
# extend RoleDefaults
|
|
21
|
+
#
|
|
22
|
+
# @funnel = nil
|
|
23
|
+
# @safe_const_get = nil
|
|
24
|
+
#
|
|
25
|
+
# def self.configure(funnel: nil, safe_const_get: nil)
|
|
26
|
+
# @funnel = funnel
|
|
27
|
+
# @safe_const_get = safe_const_get
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# def self.reset_configuration
|
|
31
|
+
# @funnel = nil
|
|
32
|
+
# @safe_const_get = nil
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# def self.funnel
|
|
36
|
+
# @funnel || default_funnel
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# def self.safe_const_get
|
|
40
|
+
# @safe_const_get || default_safe_const_get
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
module RactorRailsShim
|
|
45
|
+
module RoleDefaults
|
|
46
|
+
def default_funnel
|
|
47
|
+
RactorRailsShim::Funnel.method(:swallow)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def default_safe_const_get
|
|
51
|
+
RactorRailsShim::ConstantShareabilizer.method(:safe_const_get)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def default_reassign_shareable_const
|
|
55
|
+
RactorRailsShim.method(:_reassign_shareable_const)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|