ractor-rails-shim 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a4b7ce968f90326f2b476d713b7fab6c524f8f7b111f60a2ae582d1e162de88
4
- data.tar.gz: c144478be527ff98804db48add54d67edb4124514fba3670b970990ef09e36c0
3
+ metadata.gz: 21002fee5a30f663904441d8c016c035564095bf456048a25917657e9adab5fc
4
+ data.tar.gz: dcb6764139de152ae7b958112503efc7951d59ae211c04ee268da6bf63a2be51
5
5
  SHA512:
6
- metadata.gz: 55cc0fb00ebd523182cb66f8ae7a9f270bf33bcaaf928af79d291edf3eeb0248246f956e082200ab74563c7afceee8520336f53373ed9d54ff73f4db9ce05f40
7
- data.tar.gz: b19280d1985a1e8b456a6a13750e277a49c5123998a4f7fab587518d3d6a717249d9490cfbe6d32751483d9c5e622373d861fbed9ce50d8daeabb09578b897e9
6
+ metadata.gz: 28cf75ae0f4972142cee3965d5c68fd8b70c09b901dc60c186f178ac403fc7695c3f7e2e77e4591c9aa6aa0fc9c6d1ba5eafa8e3c64d5abad545b50159159d6c
7
+ data.tar.gz: b1bf30685c4b7082c5930a63693571e4d3affff2fa0b530fd355aaf5a57f5cf42280ff5a590f099705e8af573ae953d654ace88fbb813dc05e463379fa0c09c7
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
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Fallback IsolatedExecutionState when ActiveSupport is not available.
4
- # This is a simple thread-local storage that mimics the ActiveSupport API
5
- # enough for the shim to work. In production (with Rails loaded), the real
6
- # ActiveSupport::IsolatedExecutionState is used instead.
7
- module ActiveSupport
8
- module IsolatedExecutionState
4
+ #
5
+ # The shim's string-eval'd patch methods reference the literal constant
6
+ # `ActiveSupport::IsolatedExecutionState` (no captured binding → callable
7
+ # from any Ractor). To avoid opening the ActiveSupport namespace from a
8
+ # third-party gem, the fallback is defined in the shim's own namespace
9
+ # (RactorRailsShim::FallbackIES) and aliased onto
10
+ # ActiveSupport::IsolatedExecutionState only when the real AS IES is absent.
11
+ # When AS is loaded, the real one wins untouched and no alias is created.
12
+ module RactorRailsShim
13
+ module FallbackIES
9
14
  KEY = :active_support_execution_state_fallback
10
15
 
11
16
  class << self
@@ -30,4 +35,13 @@ module ActiveSupport
30
35
  end
31
36
  end
32
37
  end
33
- end unless defined?(ActiveSupport::IsolatedExecutionState)
38
+ end
39
+
40
+ # Alias onto ActiveSupport::IsolatedExecutionState only when the real AS IES
41
+ # is absent, so the shim's string-eval'd references resolve to the fallback
42
+ # without the shim opening the ActiveSupport namespace to define a module.
43
+ unless defined?(ActiveSupport::IsolatedExecutionState)
44
+ module ActiveSupport
45
+ IsolatedExecutionState = RactorRailsShim::FallbackIES
46
+ end
47
+ end
@@ -293,7 +293,7 @@ module RactorRailsShim
293
293
  # while the objects are still mutable) populates the ivars so the frozen,
294
294
  # shared copies already hold the values and workers only read them.
295
295
  if Ractor.main? && defined?(::Rails) && ::Rails.application
296
- begin
296
+ _swallow("warm journey routes") do
297
297
  rset = ::Rails.application.routes
298
298
  all = []
299
299
  all.concat(rset.named_routes.send(:routes).values) rescue nil
@@ -310,8 +310,6 @@ module RactorRailsShim
310
310
  p.required_names rescue nil
311
311
  p.optional_names rescue nil
312
312
  end
313
- rescue
314
- nil
315
313
  end
316
314
  end
317
315
  # Capture the (shareable) RouteSet so workers can build URLs without
@@ -422,15 +420,15 @@ module RactorRailsShim
422
420
  nrc.alias_method(:define_url_helper_without_shim, :define_url_helper)
423
421
  end
424
422
  nrc.define_method(:define_url_helper) do |mod, name, helper, url_strategy|
