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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +113 -39
  3. data/lib/ractor_rails_shim/foundation/const_reassign.rb +26 -0
  4. data/lib/ractor_rails_shim/foundation/funnel.rb +47 -0
  5. data/lib/ractor_rails_shim/foundation/ies_accessor.rb +58 -0
  6. data/lib/ractor_rails_shim/foundation/registry.rb +77 -0
  7. data/lib/ractor_rails_shim/foundation/role_defaults.rb +58 -0
  8. data/lib/ractor_rails_shim/foundation/run_mode.rb +76 -0
  9. data/lib/ractor_rails_shim/foundation/storage.rb +99 -0
  10. data/lib/ractor_rails_shim/foundation/storage_strategy.rb +173 -0
  11. data/lib/ractor_rails_shim/foundation/version_policy.rb +147 -0
  12. data/lib/ractor_rails_shim/loader.rb +79 -0
  13. data/lib/ractor_rails_shim/patches/action_controller.rb +9 -9
  14. data/lib/ractor_rails_shim/patches/action_dispatch.rb +74 -15
  15. data/lib/ractor_rails_shim/patches/action_view.rb +26 -26
  16. data/lib/ractor_rails_shim/patches/active_model_attribute.rb +62 -5
  17. data/lib/ractor_rails_shim/patches/active_record_model_schema.rb +5 -5
  18. data/lib/ractor_rails_shim/patches/active_support.rb +49 -587
  19. data/lib/ractor_rails_shim/patches/activerecord.rb +89 -89
  20. data/lib/ractor_rails_shim/patches/callables.rb +60 -6
  21. data/lib/ractor_rails_shim/patches/class_attribute.rb +25 -76
  22. data/lib/ractor_rails_shim/patches/core.rb +128 -678
  23. data/lib/ractor_rails_shim/patches/devise.rb +10 -5
  24. data/lib/ractor_rails_shim/patches/execution_wrapper.rb +3 -3
  25. data/lib/ractor_rails_shim/patches/hash_compute_if_absent.rb +98 -0
  26. data/lib/ractor_rails_shim/patches/i18n.rb +455 -0
  27. data/lib/ractor_rails_shim/patches/kaminari.rb +5 -5
  28. data/lib/ractor_rails_shim/patches/make_shareable.rb +21 -772
  29. data/lib/ractor_rails_shim/patches/mattr_accessor.rb +15 -6
  30. data/lib/ractor_rails_shim/patches/orm_adapter.rb +3 -3
  31. data/lib/ractor_rails_shim/patches/propshaft.rb +4 -4
  32. data/lib/ractor_rails_shim/patches/rack.rb +15 -15
  33. data/lib/ractor_rails_shim/patches/rails_module.rb +23 -23
  34. data/lib/ractor_rails_shim/patches/route_helpers.rb +3 -3
  35. data/lib/ractor_rails_shim/patches/url_helpers.rb +2 -2
  36. data/lib/ractor_rails_shim/patches/warden.rb +2 -2
  37. data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +4 -4
  38. data/lib/ractor_rails_shim/patches.rb +4 -102
  39. data/lib/ractor_rails_shim/roles/app_shareabilizer.rb +207 -0
  40. data/lib/ractor_rails_shim/roles/ar_model_walker.rb +93 -0
  41. data/lib/ractor_rails_shim/roles/callback_capture.rb +226 -0
  42. data/lib/ractor_rails_shim/roles/check.rb +210 -0
  43. data/lib/ractor_rails_shim/roles/constant_shareabilizer.rb +217 -0
  44. data/lib/ractor_rails_shim/roles/fallback_builder.rb +333 -0
  45. data/lib/ractor_rails_shim/roles/fallback_ies.rb +25 -0
  46. data/lib/ractor_rails_shim/roles/freezers.rb +341 -0
  47. data/lib/ractor_rails_shim/roles/install_strategy.rb +68 -0
  48. data/lib/ractor_rails_shim/roles/installer.rb +89 -0
  49. data/lib/ractor_rails_shim/roles/lifecycle.rb +39 -0
  50. data/lib/ractor_rails_shim/roles/logger_io_neutralizer.rb +146 -0
  51. data/lib/ractor_rails_shim/roles/pre_spawn_steps.rb +36 -0
  52. data/lib/ractor_rails_shim/roles/shareability_traversal.rb +488 -0
  53. data/lib/ractor_rails_shim/roles/worker_app.rb +77 -0
  54. data/lib/ractor_rails_shim/roles/worker_app_factory.rb +124 -0
  55. data/lib/ractor_rails_shim/version.rb +1 -1
  56. data/lib/ractor_rails_shim.rb +1 -2
  57. metadata +30 -5
  58. data/lib/ractor_rails_shim/check.rb +0 -200
  59. data/lib/ractor_rails_shim/fallback_ies.rb +0 -47
  60. data/lib/ractor_rails_shim/version_policy.rb +0 -72
  61. /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: 21002fee5a30f663904441d8c016c035564095bf456048a25917657e9adab5fc
