ractor-rails-shim 0.3.0 → 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/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 +74 -15
- data/lib/ractor_rails_shim/patches/action_view.rb +26 -26
- 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 +89 -89
- data/lib/ractor_rails_shim/patches/callables.rb +60 -6
- data/lib/ractor_rails_shim/patches/class_attribute.rb +25 -76
- data/lib/ractor_rails_shim/patches/core.rb +128 -678
- data/lib/ractor_rails_shim/patches/devise.rb +10 -5
- 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 +5 -5
- data/lib/ractor_rails_shim/patches/make_shareable.rb +21 -772
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +15 -6
- 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 +23 -23
- data/lib/ractor_rails_shim/patches/route_helpers.rb +3 -3
- 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 -102
- 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 +30 -5
- data/lib/ractor_rails_shim/check.rb +0 -200
- data/lib/ractor_rails_shim/fallback_ies.rb +0 -47
- data/lib/ractor_rails_shim/version_policy.rb +0 -72
- /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/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
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RunMode: the thread-mode vs Ractor-mode configuration decision.
|
|
4
|
+
#
|
|
5
|
+
# Extracted from the RactorRailsShim singleton (`core.rb`) per POODR: deciding
|
|
6
|
+
# which mode the shim runs in is a *configuration* responsibility, distinct
|
|
7
|
+
# from the install orchestration that consumes the decision. The decision
|
|
8
|
+
# used to be a side effect buried inside `install` (reading `ENV["SERVER"]`
|
|
9
|
+
# into `@thread_mode`), which made install non-deterministic based on ambient
|
|
10
|
+
# ENV with no visible configuration surface. RunMode owns that decision so it
|
|
11
|
+
# is independently specable and configurable:
|
|
12
|
+
#
|
|
13
|
+
# RactorRailsShim::RunMode.thread = true # explicit
|
|
14
|
+
# RactorRailsShim::RunMode.detect_from_env # ENV-based (pure query)
|
|
15
|
+
# RactorRailsShim::RunMode.resolve! # install-time: explicit,
|
|
16
|
+
# # else fall back to ENV
|
|
17
|
+
# RactorRailsShim::RunMode.thread? # the answer
|
|
18
|
+
# RactorRailsShim::RunMode.reset # test/teardown helper
|
|
19
|
+
#
|
|
20
|
+
# The RactorRailsShim facade keeps `thread_mode?` / `thread_mode=` delegates
|
|
21
|
+
# so existing call sites (patches/class_attribute.rb, patches/active_support.rb,
|
|
22
|
+
# specs that set `RactorRailsShim.thread_mode = true`) keep working unchanged.
|
|
23
|
+
module RactorRailsShim
|
|
24
|
+
module RunMode
|
|
25
|
+
# SERVER values that select thread-server (Puma/Falcon/Thin/Webrick) mode
|
|
26
|
+
# instead of the default Ractor mode. Matched case-insensitively. Kept as a
|
|
27
|
+
# Regex to mirror the original `core.rb:189` predicate exactly.
|
|
28
|
+
THREAD_SERVER_RE = /puma|falcon|thin|webrick|thread/i.freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# The resolved mode: true for thread-server mode, false for Ractor mode.
|
|
32
|
+
# Defaults to false when never configured. After `resolve!` or an
|
|
33
|
+
# explicit `thread=`, this is sticky — ENV no longer affects it.
|
|
34
|
+
def thread?
|
|
35
|
+
return @thread if defined?(@thread)
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Explicitly set the mode. Once called, `resolve!` becomes a no-op:
|
|
40
|
+
# explicit configuration wins over ambient ENV. This mirrors the old
|
|
41
|
+
# `install` body's `unless defined?(@thread_mode)` guard, lifted into
|
|
42
|
+
# the object that owns the state.
|
|
43
|
+
def thread=(value)
|
|
44
|
+
@thread = !!value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Pure query of the ambient ENV["SERVER"]. Returns true when the server
|
|
48
|
+
# name matches a known thread server (puma|falcon|thin|webrick|thread*),
|
|
49
|
+
# case-insensitively; false otherwise (including when SERVER is unset or
|
|
50
|
+
# empty). Has no side effects and does not mutate RunMode state — call
|
|
51
|
+
# `resolve!` to fold the ENV answer into the resolved mode.
|
|
52
|
+
def detect_from_env
|
|
53
|
+
server = ENV["SERVER"]
|
|
54
|
+
return false unless server
|
|
55
|
+
!!(server =~ THREAD_SERVER_RE)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The install-time decision. If the mode has already been set explicitly
|
|
59
|
+
# (via `thread=`), keep it — explicit wins over ENV. Otherwise, detect
|
|
60
|
+
# from ENV and freeze that as the resolved mode. Idempotent: a second
|
|
61
|
+
# call is a no-op because the first call (or an explicit `thread=`)
|
|
62
|
+
# defined `@thread`.
|
|
63
|
+
def resolve!
|
|
64
|
+
return if defined?(@thread)
|
|
65
|
+
@thread = detect_from_env
|
|
66
|
+
@thread
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reset to the unconfigured state. For specs and teardown; not part of
|
|
70
|
+
# the public install contract.
|
|
71
|
+
def reset
|
|
72
|
+
remove_instance_variable(:@thread) if defined?(@thread)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Storage role (Issue #14, Step 14.1): a pluggable key-value store contract
|
|
4
|
+
# (`[]`, `[]=`, `key?`, `delete`) with two implementations:
|
|
5
|
+
#
|
|
6
|
+
# * `Storage::IES` — delegates to ActiveSupport::IsolatedExecutionState.
|
|
7
|
+
# * `Storage::ThreadLocal` — the former `FallbackIES` body (a thread-local
|
|
8
|
+
# Hash). Used only when AS is absent.
|
|
9
|
+
#
|
|
10
|
+
# `RactorRailsShim.storage` is set once at load time: IES when AS is loaded,
|
|
11
|
+
# ThreadLocal otherwise. The eval'd method bodies in the patch files route
|
|
12
|
+
# through `RactorRailsShim.storage[...]` instead of the literal
|
|
13
|
+
# `ActiveSupport::IsolatedExecutionState[...]`, so the shim no longer needs to
|
|
14
|
+
# open the ActiveSupport namespace to alias the fallback onto it (Step 14.3
|
|
15
|
+
# deletes that namespace patch).
|
|
16
|
+
#
|
|
17
|
+
# The contract is intentionally minimal: the patch sites only use `[]`, `[]=`,
|
|
18
|
+
# `key?`, and (rarely) `delete`. `clear` is provided on ThreadLocal for
|
|
19
|
+
# spec/parity with the former FallbackIES API but is not part of the
|
|
20
|
+
# cross-implementation contract.
|
|
21
|
+
|
|
22
|
+
module RactorRailsShim
|
|
23
|
+
module Storage
|
|
24
|
+
# Delegates to the real ActiveSupport::IsolatedExecutionState when AS is
|
|
25
|
+
# loaded. Methods are class methods on the module's singleton class so
|
|
26
|
+
# callers do `Storage::IES[key]`.
|
|
27
|
+
module IES
|
|
28
|
+
class << self
|
|
29
|
+
def [](key)
|
|
30
|
+
ActiveSupport::IsolatedExecutionState[key]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def []=(key, value)
|
|
34
|
+
ActiveSupport::IsolatedExecutionState[key] = value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def key?(key)
|
|
38
|
+
ActiveSupport::IsolatedExecutionState.key?(key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(key)
|
|
42
|
+
ActiveSupport::IsolatedExecutionState.delete(key)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Thread-local fallback: the former `FallbackIES` body. A per-thread Hash
|
|
48
|
+
# stored under a well-known key on Thread.current. Provides `clear` for
|
|
49
|
+
# spec/parity (not part of the cross-implementation contract).
|
|
50
|
+
module ThreadLocal
|
|
51
|
+
KEY = :active_support_execution_state_fallback
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
def [](key)
|
|
55
|
+
Thread.current[KEY]&.[](key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def []=(key, value)
|
|
59
|
+
(Thread.current[KEY] ||= {})[key] = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def key?(key)
|
|
63
|
+
Thread.current[KEY]&.key?(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete(key)
|
|
67
|
+
Thread.current[KEY]&.delete(key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def clear
|
|
71
|
+
Thread.current[KEY] = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Select once at load: IES when AS is loaded, ThreadLocal otherwise.
|
|
77
|
+
@storage =
|
|
78
|
+
if defined?(::ActiveSupport::IsolatedExecutionState)
|
|
79
|
+
IES
|
|
80
|
+
else
|
|
81
|
+
ThreadLocal
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
# The active storage implementation (either `Storage::IES` or
|
|
86
|
+
# `Storage::ThreadLocal`). Exposed as `RactorRailsShim.storage` by the
|
|
87
|
+
# facade (defined in `patches.rb`).
|
|
88
|
+
attr_reader :storage
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
# The active storage implementation. Patch sites route through this so the
|
|
94
|
+
# shim doesn't open the ActiveSupport namespace to alias the fallback.
|
|
95
|
+
def storage
|
|
96
|
+
Storage.storage
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|