425
- begin
426
- # Detach the helper from the live route object before deep-freezing
427
- # it for cross-Ractor sharing. The non-optimized UrlHelper#call only
428
- # needs @options / @segment_keys / @route_name to build the options
429
- # hash and then delegates to `t._routes.url_for(route_name, ...)`,
430
- # which looks the route up in the (shareable) RouteSet by name. The
431
- # @route reference would pull the whole route graph into the freeze,
432
- # freezing objects that make_app_shareable! must still be able to
433
- # mutate (e.g. Devise route constraints) -> FrozenError.
423
+ # Detach the helper from the live route object before deep-freezing
424
+ # it for cross-Ractor sharing. The non-optimized UrlHelper#call only
425
+ # needs @options / @segment_keys / @route_name to build the options
426
+ # hash and then delegates to `t._routes.url_for(route_name, ...)`,
427
+ # which looks the route up in the (shareable) RouteSet by name. The
428
+ # @route reference would pull the whole route graph into the freeze,
429
+ # freezing objects that make_app_shareable! must still be able to
430
+ # mutate (e.g. Devise route constraints) -> FrozenError.
431
+ RactorRailsShim._swallow("freeze url helper") do
434
432
  if helper.respond_to?(:instance_variable_get)
435
433
  helper.instance_variable_set(:@route, nil) rescue nil
436
434
  opts = helper.instance_variable_get(:@options)
@@ -439,8 +437,6 @@ module RactorRailsShim
439
437
  helper.instance_variable_set(:@segment_keys, segs.dup.freeze) rescue nil
440
438
  end
441
439
  helper = Ractor.make_shareable(helper)
442
- rescue
443
- nil
444
440
  end
445
441
  RactorRailsShim::URL_HELPERS[name] = helper
446
442
  strategy_const = url_strategy.equal?(::ActionDispatch::Routing::RouteSet::PATH) ?
@@ -705,7 +701,7 @@ module RactorRailsShim
705
701
  if defined?(::ActionDispatch::Journey::GTG::Builder) &&
706
702
  ::ActionDispatch::Journey::GTG::Builder.const_defined?(:DUMMY_END_NODE)
707
703
  node = ::ActionDispatch::Journey::GTG::Builder.const_get(:DUMMY_END_NODE)
708
- Ractor.make_shareable(node) rescue nil
704
+ _swallow("make journey dummy end node shareable") { Ractor.make_shareable(node) }
709
705
  end
710
706
  end
711
707
 
@@ -787,7 +783,7 @@ module RactorRailsShim
787
783
  next unless c.is_a?(::Array) || c.is_a?(::Hash)
788
784
  next if c.frozen?
789
785
  c.freeze
790
- ::Ractor.make_shareable(c) rescue nil
786
+ _swallow("make mime negotiation constant shareable") { ::Ractor.make_shareable(c) }
791
787
  end
792
788
  rescue => e
793
789
  warn "[ractor-rails-shim] _freeze_mime_negotiation!: #{e.class}: #{e.message}"
@@ -244,7 +244,7 @@ module RactorRailsShim
244
244
  (defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application) ? ::Rails.application.routes : nil,
245
245
  nil
246
246
  )
247
- Ractor.make_shareable(fallback) rescue nil
247
+ fallback = _swallow("make view context fallback shareable") { Ractor.make_shareable(fallback) }
248
248
  self._view_context_fallback = fallback
249
249
  end
250
250
 
@@ -199,12 +199,7 @@ module RactorRailsShim
199
199
  end
200
200
  snapshot.freeze
201
201
  Ractor.make_shareable(snapshot)
202
- verbose, $VERBOSE = $VERBOSE, nil
203
- begin
204
- const_set(:AR_CONFIGURATIONS_SNAPSHOT, snapshot)
205
- ensure
206
- $VERBOSE = verbose
207
- end
202
+ _reassign_shareable_const(:AR_CONFIGURATIONS_SNAPSHOT, snapshot)
208
203
  rescue => e
209
204
  # Best-effort; if we can't capture configs, workers won't be able
210
205
  # to auto-init connections. They can call init_worker_ar_connections!
@@ -459,12 +454,7 @@ module RactorRailsShim
459
454
  pk_map[n] = pk if pk
460
455
  end
461
456
  shareable = Ractor.make_shareable(pk_map)
462
- verbose, $VERBOSE = $VERBOSE, nil
463
- begin
464
- const_set(:AR_PRIMARY_KEYS_SHAREABLE, shareable)
465
- ensure
466
- $VERBOSE = verbose
467
- end
457
+ _reassign_shareable_const(:AR_PRIMARY_KEYS_SHAREABLE, shareable)
468
458
  rescue => e
469
459
  # best-effort
470
460
  end
@@ -916,12 +906,7 @@ module RactorRailsShim
916
906
  cfg = ::ActiveRecord::Base.configurations
917
907
  cfg = Ractor.make_shareable(cfg) if cfg
918
908
  if cfg
919
- verbose, $VERBOSE = $VERBOSE, nil
920
- begin
921
- const_set(:AR_CONFIGURATIONS_SHAREABLE, cfg)
922
- ensure
923
- $VERBOSE = verbose
924
- end
909
+ _reassign_shareable_const(:AR_CONFIGURATIONS_SHAREABLE, cfg)
925
910
  end
926
911
  rescue => e