4
- data.tar.gz: dcb6764139de152ae7b958112503efc7951d59ae211c04ee268da6bf63a2be51
3
+ metadata.gz: 6ebcbda319623aa7003caf170774074e19fdf2d6452ed086fbc5edbfec037fa5
4
+ data.tar.gz: 7ab60400652444ccbc5ca39ac403d387c156cc8a6178700f10c9d744db9c3fad
5
5
  SHA512:
6
- metadata.gz: 28cf75ae0f4972142cee3965d5c68fd8b70c09b901dc60c186f178ac403fc7695c3f7e2e77e4591c9aa6aa0fc9c6d1ba5eafa8e3c64d5abad545b50159159d6c
7
- data.tar.gz: b1bf30685c4b7082c5930a63693571e4d3affff2fa0b530fd355aaf5a57f5cf42280ff5a590f099705e8af573ae953d654ace88fbb813dc05e463379fa0c09c7
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 # entry point — autoload-install if Rails is loaded
67
+ lib/ractor_rails_shim.rb # entry point — autoload-install if Rails is loaded
68
68
  lib/ractor_rails_shim/
69
- version.rb # VERSION constant (currently 0.2.5)
70
- version_check.rb # Gem::Version-based Ruby/Rails detection + policy
71
- fallback_ies.rb # thread-local IES shim when ActiveSupport is absent
72
- check.rb # the ractor-rails-check audit (Check.scan / report)
73
- patches.rb # requires all per-concern patch files in order
74
- patches/
75
- core.rb # module skeleton, registries, install, prepare_for_ractors!, WorkerApp
76
- make_shareable.rb # make_app_shareable!, callable/lock replacement, shareable fallback
77
- rails_module.rb # Rails.application / Rails.cache / Rails.logger / Rails.env …
78
- mattr_accessor.rb # Module#mattr_accessor / cattr_accessor macro rewrite
79
- class_attribute.rb # ActiveSupport class_attribute macro rewrite
80
- zeitwerk_registry.rb # Zeitwerk::Registry class ivars
81
- route_helpers.rb # ActionDispatch::Routing::RouteSet#generate_url_helpers
82
- url_helpers.rb # URL helper singleton + module fixes
83
- execution_wrapper.rb # ActiveSupport::ExecutionWrapper + callback replay
84
- rack.rb # Rack::Request / Rack::Utils
85
- action_view.rb # PathRegistry, LookupContext, Template handlers, compiled_method_container
86
- action_controller.rb # AbstractController, ActionController name/encoding
87
- action_dispatch.rb # ActionDispatch routing, http_url, journey, mounted helpers
88
- polymorphic_routes.rb # polymorphic_path(s) URL helpers
89
- active_support.rb # Inflector, error_reporter, ExecutionContext, I18n, JSON encoding, Reloader
90
- warden.rb # Warden hooks / strategies / serializer
91
- devise.rb # Devise url_helpers / authenticatable / failure_app
92
- active_model_attribute.rb # ActiveModel::Attribute dup_or_share for frozen graphs
93
- active_record_model_schema.rb # AR ModelSchema reload_schema_from_cache (worker-safe)
94
- activerecord.rb # AR connection handler, configurations, query caches, …
95
- kaminari.rb # Kaminari config
96
- propshaft.rb # Propshaft asset server
97
- orm_adapter.rb # orm_adapter
98
- openssl.rb # OpenSSL digest cache
99
- rubygems.rb # Rubygems msgpack pre-check
100
- exe/ractor-rails-check # CLI audit tool
101
- spec/ # 7 spec files, 61 unit tests (no Rails dep) + integration spec
102
- script/make_test_app.sh # build the minimal Rails 8.1 test app (CI uses this)
103
- script/make_full_test_app.sh # build the full-featured test app (Devise/PG/Kaminari)
104
- .github/workflows/ci.yml # unit job + integration job (GET /up → 200 in a worker Ractor)
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. `patches.rb` requires them in dependency order
109
- (`core.rb` first — it defines the module skeleton + registries the rest
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