927
912
  # best-effort
@@ -977,14 +962,9 @@ module RactorRailsShim
977
962
  handlers = ::ActiveRecord::DatabaseConfigurations.db_config_handlers
978
963
  # Make each handler Proc shareable (freezes its binding). A shareable
979
964
  # Proc is callable from any Ractor.
980
- handlers.each { |h| Ractor.make_shareable(h) rescue nil }
965
+ handlers.each { |h| _swallow("make ar db config handler shareable") { Ractor.make_shareable(h) } }
981
966
  shareable = Ractor.make_shareable(handlers.dup)
982
- verbose, $VERBOSE = $VERBOSE, nil
983
- begin
984
- const_set(:AR_DB_CONFIG_HANDLERS_SHAREABLE, shareable)
985
- ensure
986
- $VERBOSE = verbose
987
- end
967
+ _reassign_shareable_const(:AR_DB_CONFIG_HANDLERS_SHAREABLE, shareable)
988
968
  rescue => e
989
969
  # best-effort
990
970
  end
@@ -1031,14 +1011,9 @@ module RactorRailsShim
1031
1011
  if Ractor.main?
1032
1012
  begin
1033
1013
  transformers = ::ActiveRecord.query_transformers
1034
- transformers.each { |t| Ractor.make_shareable(t) rescue nil }
1014
+ transformers.each { |t| _swallow("make ar query transformer shareable") { Ractor.make_shareable(t) } }
1035
1015
  shareable = Ractor.make_shareable(transformers.dup)
1036
- verbose, $VERBOSE = $VERBOSE, nil
1037
- begin
1038
- const_set(:AR_QUERY_TRANSFORMERS_SHAREABLE, shareable)
1039
- ensure
1040
- $VERBOSE = verbose
1041
- end
1016
+ _reassign_shareable_const(:AR_QUERY_TRANSFORMERS_SHAREABLE, shareable)
1042
1017
  rescue => e
1043
1018
  # best-effort
1044
1019
  end
@@ -1085,12 +1060,7 @@ module RactorRailsShim
1085
1060
  begin
1086
1061
  val = ::ActiveRecord.public_send(method_name)
1087
1062
  shareable = Ractor.make_shareable(val.is_a?(::Array) ? val.dup : val)
1088
- verbose, $VERBOSE = $VERBOSE, nil
1089
- begin
1090
- const_set(const_name, shareable)
1091
- ensure
1092
- $VERBOSE = verbose
1093
- end
1063
+ _reassign_shareable_const(const_name, shareable)
1094
1064
  rescue => e
1095
1065
  # best-effort
1096
1066
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Callable / lock-replacement object model. These classes are shareable
4
+ # stand-ins for unshareable Rails internals (self-capturing Procs, Mutexes,
5
+ # IO-backed log devices) that make_app_shareable! swaps into the app graph
6
+ # before Ractor.make_shareable. Extracted from make_shareable.rb so the
7
+ # object model has its own home.
8
+ #
9
+ # Defined on RactorRailsShim's singleton class (the same access path the rest
10
+ # of the codebase uses: RactorRailsShim.singleton_class.const_get(:NoOpProc)).
11
+
12
+ module RactorRailsShim
13
+ class << self
14
+ class NoOpProc
15
+ def call(*_); nil; end
16
+ # A NoOpProc is a shareable stand-in for an arbitrary Proc in the app
17
+ # graph. Some Rails code passes such values through `&block`, which
18
+ # calls `#to_proc` and then requires the result to be a real Proc.
19
+ # Return a frozen no-op lambda so the implicit conversion succeeds and
20
+ # the (side-effect-free) call is a true no-op, matching `#call`.
21
+ #
22
+ # The lambda is a shareable constant (frozen at class load), NOT
23
+ # memoized on `@_to_proc`: NoOpProc instances are deep-frozen by
24
+ # `Ractor.make_shareable` during `make_app_shareable!`, so an
25
+ # `@_to_proc ||= ...` write would raise FrozenError on the frozen
26
+ # instance. A constant avoids the write entirely and is safe to share.
27
+ NO_OP_LAMBDA = ->(*) { nil }.freeze
28
+ Ractor.make_shareable(NO_OP_LAMBDA)
29
+ def to_proc
30
+ NO_OP_LAMBDA
31
+ end
32
+ end
33
+
34
+ class Callable
35
+ def initialize(target, method_name)
36
+ @target = target
37
+ @method_name = method_name
38
+ end
39
+ def call(*args)
40
+ @target.__send__(@method_name, *args)
41
+ end
42
+ end
43
+
44
+ class CallableConst
45
+ def initialize(value); @value = value; end
46
+ def call(*_); @value; end
47
+ end
48
+
49
+ # Shareable snapshot of a Devise::Mapping. The real Mapping holds an
50
+ # unshareable lambda (failure_app) plus a default-proc Hash (controllers),
51
+ # so it can't be Ractor.make_shareable'd. Request-time code only reads a
52
+ # handful of attributes (name, to/class, router_name, controllers, ...),
53
+ # which are all shareable values. We copy those now (in main) into a
54
+ # frozen Plain Old Object that plays the role of the Mapping in workers.
55
+ class DeviseMappingSnapshot
56
+ def initialize(mapping)
57
+ @name = mapping.name
58
+ @klass = mapping.to
59
+ @router_name = mapping.instance_variable_get(:@router_name)
60
+ @singular = mapping.instance_variable_get(:@singular)
61
+ @scoped_path = mapping.instance_variable_get(:@scoped_path)
62
+ @path = mapping.instance_variable_get(:@path)
63
+ @path_prefix = mapping.instance_variable_get(:@path_prefix)
64
+ @format = mapping.instance_variable_get(:@format)
65
+ @sign_out_via = mapping.instance_variable_get(:@sign_out_via)
66
+ @modules = mapping.modules
67
+ @strategies = mapping.strategies
68
+ @routes = mapping.routes
69
+ @used_helpers = mapping.used_helpers
70
+ # controllers is a Hash with a default proc (unshareable) — copy the
71
+ # entries into a plain frozen Hash.
72
+ h = {}
73
+ RactorRailsShim._swallow("devise mapping controllers") do
74
+ mapping.controllers.each { |k, v| h[k] = v }
75
+ end
76
+ @controllers = h.freeze
77
+ # failure_app is either Devise::FailureApp (a shareable class) or a
78
+ # lambda (when configured as a String) — keep only the shareable class.
79
+ fa = mapping.instance_variable_get(:@failure_app)
80
+ fa = ::Devise::FailureApp unless fa.is_a?(Class)
81
+ @failure_app = fa
82
+ freeze
83
+ end
84
+
85
+ def name; @name; end
86
+ def to; @klass; end
87
+ def router_name; @router_name; end
88
+ def singular; @singular; end
89
+ def scoped_path; @scoped_path; end
90
+ def path; @path; end
91
+ def path_prefix; @path_prefix; end
92
+ def format; @format; end
93
+ def sign_out_via; @sign_out_via; end
94
+ def modules; @modules; end
95
+ def strategies; @strategies; end
96
+ def routes; @routes; end
97
+ def used_helpers; @used_helpers; end
98
+ def controllers; @controllers; end
99
+ def failure_app; @failure_app; end
100
+ def authenticatable?; @modules.any? { |m| m.to_s =~ /authenticatable/ }; end
101
+ def no_input_strategies; @strategies & Devise::NO_INPUT; end
102
+ def fullpath; "/#{@path_prefix}/#{@path}".squeeze("/"); end
103
+ # Devise::Mapping defines one `x?` predicate per Devise module
104
+ # (confirmable?, rememberable?, registerable?, ...) via `add_module`.
105
+ # Rather than enumerate them, fall back for any `x?` predicate to
106
+ # checking @modules — matching the generated behaviour.
107
+ def respond_to_missing?(method, _)
108
+ method.to_s.end_with?("?") || super
109
+ end
110
+
111
+ def method_missing(method, *args)
112
+ s = method.to_s
113
+ if s.end_with?("?") && args.empty?
114
+ @modules.include?(s.chomp("?").to_sym)
115
+ else
116
+ super
117
+ end
118
+ end
119
+ end
120
+
121
+ def _devise_mapping_snapshot(mapping)
122
+ _swallow("devise mapping snapshot") { DeviseMappingSnapshot.new(mapping) }
123
+ end
124
+
125
+ class NoOpLock
126
+ def synchronize; yield; end
127
+ def mon_synchronize; yield; end
128
+ def lock; self; end
129
+ def unlock; self; end
130
+ def locked?; false; end
131
+ def mon_enter; end
132
+ def mon_exit; end
133
+ def mon_locked?; false; end
134
+ def try_lock; true; end
135
+ def new_cond; Struct.new(:wait, :signal, :broadcast).new(-> {}, -> {}, -> {}); end
136
+ end
137
+
138
+ # No-op log device sink: a frozen, shareable stand-in for an IO, swapped
139
+ # in for $stdout/$stderr in the app's logger before make_shareable so
140
+ # the real IOs aren't frozen. Responds to the write methods a
141
+ # Logger::LogDevice might call.
142
+ class NoOpLogDev
143
+ def write(*_); self; end
144
+ def <<(*_); self; end
145
+ def puts(*_); self; end
146
+ def print(*_); self; end
147
+ def flush; self; end
148
+ def close; self; end
149
+ def sync=(*_); self; end
150
+ def binmode; self; end
151
+ def tty?; false; end
152
+ def closed?; false; end
153
+ end
154
+ end
155
+ end