Module: RactorRailsShim

Defined in:
lib/ractor_rails_shim.rb,
lib/ractor_rails_shim/version.rb,
lib/ractor_rails_shim/callbacks.rb,
lib/ractor_rails_shim/roles/check.rb,
lib/ractor_rails_shim/patches/core.rb,
lib/ractor_rails_shim/patches/i18n.rb,
lib/ractor_rails_shim/patches/mail.rb,
lib/ractor_rails_shim/patches/rack.rb,
lib/ractor_rails_shim/patches/devise.rb,
lib/ractor_rails_shim/patches/marcel.rb,
lib/ractor_rails_shim/patches/warden.rb,
lib/ractor_rails_shim/roles/freezers.rb,
lib/ractor_rails_shim/patches/openssl.rb,
lib/ractor_rails_shim/roles/installer.rb,
lib/ractor_rails_shim/roles/lifecycle.rb,
lib/ractor_rails_shim/patches/kaminari.rb,
lib/ractor_rails_shim/patches/rubygems.rb,
lib/ractor_rails_shim/roles/worker_app.rb,
lib/ractor_rails_shim/foundation/funnel.rb,
lib/ractor_rails_shim/patches/callables.rb,
lib/ractor_rails_shim/patches/propshaft.rb,
lib/ractor_rails_shim/callbacks/registry.rb,
lib/ractor_rails_shim/foundation/storage.rb,
lib/ractor_rails_shim/roles/fallback_ies.rb,
lib/ractor_rails_shim/foundation/registry.rb,
lib/ractor_rails_shim/foundation/run_mode.rb,
lib/ractor_rails_shim/patches/action_view.rb,
lib/ractor_rails_shim/patches/orm_adapter.rb,
lib/ractor_rails_shim/patches/url_helpers.rb,
lib/ractor_rails_shim/patches/activerecord.rb,
lib/ractor_rails_shim/patches/rails_module.rb,
lib/ractor_rails_shim/patches/route_helpers.rb,
lib/ractor_rails_shim/roles/ar_model_walker.rb,
lib/ractor_rails_shim/roles/pre_spawn_steps.rb,
lib/ractor_rails_shim/patches/active_storage.rb,
lib/ractor_rails_shim/patches/active_support.rb,
lib/ractor_rails_shim/patches/make_shareable.rb,
lib/ractor_rails_shim/patches/mattr_accessor.rb,
lib/ractor_rails_shim/roles/callback_capture.rb,
lib/ractor_rails_shim/roles/fallback_builder.rb,
lib/ractor_rails_shim/roles/install_strategy.rb,
lib/ractor_rails_shim/foundation/ies_accessor.rb,
lib/ractor_rails_shim/patches/action_dispatch.rb,
lib/ractor_rails_shim/patches/class_attribute.rb,
lib/ractor_rails_shim/roles/app_shareabilizer.rb,
lib/ractor_rails_shim/foundation/role_defaults.rb,
lib/ractor_rails_shim/foundation/version_check.rb,
lib/ractor_rails_shim/roles/worker_app_factory.rb,
lib/ractor_rails_shim/foundation/const_reassign.rb,
lib/ractor_rails_shim/foundation/version_policy.rb,
lib/ractor_rails_shim/patches/action_controller.rb,
lib/ractor_rails_shim/patches/execution_wrapper.rb,
lib/ractor_rails_shim/patches/zeitwerk_registry.rb,
lib/ractor_rails_shim/patches/polymorphic_routes.rb,
lib/ractor_rails_shim/foundation/storage_strategy.rb,
lib/ractor_rails_shim/patches/active_record_store.rb,
lib/ractor_rails_shim/roles/logger_io_neutralizer.rb,
lib/ractor_rails_shim/callbacks/symbolic_transport.rb,
lib/ractor_rails_shim/roles/constant_shareabilizer.rb,
lib/ractor_rails_shim/roles/shareability_traversal.rb,
lib/ractor_rails_shim/patches/active_model_attribute.rb,
lib/ractor_rails_shim/patches/hash_compute_if_absent.rb,
lib/ractor_rails_shim/patches/activerecord_reflection.rb,
lib/ractor_rails_shim/patches/active_record_model_schema.rb,
lib/ractor_rails_shim/callbacks/dependent_association_transport.rb

Overview

Callbacks::DependentAssociationTransport — replays a model's dependent: association cascades on the :destroy kind. This is the reference transport for a callback that is a LAMBDA (the dependent: option registers an unshareable before_destroy lambda) but whose effect reduces to a shareable, declarative spec: (class_name => [type:, macro:]).

On :destroy, for each captured dependent association the transport calls record.association(name).handle_dependency — the exact method the original lambda invoked — so :destroy / :delete / :nullify / :restrict_* all dispatch correctly (that dispatch lives in AR's handle_dependency).

Source shape (frozen into SHAREABLE_DEPENDENT_ASSOCIATIONS):

{ class_name(String) => [ {name: Symbol, type: Symbol, macro: Symbol},  ] }

Keyed by class name (not object_id) because dependent: replay looks up the record's own class, not its ancestors (a class only cascades the associations it itself declared).

Defined Under Namespace

Modules: ARModelWalker, ActionDispatchStrategy, ActiveModelAttributePatch, ActiveModelAttributeRegistrationPatch, ActiveRecordAttributesPatch, ActiveRecordModelSchemaPatch, ActiveRecordStoreInstancePatch, ActiveRecordStorePatch, ActiveStorageAttachedPatch, AppShareabilizer, CallbackCapture, Callbacks, ConstReassign, ConstantShareabilizer, FallbackBuilder, Freezers, Funnel, IESAccessor, InstallStrategy, Installer, Lifecycle, LoggerIONeutralizer, MarcelPatch, Patches, PreSpawnSteps, ReflectionAbstractPatch, ReflectionAssociationPatch, ReflectionMacroPatch, ReflectionMemoPatch, ReflectionThroughPatch, Registry, RoleDefaults, RunMode, ShareabilityTraversal, Storage, StorageStrategy, Version, VersionPolicy, WorkerAppFactory Classes: ArWorkerInitWrapper, Callable, CallableConst, Check, DeviseMappingSnapshot, NoOpCond, NoOpLock, NoOpLogDev, NoOpProc, WorkerApp

Constant Summary collapse

VERSION =
"0.4.0"
KEYS =

The keys under which each global is stored in IsolatedExecutionState. Namespaced to avoid collisions with Rails' own uses of IES.

{
  application: :ractor_rails_shim_application,
  app_class: :ractor_rails_shim_app_class,
  cache: :ractor_rails_shim_cache,
  logger: :ractor_rails_shim_logger,
  env: :ractor_rails_shim_env,
  backtrace_cleaner: :ractor_rails_shim_backtrace_cleaner
}.freeze
CLASS_ATTRIBUTES =

The nine shared registries. Issue #35 (Round 4): storage OWNS the mutable registries (Array/Hash) — the facade constants below are the SAME object as Registry's instance variables, so appends are visible through both paths. The frozen (shareable) registries are swapped via _reassign_shareable_const which updates BOTH the facade constant (for the string-eval'd code that reads RactorRailsShim::SHAREABLE_FALLBACK) and Registry (so role objects that read Registry.shareable_fallback see the new value). New code should prefer RactorRailsShim::Registry.

Registry.class_attributes
MATTR_DEFAULTS =
Registry.mattr_defaults
CLASS_ATTR_VALUES =
Registry.class_attr_values
SHAREABLE_MATTR_DEFAULTS =
Registry.shareable_mattr_defaults
SHAREABLE_CONSTANTS =
Registry.shareable_constants
SHAREABLE_CLASS_IVARS =
Registry.shareable_class_ivars
ABSTRACT_REGISTRY =
Registry.abstract_registry
VIEW_CONTEXT_REGISTRY =
Registry.view_context_registry
SHAREABLE_FALLBACK =
Registry.shareable_fallback
SHAREABLE_PENDING_ATTR_MODS =

Per-model pending_attribute_modifications (custom attribute macros: user defaults, type decorators) captured in the main Ractor during prepare_for_ractors! and read by workers (whose class-ivar space is separate from main's under kino). Keyed by model object_id so a worker can find its model's pending modifications. Reassigned (frozen + shareable) at prepare time; models with unshareable modifications (e.g. a Proc-backed decorator) are simply omitted, degrading to [] in workers.

{}.freeze
SHAREABLE_GEN_ATTR_METHODS =

Shareable map of { model_object_id => Module } capturing each AR model's in main). Worker Ractors read this to reuse the pre-built attribute methods instead of creating an empty Module (which would leave name=, id=, etc. undefined and cause DelegationError on ActiveStorage::Attachment).

{}.freeze
PATCH_VERSIONS =

Registry of patch names → tested Rails version segments. Owned by VersionPolicy; the constant here is an alias so the historical RactorRailsShim::PATCH_VERSIONS reference keeps working.

RactorRailsShim::VersionPolicy::PATCH_VERSIONS
UnsupportedVersionError =

Alias for backward compatibility — errors are catchable as either RactorRailsShim::UnsupportedVersionError or RactorRailsShim::VersionPolicy::UnsupportedVersionError.

RactorRailsShim::VersionPolicy::UnsupportedVersionError
SUPPORTED_RUBY =
RactorRailsShim::Version::SUPPORTED_RUBY
SUPPORTED_RAILS =
"8.1"
NON_DISPATCHED_FRAMEWORK_PATCHES =

install*_patch methods called from OTHER install paths, not from the dispatcher. The constant + the dispatcher live on Installer (extracted Issue #13, Step 13.6); kept as a facade delegation so the existing framework_patch_dispatch_spec (which reads the dispatcher's source location) and version_spec keep passing. See Installer for the contract.

RactorRailsShim::Installer::NON_DISPATCHED_FRAMEWORK_PATCHES
FILES_LOC =

Source-location constant used by make_app_shareable!'s proc-replacement graph traversal (moved here from make_shareable.rb so the Rack concern's pieces live together).

"/rack/files.rb".freeze
DEVISE_SCOPE_LOC =

Source-location constant for the Devise scope constraint Proc (moved from make_shareable.rb so the Devise-related callable lives with the Devise patch).

"/devise/rails/routes.rb".freeze
FallbackIES =
Storage::ThreadLocal
SHAREABLE_COMPILED_MODULE =

Shareable, mutable module that holds compiled template methods (e.g. _app_views_...). ActionView attaches compiled template methods to compiled_method_container; the default returns a per-class container, which isolates the shared application layout's compiled method to whichever controller first rendered it (so other controllers raise NoMethodError on the layout). Routing every view_context_class to this ONE shared module makes compiled methods available to all controllers/workers. It is a plain Module (shareable without freezing, so workers can still define methods on it).

Module.new
AR_PRIMARY_KEYS_SHAREABLE =

Shareable snapshot of each AR model class's primary_key, captured at prepare time. Workers read this instead of the raw @primary_key class ivar (which is initialized to PRIMARY_KEY_NOT_SET, a BasicObject that can't be made shareable). Populated by _share_model_classes! in the main ractor.

Ractor.make_shareable({})
SHAREABLE_DEPENDENT_ASSOCIATIONS =

Shareable table of dependent: associations, captured at prepare time so worker Ractors can replay them. The Rails dependent: <type> option registers a LAMBDA before_destroy filter (->(o) { o.association(reflection.name).handle_dependency }). A lambda is an unshareable Proc, so the model's __callbacks chain can't be made Ractor-shareable and workers get an EMPTY destroy chain — meaning dependent children are never deleted/cascade in workers (FK-violation 500). We instead capture (model_name => [type:, macro:]) and, in a worker's empty :destroy chain, call record.association(name).handle_dependency directly (the exact method the original lambda invoked). Entries are Hashes of Symbols/booleans -> natively shareable.

nil
PgBindBlock =

Shareable callable that replaces Arel::Visitors::PostgreSQL::BIND_BLOCK (a Proc proc { |i| "$#{i}" }). Callable cross-Ractor.

Ractor.make_shareable(Object.new.tap do |o|
  def o.call(i); "$#{i}"; end
  def o.to_proc; method(:call).to_proc; end
end)
SqlBindBlock =

Shareable callable that replaces Arel::Visitors::ToSql::BIND_BLOCK (a Proc proc { "?" }). Callable cross-Ractor.

Ractor.make_shareable(Object.new.tap do |o|
  def o.call(_i = nil); "?"; end
  def o.to_proc; method(:call).to_proc; end
end)
AR_CONFIGURATIONS_SNAPSHOT =

Shareable snapshot of ActiveRecord::Base.configurations at prepare_for_ractors! time. Workers read this to establish their own connection pools with the same db config. Made shareable (frozen).

nil
AR_CONFIGURATIONS_SHAREABLE =

Shareable (deep-frozen) copy of ActiveRecord::Base.configurations (the DatabaseConfigurations object) captured at prepare time. Workers read this instead of the raw @@configurations class variable, which a non-main Ractor cannot access.

nil
AR_DB_CONFIG_HANDLERS_SHAREABLE =

Shareable (deep-frozen) copy of DatabaseConfigurations.db_config_handlers (an Array of shareable handler Procs) captured at prepare time. Workers read this instead of the per-Ractor class instance variable.

nil
AR_QUERY_TRANSFORMERS_SHAREABLE =

Shareable (deep-frozen) copy of ActiveRecord.query_transformers (an Array of transformer classes/objects) captured at prepare time. Workers read this instead of the per-Ractor class instance variable.

nil
ACTIVE_STORAGE_PREFIX =

Shareable constants holding ActiveStorage's table-name prefix/suffix. Seeded with the real (string) values in main at prepare_for_ractors! time (see _seed_active_storage_prefix!); workers read these instead of the un-shareable mattr_accessor readers. Reassigned (not mutated) in main.

"".freeze
ACTIVE_STORAGE_SUFFIX =
"".freeze
SSL_LOC =

Source-location constants used by make_app_shareable!'s proc-replacement graph traversal (moved here from make_shareable.rb so each concern's pieces live together).

"/active_dispatch/middleware/ssl.rb".freeze
"/session/cookie_store.rb".freeze
MAPPER_LOC =
"/action_dispatch/routing/mapper.rb".freeze
EMPTY_CALLBACKS_HASH =

Frozen shared sentinel for the __callbacks class_attribute's missing-slot default. Rails callers index the result (__callbacks[:process_action]), so a missing slot must return an empty Hash rather than nil. A single frozen constant avoids the per-read {} allocation the original reader had. Defined on the module (not the singleton class) so it's readable as RactorRailsShim::EMPTY_CALLBACKS_HASH from the eval'd method bodies. Made Ractor-shareable so worker Ractors can read it too.

Ractor.make_shareable({}.freeze)
SHAREABLE_ALLOW_FORGERY =

Captured at prepare_for_ractors! time: the main Ractor's resolved ActionController forgery-protection flag. Replayed in worker Ractors (see _install_action_controller_forgery_patch) because allow_forgery_protection delegates to config.allow_forgery_protection, and a worker's shared config resolves to an EMPTY OrderedOptions -> the flag is lost, so no CSRF token is ever emitted in workers (forms render without an authenticity token, and POST/CSRF validation can't be exercised). A boolean is shareable.

false

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

._abstract_registryObject

Accessor for the abstract-controller registry (written by abstract! in main, read by abstract? in workers). Reassigned to a shareable frozen Hash at prepare_for_ractors! time.



68
69
70
# File 'lib/ractor_rails_shim/patches/core.rb', line 68

def _abstract_registry
  @_abstract_registry
end

._view_context_fallbackObject

Returns the value of attribute _view_context_fallback.



70
71
72
# File 'lib/ractor_rails_shim/patches/core.rb', line 70

def _view_context_fallback
  @_view_context_fallback
end

._view_context_registryObject

Returns the value of attribute _view_context_registry.



69
70
71
# File 'lib/ractor_rails_shim/patches/core.rb', line 69

def _view_context_registry
  @_view_context_registry
end

.storage_strategyObject



287
288
289
290
# File 'lib/ractor_rails_shim/foundation/storage_strategy.rb', line 287

def storage_strategy
  return @storage_strategy if defined?(@storage_strategy)
  RactorRailsShim::RunMode.thread? ? StorageStrategy::Thread : StorageStrategy::Ractor
end

Class Method Details

._apply_active_storage_blob_patchObject

ActiveStorage::Blob is loaded after the attached macros, so it is patched in a later TracePoint event. Both methods here use un-shareable blocks compiled in the main Ractor, which raise "defined with an un-shareable Proc in a different Ractor" when a worker uploads an attachment; reimplement without blocks.



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 369

def _apply_active_storage_blob_patch
  return unless defined?(::ActiveStorage::Blob)
  ::ActiveStorage::Blob.prepend(ActiveStorageAttachedPatch::BlobChecksumPatch)
  ::ActiveStorage::Blob.singleton_class.prepend(ActiveStorageAttachedPatch::BlobBuildPatch)
  # Ensure `ActiveStorage::Blob#metadata` is a serialized store attribute even
  # when the class is freshly autoloaded in a worker Ractor's empty constant
  # namespace. `store :metadata` registers a `Type::Serialized` decorator via
  # `decorate_attributes`, but the decorator is only applied to attributes that
  # already exist in `cast_types` — and `cast_types` is built lazily from the
  # DB `columns_hash`. If the class is loaded before the worker's DB connection
  # is established, `columns_hash` (and thus `cast_types`) is empty, so the
  # `:metadata` decorator is silently skipped and `store_accessor_for` later
  # raises "the column 'metadata' has not been configured as a store". Force
  # the serialized type for `:metadata` directly so `write_store_attribute`
  # works in the worker regardless of load-order.
  ::ActiveStorage::Blob.singleton_class.prepend(ActiveStorageAttachedPatch::BlobMetadataTypePatch)
  ::ActiveStorage::Blob.prepend(ActiveStorageAttachedPatch::ActiveStorageServicePatch)
  @_as_patched_blob = true
end

._apply_active_storage_macro_patchObject



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 337

def _apply_active_storage_macro_patch
  return unless defined?(::ActiveStorage::Attached::Model::ClassMethods)
  ::ActiveStorage::Attached::Model::ClassMethods.prepend(ActiveStorageAttachedPatch)

  # `ActiveStorage.table_name_prefix` / `table_name_suffix` are declared via
  # `mattr_accessor` (railties/engine.rb) whose reader is an un-shareable
  # `define_method` Proc when invoked from a worker Ractor. They feed
  # `ActiveRecord::ModelSchema#full_table_name_prefix`, which
  # `compute_table_name` calls for any ActiveStorage model whose explicit
  # `table_name=` (set in main) is invisible to a worker's per-Ractor IES —
  # so without this fix a worker raises "defined with an un-shareable Proc".
  # Redefine the readers as shareable string-eval `def`s that return the
  # shareable ACTIVE_STORAGE_PREFIX / ACTIVE_STORAGE_SUFFIX constants. Those
  # constants are seeded with the real values in main at prepare_for_ractors!
  # time (see `_seed_active_storage_prefix!`), so workers read the correct
  # prefix ("active_storage_") without crossing the Ractor boundary.
  if defined?(::ActiveStorage)
    ::ActiveStorage.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def table_name_prefix; RactorRailsShim::ACTIVE_STORAGE_PREFIX; end
      def table_name_suffix; RactorRailsShim::ACTIVE_STORAGE_SUFFIX; end
    RUBY
  end

  @_as_patched_macros = true
  _register_patch :active_storage, "8.1"
end

._apply_activerecord_autosave_patchObject



2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2520

def _apply_activerecord_autosave_patch
  return unless defined?(::ActiveRecord::AutosaveAssociation::ClassMethods)

  mod = ::ActiveRecord::AutosaveAssociation::ClassMethods

  # --- save callbacks ---
  mod.alias_method(:_rrs_orig_add_autosave_association_callbacks,
                   :add_autosave_association_callbacks) unless
    mod.method_defined?(:_rrs_orig_add_autosave_association_callbacks)

  mod.module_eval do
    def add_autosave_association_callbacks(reflection)
      # Call the original: registers the callback (after_create/save/etc.)
      # and creates the unshareable define_method method.
      _rrs_orig_add_autosave_association_callbacks(reflection)

      # Immediately redefine the method via string eval (no captured
      # binding). The reflection is stored in the shareable registry.
      save_method = :"autosave_associated_records_for_#{reflection.name}"
      key = [self.name.to_s, save_method.to_s]

      _rrs_store_autosave_reflection(key, reflection)

      if reflection.collection?
        body_call = "save_collection_association(_rrs_autosave_reflection(#{key.inspect}))"
      elsif reflection.has_one?
        body_call = "save_has_one_association(_rrs_autosave_reflection(#{key.inspect}))"
      else
        body_call = "throw(:abort) if save_belongs_to_association(_rrs_autosave_reflection(#{key.inspect})) == false"
      end

      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{save_method}
          @_already_called ||= {}
          return true if @_already_called[#{save_method.inspect}]
          result = true
          begin
            @_already_called[#{save_method.inspect}] = true
            #{body_call}
          ensure
            @_already_called[#{save_method.inspect}] = false
          end
          result
        end
      RUBY
    end
  end

  # --- validation callbacks ---
  mod.alias_method(:_rrs_orig_define_autosave_validation_callbacks,
                   :define_autosave_validation_callbacks) unless
    mod.method_defined?(:_rrs_orig_define_autosave_validation_callbacks)

  mod.module_eval do
    def define_autosave_validation_callbacks(reflection)
      # Call the original: registers the validate callback + creates the
      # unshareable define_method method.
      _rrs_orig_define_autosave_validation_callbacks(reflection)

      validation_method = :"validate_associated_records_for_#{reflection.name}"
      # Only redefine if the original actually created the method.
      return unless method_defined?(validation_method, false)

      key = [self.name.to_s, validation_method.to_s]
      _rrs_store_autosave_reflection(key, reflection)

      if reflection.collection?
        val_method = :validate_collection_association
      elsif reflection.has_one?
        val_method = :validate_has_one_association
      else
        val_method = :validate_belongs_to_association
      end

      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{validation_method}
          @_already_called ||= {}
          return true if @_already_called[#{validation_method.inspect}]
          result = true
          begin
            @_already_called[#{validation_method.inspect}] = true
            send(#{val_method.inspect}, _rrs_autosave_reflection(#{key.inspect}))
          ensure
            @_already_called[#{validation_method.inspect}] = false
          end
          result
        end
      RUBY
    end
  end

  # --- helper methods for registry access ---
  mod.module_eval do
    # Store a reflection in the shareable registry. Called at boot time
    # (main Ractor) during association declaration.
    def _rrs_store_autosave_reflection(key, reflection)
      cur = RactorRailsShim::SHAREABLE_AUTOSAVE_REFLECTIONS.dup
      cur[key] = reflection
      RactorRailsShim.send(:remove_const, :SHAREABLE_AUTOSAVE_REFLECTIONS) if
        RactorRailsShim.const_defined?(:SHAREABLE_AUTOSAVE_REFLECTIONS, false)
      RactorRailsShim.const_set(:SHAREABLE_AUTOSAVE_REFLECTIONS, Ractor.make_shareable(cur))
    end

    # Look up a reflection from the shareable registry. Called at runtime
    # (worker Ractor) inside the string-eval'd method body.
    def _rrs_autosave_reflection(key)
      RactorRailsShim::SHAREABLE_AUTOSAVE_REFLECTIONS[key]
    end
  end
end

._apply_activerecord_reflection_patchObject



307
308
309
310
311
312
313
314
315
# File 'lib/ractor_rails_shim/patches/activerecord_reflection.rb', line 307

def _apply_activerecord_reflection_patch
  return unless defined?(::ActiveRecord::Reflection::AbstractReflection)

  ::ActiveRecord::Reflection::AbstractReflection.prepend(ReflectionAbstractPatch)
  ::ActiveRecord::Reflection::MacroReflection.prepend(ReflectionMacroPatch)
  ::ActiveRecord::Reflection::AssociationReflection.prepend(ReflectionAssociationPatch)
  ::ActiveRecord::Reflection::ThroughReflection.prepend(ReflectionThroughPatch)
  _register_patch :activerecord_reflection, "8.1"
end

._apply_activerecord_scope_patchObject



2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2301

def _apply_activerecord_scope_patch
  return unless defined?(::ActiveRecord::Scoping::Named::ClassMethods)

  # Shareable (frozen) registry of scope source code:
  # { "Model" => { :recent => [body_source, [params]] } }. It MUST be a
  # frozen shareable object so worker Ractors can read it from the shared
  # app graph. But scopes register their bodies during boot eager-load
  # (after this patch installs, before the graph freezes), so each write
  # rebuilds the frozen Hash atomically via const_set. Cost is negligible
  # (scopes are defined only at boot).
  unless RactorRailsShim.const_defined?(:SCOPE_SOURCE_CODES)
    RactorRailsShim.const_set(:SCOPE_SOURCE_CODES, Ractor.make_shareable({}))
  end

  mod = ::ActiveRecord::Scoping::Named::ClassMethods
  mod.module_eval do
    alias_method :_rrs_orig_scope, :scope unless method_defined?(:_rrs_orig_scope)

    def scope(name, body = nil, &block)
      unless body.respond_to?(:call)
        raise ArgumentError, "The scope body needs to be callable."
      end

      if dangerous_class_method?(name)
        raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
          "on the model \"#{self.name}\", but Active Record already defined " \
          "a class method with the same name."
      end

      if method_defined_within?(name, ::ActiveRecord::Relation)
        raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
          "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
          "an instance method with the same name."
      end

      extension = Module.new(&block) if block

      # Extract the body's source code from the lambda's source_location
      # so workers can eval it without calling the original Proc.
      model_name_str = self.name.to_s
      scope_name_str = name.to_s
      body_source = nil
      if body.respond_to?(:source_location)
        file, line = body.source_location
        if file && line
          begin
            source_lines = File.readlines(file)
            raw_line = source_lines[line - 1]&.strip
            if raw_line
              # Extract the body from patterns like:
              #   scope :name, -> { ... }
              #   scope(:name, -> { ... })
              #   scope :name, ->(arg) { ... }
              if raw_line =~ /->\s*(?:\([^)]*\))?\s*\{/
                # Find the matching closing brace
                full_source = raw_line
                depth = 0
                start_idx = raw_line.index("{")
                if start_idx
                  (start_idx...raw_line.length).each do |i|
                    case raw_line[i]
                    when "{"
                      depth += 1
                    when "}"
                      depth -= 1
                      if depth == 0
                        full_source = raw_line[start_idx + 1..i - 1].strip
                        break
                      end
                    end
                  end
                end
                # Also check multi-line if the brace wasn't closed
                if depth > 0
                  ((line)..(line + 10)).each do |ln|
                    next_line = source_lines[ln]&.strip
                    next unless next_line
                    full_source += " " + next_line
                    next_line.each_char do |c|
                      depth += 1 if c == "{"
                      depth -= 1 if c == "}"
                      if depth == 0
                        # Trim the extra }
                        full_source = full_source[0..-(next_line.length - next_line.rindex("}") + 2)]
                        break
                      end
                    end
                    break if depth <= 0
                  end
                end
                body_source = full_source
              end
            end
          rescue StandardError
            nil
          end
        end
      end

      # Store the body_source + parameter names in the shareable constant
      # (cross-Ractor). Parameter names let workers bind the call's args
      # (scopes like `by_title(q)`) without ever referencing the caller's
      # main-Ractor locals.
      if body_source
        param_names = if body.respond_to?(:parameters)
                        body.parameters.map { |p| p[1] }.compact
                      else
                        []
                      end
        cur = RactorRailsShim.const_defined?(:SCOPE_SOURCE_CODES) ? RactorRailsShim::SCOPE_SOURCE_CODES : {}
        cur = cur.dup
        cur[model_name_str] ||= {}
        cur[model_name_str] = cur[model_name_str].dup
        cur[model_name_str][scope_name_str.to_sym] = [body_source, param_names]
        # Reassigning a constant that already exists warns ("already
        # initialized constant"); drop the old binding first so each scope
        # registration stays silent.
        RactorRailsShim.send(:remove_const, :SCOPE_SOURCE_CODES) if RactorRailsShim.const_defined?(:SCOPE_SOURCE_CODES, false)
        RactorRailsShim.const_set(:SCOPE_SOURCE_CODES, Ractor.make_shareable(cur))
      end

      # Also store extension if present. Scope extensions are Modules, which
      # are not shareable across Ractors, so keep them in per-Ractor storage
      # (available in the Ractor that defined the scope; workers without the
      # extension simply skip it — acceptable for the common no-extension case).
      if extension
        Ractor.current[:"rrs_scope_ext_#{model_name_str}_#{scope_name_str}"] = extension
      end

      # Define via string eval (compiled def, not define_method block).
      #
      # CRITICAL: the scope body must be evaluated from a STRING, not a
      # block. A literal block `{ order(...) }` written here is compiled in
      # the main Ractor (where this method is defined) and calling it from a
      # worker raises "defined with an un-shareable Proc in a different
      # Ractor". `instance_eval(string)` compiles the string at CALL time, in
      # the worker Ractor, so it is shareable. Scope args are bridged onto a
      # transient ivar on the relation (`@_rrs_scope_args`) so the body can
      # read them with `self` bound to the relation.
      if body_source
        singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{name}(*args)
            store = RactorRailsShim::SCOPE_SOURCE_CODES[self.name]
            info = store && store[:"#{scope_name_str}"]
            ext_key = :"rrs_scope_ext_\#{self.name}_#{scope_name_str}"
            ext = Ractor.current[ext_key]
            return super(*args) unless info
            body_source, param_names = info
            bind = param_names.each_with_index.map { |n, i| "\#{n} = @_rrs_scope_args[\#{i}]" }.join("; ")
            code = bind.empty? ? body_source : "\#{bind}; \#{body_source}"
            # Capture `all` once: it is a method call that returns a fresh
            # relation each time, so setting the args ivar on one instance
            # and evaluating `code` on another would leave @_rrs_scope_args
            # nil on the eval target (raising "undefined method '[]' for
            # nil"). Use the same relation for both.
            rel = all
            rel.instance_variable_set(:@_rrs_scope_args, args)
            scope = rel.instance_eval(code)
            scope = scope.extending(ext) if ext
            scope
          end
        RUBY
      else
        # Fallback: use the original define_method approach (works in main only).
        if body.respond_to?(:to_proc)
          singleton_class.define_method(name) do |*args|
            scope = all._exec_scope(*args, &body)
            scope = scope.extending(extension) if extension
            scope
          end
        else
          singleton_class.define_method(name) do |*args|
            scope = body.call(*args) || all
            scope = scope.extending(extension) if extension
            scope
          end
        end
        singleton_class.send(:ruby2_keywords, name)
      end

      generate_relation_method(name)
    end
  end
end

._capture_ar_configurations!Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 170

def _capture_ar_configurations!
  return if @_ar_configs_captured
  @_ar_configs_captured = true
  return unless defined?(::ActiveRecord::Base)

  begin
    # Build a plain Hash snapshot of every db config keyed by
    # [env_name][config_name] => config_hash. Use
    # ActiveRecord::Base.configurations.configs_for (returns DbConfig
    # objects with .name, .env_name, .configuration_hash) rather than
    # Rails.application.config.database_configuration (a legacy Hash whose
    # shape differs between single-config apps (flat Hash = the config
    # itself) and multi-config apps (nested Hash of name => config)).
    cfgs = ::ActiveRecord::Base.configurations
    snapshot = {}
    if cfgs.respond_to?(:configs_for)
      cfgs.configs_for.each do |dc|
        env_name = dc.env_name
        name = dc.name || "primary"
        hash = dc.configuration_hash
        next unless hash.is_a?(::Hash)
        snapshot[env_name] ||= {}
        snapshot[env_name][name] = hash.reject { |_k, v| v.nil? }
      end
    end
    # Fallback: legacy database_configuration Hash (env => { name => config }
    # OR env => flat config). Used if configs_for is unavailable.
    if snapshot.empty?
      raw = ::Rails.application.config.database_configuration rescue {}
      raw.each do |env_name, env_configs|
        next unless env_configs.is_a?(::Hash)
        snapshot[env_name] ||= {}
        if env_configs.key?("adapter") || env_configs.key?(:adapter)
          # Flat config: the env value IS the "primary" config itself.
          snapshot[env_name]["primary"] = env_configs.reject { |_k, v| v.nil? }
        else
          # Nested: env => { name => config }
          env_configs.each do |name, config|
            next unless config.is_a?(::Hash)
            snapshot[env_name][name] = config.reject { |_k, v| v.nil? }
          end
        end
      end
    end
    snapshot.freeze
    Ractor.make_shareable(snapshot)
    _reassign_shareable_const(:AR_CONFIGURATIONS_SNAPSHOT, snapshot)
  rescue StandardError => e
    # Best-effort; if we can't capture configs, workers won't be able
    # to auto-init connections. They can call init_worker_ar_connections!
    # manually with explicit configs.
  end
end

._capture_dependent_associations!Object

Capture every AR model's dependent: associations into the shareable SHAREABLE_DEPENDENT_ASSOCIATIONS table so worker Ractors can replay them (see StorageStrategy::Ractor#replay_callbacks! / the :destroy branch of the nil-safe run_callbacks patch). The dependent: lambda filter is an unshareable Proc, so it can't live in the frozen, shared __callbacks chain; instead we record (model_name => [type:, macro:]) and re-drive record.association(name).handle_dependency in workers. Runs in the main Ractor after eager-load (reflections + descendants are ready).



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 530

def _capture_dependent_associations!
  return unless defined?(::ActiveRecord::Base)
  table = {}
  classes = [::ActiveRecord::Base]
  classes.concat(::ActiveRecord::Base.descendants) rescue nil
  classes.each do |klass|
    name = klass.name
    next unless name
    next if klass.respond_to?(:abstract_class?) && klass.abstract_class?
    begin
      next unless klass.respond_to?(:reflect_on_all_associations, true)
      klass.reflect_on_all_associations.each do |refl|
        dep = refl.options[:dependent]
        next unless dep
        (table[name] ||= []) << {
          name: refl.name.to_sym,
          type: dep.to_sym,
          macro: refl.macro,
        }
      end
    rescue StandardError
      nil
    end
  end
  _reassign_shareable_const(
    :SHAREABLE_DEPENDENT_ASSOCIATIONS,
    Ractor.make_shareable(table)
  )
end

._check_version_supportObject

Verify the runtime matches the versions the shim was developed against. The shim's patches target specific Rails 8.1 class layouts and Ruby 4.0 Ractor semantics. On other versions, the patches may silently miss or break things. Behavior on mismatch is governed by version_policy:

:warn   (default) print a warning to $stderr, proceed anyway
:strict raise RactorRailsShim::UnsupportedVersionError
:off    silent (for advanced users / experimentation)

Ruby mismatch always warns (Ractor semantics are not stable across majors); Rails mismatch uses the policy. This is real version detection (Gem::Version-based), not a string-prefix compare, so pre-release and patch versions sort correctly.



257
258
259
# File 'lib/ractor_rails_shim/patches/core.rb', line 257

def _check_version_support
  RactorRailsShim::VersionPolicy.check_version_support
end

._class_attr_methods(method_name, namespaced_name, missing_default) ⇒ Object

Build the reader/writer pair for one method name. ONE body for both run modes — the selected RactorRailsShim.storage_strategy (set once at install from RunMode.thread?) decides the lookup/store backend. method_name is the def name (namespaced or public); namespaced_name is the attribute Symbol (e.g. :mimes_for_respond_to) — the strategy derives a PER-RECEIVER storage key from it at call time so each class keeps its own class_attribute value (and inherits via an ancestor walk), instead of all classes sharing one slot keyed by the declaring module. missing_default is the inlined missing-slot default expression (string of Ruby source — only the Thread strategy consults it; the Ractor strategy relies on IES + SHAREABLE_FALLBACK + CLASS_ATTR_VALUES).



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/ractor_rails_shim/patches/class_attribute.rb', line 162

def _class_attr_methods(method_name, namespaced_name, missing_default)
  # `resolved_key` is a literal symbol baked into the generated method
  # source (interpolated at install time, not per read). The hot reader
  # path reads the resolved value from IES under this literal key, indexed
  # by the receiver's object_id — both are allocation-free, so the
  # regression guard (ShimSpec#test_0009) holds. The ancestor walk lives in
  # `lookup_by_attr` and only runs on the cold (first) read per receiver.
  rkey = :"ractor_rails_shim_resolved_#{namespaced_name}"
  <<~RUBY
    def #{method_name}
      RactorRailsShim.storage_strategy.lookup_resolved(self, :#{namespaced_name}, #{missing_default}, :#{rkey})
    end

    def #{method_name}=(new_value)
      RactorRailsShim.storage_strategy.store_resolved(self, :#{namespaced_name}, new_value, :#{rkey})
      new_value
    end
  RUBY
end

._devise_mapping_replacement(proc_obj, _parent) ⇒ Object

Build a shareable replacement for a Devise scope constraint. The original Proc (devise/rails/routes.rb:363) does:

request.env["devise.mapping"] = Devise.mappings[scope]
true

The scope is captured in the Proc's binding. We call the original Proc once in main with a mock request to capture the mapping, then make it shareable and wrap it in a DeviseMappingCallable.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ractor_rails_shim/patches/warden.rb', line 177

def _devise_mapping_replacement(proc_obj, _parent)
  mock_env = { "devise.mapping" => nil }
  mock_req = Struct.new(:env).new(mock_env)
  begin
    proc_obj.call(mock_req)
  rescue StandardError
  end
  mapping = mock_env["devise.mapping"]
  if mapping
    mapping = _devise_mapping_snapshot(mapping)
  end
  if mapping
    DeviseMappingCallable.new(mapping)
  else
    CallableConst.new(true)
  end
end

._devise_mapping_snapshot(mapping) ⇒ Object



160
161
162
# File 'lib/ractor_rails_shim/patches/callables.rb', line 160

def _devise_mapping_snapshot(mapping)
  _swallow("devise mapping snapshot") { DeviseMappingSnapshot.new(mapping) }
end

._find_files_server(mw) ⇒ Object

Find the Rack::Files (asset) server in the middleware chain, used by make_app_shareable! when replacing the Rack::Head#@app lambda (whose binding receiver is the Rack::Files instance). Moved here from make_shareable.rb so the Rack concern's pieces live together.



149
150
151
152
153
154
155
156
157
158
# File 'lib/ractor_rails_shim/patches/rack.rb', line 149

def _find_files_server(mw)
  cur = mw
  while cur
    if cur.class.name == "ActionDispatch::Static"
      return cur.instance_variable_get(:@file_server)
    end
    cur = cur.instance_variable_get(:@app) rescue nil
  end
  nil
end

._fix_blob_metadata_type!Object

Force-rebuild ActiveStorage::Blob's _default_attributes and @attribute_types in MAIN so the :metadata attribute uses the correct Type::Serialized before the graph is frozen. The store :metadata decorator registers a Type::Serialized via decorate_attributes, but @attribute_types is memoized separately and isn't invalidated when the decorator runs — so it keeps a stale Type::Value. read_attribute uses the attribute set (not type_for_attribute), so the stale type makes read_attribute(:metadata) return a raw String instead of a deserialized Hash, breaking write_store_attribute and identified= in workers. Must run in the main Ractor (writes class ivars).



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 459

def 
  return unless defined?(::ActiveStorage::Blob) && ::Ractor.main?
  blob_cls = ::ActiveStorage::Blob
  # Directly fix the attribute set: replace the :metadata attribute's type
  # with Type::Serialized. The `store :metadata` decorator was applied
  # once during eager-load but only to the (now stale) `_default_attributes`
  # cache. Re-register the decorator and rebuild from scratch.
  attrs = blob_cls._default_attributes
  meta_attr = attrs[:metadata]
  if meta_attr && !meta_attr.type.is_a?(::ActiveRecord::Type::Serialized)
    # Re-run the serialize decorator for :metadata. The original `store
    # :metadata, coder: ActiveRecord::Coders::JSON` uses
    # `build_column_serializer` which instantiates `Coders::JSON.new`
    # (the class itself doesn't respond to dump/load). Reproduce that here.
    coder = ::ActiveRecord::Coders::JSON.new
    ind_coder = ::ActiveRecord::Store::IndifferentCoder.new(:metadata, coder)
    meta_type = ::ActiveRecord::Type::Serialized.new(meta_attr.type, ind_coder)
    # Fix BOTH the string and symbol key entries (the attribute set has
    # both "metadata" and :metadata — read_attribute uses the string key,
    # which has the wrong Type::Text).
    attrs[:metadata] = meta_attr.with_type(meta_type)
    string_attr = attrs["metadata"]
    if string_attr && !string_attr.type.is_a?(::ActiveRecord::Type::Serialized)
      attrs["metadata"] = string_attr.with_type(meta_type)
    end
    # Clear the stale @attribute_types cache so it's rebuilt with the
    # corrected type from the updated attribute set.
    blob_cls.remove_instance_variable(:@attribute_types) if blob_cls.instance_variable_defined?(:@attribute_types)
  end
end

._freeze_class_ivars!(owner) ⇒ Object

Make every unshareable class ivar on owner shareable (deep-freeze) and write it back. A class ivar holding a shareable value is readable from a worker Ractor. Monitor/Mutex->NoOpLock; Concurrent::Map->frozen Hash; values that can't be frozen (Procs, TypeMap) are left as-is.



762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 762

def _freeze_class_ivars!(owner)
  begin
    owner.instance_variables.each do |iv|
      v = owner.instance_variable_get(iv) rescue nil
      next unless v
      next if Ractor.shareable?(v)
      replacement = _shareable_ivar_replacement(v)
      next unless replacement
      begin
        owner.instance_variable_set(iv, replacement)
      rescue StandardError => e
        # frozen owner — leave as-is
      end
    end
  rescue StandardError => e
  end
end

._freeze_journey_visitors!Object

Journey's routing visitors are stored as instance singletons in class constants (e.g. ActionDispatch::Journey::Visitors::Each::INSTANCE). Worker Ractors read these constants while recognizing routes (Node#eachEach::INSTANCE.accept, Path::Pattern#matchoffsetsnode.each), and a non-frozen instance is NOT a shareable object → Ractor::IsolationError: can not access non-shareable objects in constant ...::Each::INSTANCE by non-main Ractor. The visitor instances are stateless, so freezing them makes them shareable with no behavior change. The same applies to the DISPATCH_CACHE Hashes the visitor accept/visit dispatch through. These constants are NOT reachable from the frozen app graph (Ractor.make_shareable never touches them), so we must freeze them explicitly here (in main, before workers spawn).



680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 680

def _freeze_journey_visitors!
  return unless defined?(::ActionDispatch::Journey::Visitors)
  v = ::ActionDispatch::Journey::Visitors
  [[:Each, :INSTANCE], [:String, :INSTANCE], [:Dot, :INSTANCE]].each do |klass, const|
    mod = v.const_get(klass) rescue nil
    next unless mod && mod.const_defined?(const)
    inst = mod.const_get(const)
    inst.freeze if inst.respond_to?(:freeze) && !inst.frozen?
  end
  [[:Visitor, :DISPATCH_CACHE], [:FunctionalVisitor, :DISPATCH_CACHE]].each do |klass, const|
    mod = v.const_get(klass) rescue nil
    next unless mod && mod.const_defined?(const)
    cache = mod.const_get(const)
    cache.freeze if cache.respond_to?(:freeze) && !cache.frozen?
  end
  # GTG::Builder::DUMMY_END_NODE is a non-shareable instance referenced when
  # a worker Ractor rebuilds the route simulator (e.g. if the warmed
  # @simulator cache is missing on the frozen graph). Make it Ractor-shareable
  # (deep-freeze) so workers can read the constant without
  # Ractor::IsolationError. It is a stateless dummy node, so this is
  # behavior-preserving.
  if defined?(::ActionDispatch::Journey::GTG::Builder) &&
     ::ActionDispatch::Journey::GTG::Builder.const_defined?(:DUMMY_END_NODE)
    node = ::ActionDispatch::Journey::GTG::Builder.const_get(:DUMMY_END_NODE)
    _swallow("make journey dummy end node shareable") { Ractor.make_shareable(node) }
  end
end

._freeze_mime_negotiation!Object

ActionDispatch::Http::MimeNegotiation holds module-level constants (e.g. RESCUABLE_MIME_FORMAT_ERRORS, an Array of exception classes) that are referenced from the request path (params_readable? -> rescue * RESCUABLE_MIME_FORMAT_ERRORS). These Arrays are non-frozen, hence non-shareable, so a worker Ractor raises Ractor::IsolationError when it reads them. Freeze the mutable constant-containing modules so workers can read shareable copies. Regexp/Class constants are already shareable; only the wrapping Array/Hash need freezing.



778
779
780
781
782
783
784
785
786
787
788
789
790
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 778

def _freeze_mime_negotiation!
  return unless defined?(::ActionDispatch::Http::MimeNegotiation)
  mod = ::ActionDispatch::Http::MimeNegotiation
  mod.constants.each do |name|
    c = mod.const_get(name) rescue nil
    next unless c.is_a?(::Array) || c.is_a?(::Hash)
    next if c.frozen?
    c.freeze
    _swallow("make mime negotiation constant shareable") { ::Ractor.make_shareable(c) }
  end
rescue StandardError => e
  warn "[ractor-rails-shim] _freeze_mime_negotiation!: #{e.class}: #{e.message}"
end

._freeze_secure_random_alphabets!Object

Freeze SecureRandom::BASE36_ALPHABET / BASE58_ALPHABET if they exist and are not yet shareable. Idempotent — safe to call from install, from prepare_for_ractors!, and from the TracePoint callback.



320
321
322
323
324
325
326
327
328
329
330
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 320

def _freeze_secure_random_alphabets!
  return unless defined?(::SecureRandom)
  %i[BASE36_ALPHABET BASE58_ALPHABET].each do |const|
    next unless ::SecureRandom.const_defined?(const)
    alphabet = ::SecureRandom.const_get(const)
    next if ::Ractor.shareable?(alphabet)
    ::SecureRandom.const_set(const, ::Ractor.make_shareable(alphabet))
  rescue StandardError
    nil
  end
end

._install_abstract_controller_patchObject

Patch AbstractController::Base.controller_path to not write/read the action_methods, clear_action_methods!, abstract!, abstract?, and _prefixes to route through IES or use the shareable fallback.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 95

def _install_abstract_controller_patch
  return if @abstract_controller_patched
  @abstract_controller_patched = true
  _register_patch :abstract_controller, "8.1"
  return unless defined?(::AbstractController::Base)
  ac = ::AbstractController::Base

  # Populate the shareable abstract registry from every loaded controller
  # class's @abstract ivar (set by abstract! / inherited at boot). Workers
  # read this via the patched abstract? (per-class values can't live in
  # per-Ractor IES).
  registry = {}
  ac.descendants.each do |klass|
    begin
      registry[klass] = klass.instance_variable_get(:@abstract) if klass.instance_variable_defined?(:@abstract)
    rescue StandardError => e
      # ignore — best-effort
    end
  end
  registry[ac] = ac.instance_variable_get(:@abstract) if ac.instance_variable_defined?(:@abstract)
  registry.freeze
  Ractor.make_shareable(registry)
  self._abstract_registry = registry
  ac.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def controller_path
      cache = (RactorRailsShim.storage[:ractor_rails_shim_controller_path_cache] ||= {})
      v = cache[self]
      return v if v
      if Ractor.main? && instance_variable_defined?(:@controller_path)
        v = @controller_path
        cache[self] = v
        return v
      end
      computed = anonymous? ? nil : name.delete_suffix("Controller").underscore
      cache[self] = computed
      computed
    end

    # action_methods: `@action_methods ||= public_instance_methods(true) -
    # internal_methods).map(&:name).to_set` — raw class-ivar lazy init.
    # The value is a Set of Symbols (shareable once frozen). Route through
    # IES; workers compute it from public_instance_methods (no ivar read)
    # and cache in their own slot. Read per-request during dispatch.
    def action_methods
      cache = (RactorRailsShim.storage[:ractor_rails_shim_action_methods_cache] ||= {})
      v = cache[self]
      return v if v
      if Ractor.main? && instance_variable_defined?(:@action_methods)
        v = @action_methods
        cache[self] = v
        return v
      end
      methods = public_instance_methods(true) - internal_methods
      methods.map!(&:name)
      computed = methods.to_set
      cache[self] = computed
      computed
    end

    def clear_action_methods!
      if Ractor.main?
        @action_methods = nil
      end
      RactorRailsShim.storage[:ractor_rails_shim_action_methods_cache] = nil
    end

    # abstract! / abstract / abstract? — raw class ivar (@abstract), a
    # per-CLASS boolean. IES is per-Ractor (single value), so we can't use a
    # single IES key for all classes. Instead use a shareable registry
    # (Hash class→bool) built at prepare_for_ractors! time. Workers read
    # the registry; main reads its live @abstract ivar (set by abstract!
    # / inherited). `internal_methods` loops on abstract?.
    #
    # The registry is frozen with `Ractor.make_shareable` at install time so
    # it can travel the shared app graph. Mutating a frozen Hash raises
    # FrozenError, so `abstract!` only writes the live ivar (main) and
    # guards the registry write behind a mutability check. `abstract!`
    # after install in main is a no-op on the registry (already captured);
    # callers that need to flip a class to abstract post-install should
    # rebuild the registry (rare — abstract! is a boot-time declaration).
    def abstract!
      reg = RactorRailsShim._abstract_registry
      reg[self] = true if reg && !reg.frozen? && Ractor.main?
      @abstract = true if Ractor.main?
    end

    def abstract
      if Ractor.main? && instance_variable_defined?(:@abstract)
        @abstract
      else
        (RactorRailsShim._abstract_registry || RactorRailsShim::ABSTRACT_REGISTRY)[self] || false
      end
    end
    alias_method :abstract?, :abstract
  RUBY

  # Patch ActionView::ViewPaths::ClassMethods#_prefixes (overrides any
  # Base version). Original: `@_prefixes ||= begin; return local_prefixes
  # if superclass.abstract?; local_prefixes + superclass._prefixes; end`.
  # @_prefixes is a per-CLASS class ivar (workers can't read). Recurse
  # using the patched abstract? and cache in a per-Ractor Hash by class.
  if defined?(::ActionView::ViewPaths::ClassMethods)
    vp = ::ActionView::ViewPaths::ClassMethods
    vp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def _prefixes
        cache = (RactorRailsShim.storage[:ractor_rails_shim_vp_prefixes_cache] ||= {})
        v = cache[self]
        return v if v
        if Ractor.main? && instance_variable_defined?(:@_prefixes)
          v = @_prefixes
          cache[self] = v
          return v
        end
        computed = if superclass.respond_to?(:abstract?) && superclass.abstract?
          local_prefixes
        elsif superclass.respond_to?(:_prefixes)
          local_prefixes + superclass._prefixes
        else
          local_prefixes
        end
        cache[self] = computed
        computed
      end
    RUBY
  end

  # AbstractController::UrlFor::ClassMethods#action_methods ALSO has a
  # `@action_methods ||= ...` lazy init (it overrides Base.action_methods
  # to subtract route helper names). Patch it the same way.
  if defined?(::AbstractController::UrlFor::ClassMethods)
    url_for_cm = ::AbstractController::UrlFor::ClassMethods
    url_for_cm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def action_methods
        cache = (RactorRailsShim.storage[:ractor_rails_shim_url_for_action_methods_cache] ||= {})
        v = cache[self]
        return v if v
        if Ractor.main? && instance_variable_defined?(:@action_methods)
          v = @action_methods
          cache[self] = v
          return v
        end
        # NOTE: the original reads `@action_methods ||= if _routes; super -
        # _routes.named_routes.helper_names; else; super; end`. But
        # `_routes` is a singleton method defined via `define_method` with
        # a block (route_set.rb:610), capturing the defining Ractor's
        # binding → "defined with an un-shareable Proc in a different
        # Ractor" when called from a worker. Instead, read the route set
        # directly from the shareable Rails.application (frozen, shared).
        base = super
        routes = Ractor.main? ? (respond_to?(:_routes) ? _routes : nil) : (defined?(::Rails) && ::Rails.application ? ::Rails.application.routes : nil)
        computed = if routes
          base - routes.named_routes.helper_names
        else
          base
        end
        cache[self] = computed
        computed
      end
    RUBY
  end
end

._install_action_controller_controller_name_patchObject

Patch ActionController::Metal.controller_name (a class method). It memoizes its computed String in a lazy class ivar (@controller_name ||=), which a worker Ractor cannot write. Route the cache through IsolatedExecutionState keyed by the class name so each Ractor builds its own copy; the computation is deterministic from the class name.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 262

def _install_action_controller_controller_name_patch
  return if @action_controller_controller_name_patched
  @action_controller_controller_name_patched = true
  _register_patch :action_controller_controller_name, "8.1"
  return unless defined?(::ActionController::Metal)
  ::ActionController::Metal.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def controller_name
      key = :"ractor_rails_shim_controller_name_\#{name}"
      v = RactorRailsShim.storage[key]
      return v if v
      cn = (name.demodulize.delete_suffix("Controller").underscore unless anonymous?)
      RactorRailsShim.storage[key] = cn
      cn
    end
  RUBY
end

._install_action_controller_forgery_patchObject

Replay ActionController's forgery-protection flag in worker Ractors. allow_forgery_protection delegates to config.allow_forgery_protection; in a worker the shared config is an empty ActiveSupport::OrderedOptions, so forms never render a CSRF token (breaking token issuance/validation in workers). Capture the resolved flag from the main Ractor at prepare time (after any boot-time override) and force it in workers so token issuance/validation work off the frozen, shared graph.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 61

def _install_action_controller_forgery_patch
  return if @action_controller_forgery_patched
  @action_controller_forgery_patched = true
  _register_patch :action_controller_forgery, "8.1"
  return unless defined?(::ActionController::RequestForgeryProtection)
  return unless defined?(::ActionController::Base)

  # Capture the resolved flag. ActionController::Base.allow_forgery_protection
  # is a class method that reads config; in the main Ractor config carries
  # the boot-time override. Booleans are shareable.
  _reassign_shareable_const(
    :SHAREABLE_ALLOW_FORGERY,
    !!::ActionController::Base.allow_forgery_protection
  )

  mod = ::ActionController::Base
  # Override the instance-method delegation (used by protect_against_forgery?).
  mod.module_eval do
    def allow_forgery_protection
      ::Ractor.main? ? super : ::RactorRailsShim::SHAREABLE_ALLOW_FORGERY
    end
  end
  # Override the class-method delegation (singleton delegate to :config).
  mod.singleton_class.module_eval do
    def allow_forgery_protection
      ::Ractor.main? ? super : ::RactorRailsShim::SHAREABLE_ALLOW_FORGERY
    end
  end
end

._install_action_dispatch_http_url_patchObject

ActionDispatch::Http::URL reads the tld_length class variable DIRECTLY (@@tld_length) in normalize_host and in the default-parameter of domain/subdomains/subdomain. Class variables are not readable from a non-main Ractor, so a worker raises "Ractor::IsolationError: can not access class variables ... @@tld_length". The shim routes the mattr_accessor :tld_length READER through IES, but the literal @@tld_length references bypass that reader. Replace them with the accessor method (which the shim's mattr_accessor rewrite makes worker-safe). domain/subdomains/subdomain live in the Url module mixed into ActionDispatch::Request, so patch that module too.



911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 911

def _install_action_dispatch_http_url_patch
  return if @action_dispatch_http_url_patched
  @action_dispatch_http_url_patched = true
  _register_patch :action_dispatch_http_url, "8.1"
  return unless defined?(::ActionDispatch::Http::URL)

  url = ::ActionDispatch::Http::URL
  # normalize_host is a module_function: build_host_url calls the
  # MODULE-LEVEL copy, so redefining the instance method alone leaves the
  # original (@@tld_length-reading) one in place. Patch the singleton
  # (module-level) method instead.
  url.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def normalize_host(_host, options)
      return _host unless named_host?(_host)
      tld_length = options[:tld_length] || tld_length()
      subdomain  = options.fetch :subdomain, true
      domain     = options[:domain]
      host = +""
      if subdomain == true
        return _host if domain.nil?
        host << extract_subdomains_from(_host, tld_length).join(".")
      elsif subdomain
        host << subdomain.to_param
      end
      host << "." unless host.empty?
      host << (domain || extract_domain_from(_host, tld_length))
      host
    end
  RUBY

  if defined?(::ActionDispatch::Http::URL::Url)
    ::ActionDispatch::Http::URL::Url.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def domain(tld_length = tld_length())
        ActionDispatch::Http::URL.extract_domain(host, tld_length)
      end
      def subdomains(tld_length = tld_length())
        ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
      end
      def subdomain(tld_length = tld_length())
        ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
      end
    RUBY
  end
end

._install_action_dispatch_mounted_helpers_patchObject

Patch ActionDispatch::Routing::RouteSet::MountedHelpers#main_app (and its _main_app worker). main_app is define_method-ed at boot capturing the MAIN ractor's RouteSet + url_helpers in its block binding, so calling it from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor". Devise's _devise_route_context calls send(:main_app) to get the route context for its url helpers. Redefine via string eval, building the RoutesProxy from the shareable RouteSet (RactorRailsShim::SHAREABLE_ROUTES) so workers get a valid context.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 183

def _install_action_dispatch_mounted_helpers_patch
  return if @mounted_helpers_patched
  @mounted_helpers_patched = true
  _register_patch :mounted_helpers, "8.1"
  return unless defined?(::ActionDispatch::Routing::RouteSet::MountedHelpers)
  mh = ::ActionDispatch::Routing::RouteSet::MountedHelpers
  return unless mh.method_defined?(:main_app)
  mh.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def _main_app
      ::ActionDispatch::Routing::RoutesProxy.new(
        RactorRailsShim::SHAREABLE_ROUTES,
        _routes_context,
        RactorRailsShim::SHAREABLE_ROUTES.url_helpers,
        nil
      )
    end
    def main_app
      @_main_app ||= _main_app
    end
  RUBY
end

._install_action_dispatch_routing_patchObject

Patch ActionDispatch::Routing::RouteSet URL generation. The named route helpers (post_path, session_path, ...) are generated at boot in the main Ractor by RouteSet#add, which captures the PATH / UNKNOWN lambda constants (route_set.rb:349-350) into each helper's url_strategy ivar. Those lambdas were defined in the main Ractor, so calling them from a worker Ractor raises RuntimeError: defined with an un-shareable Proc in a different Ractor. Replace them with shareable Callable objects (Plain old objects with a #call method, made shareable via Ractor.make_shareable) that delegate to ActionDispatch::Http::URL (module methods, callable from any Ractor).



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 215

def _install_action_dispatch_routing_patch
  return if @action_dispatch_routing_patched
  @action_dispatch_routing_patched = true
  _register_patch :action_dispatch_routing, "8.1"
  return unless defined?(::ActionDispatch::Routing::RouteSet)
  return unless defined?(::ActionDispatch::Http::URL)

  # Shareable Callable replacements for the PATH / UNKNOWN lambda constants.
  unless RactorRailsShim.const_defined?(:AVPathStrategy)
    RactorRailsShim.const_set(:AVPathStrategy,
      Ractor.make_shareable(Object.new.tap do |o|
        def o.call(options)
          ActionDispatch::Http::URL.path_for(options)
        end
      end))
  end
  unless RactorRailsShim.const_defined?(:AVUnknownStrategy)
    RactorRailsShim.const_set(:AVUnknownStrategy,
      Ractor.make_shareable(Object.new.tap do |o|
        def o.call(options)
          ActionDispatch::Http::URL.url_for(options)
        end
      end))
  end

  rs = ::ActionDispatch::Routing::RouteSet
  # `RouteSet::PATH` / `RouteSet::UNKNOWN` (route_set.rb:349-350) are lambdas
  # defined in the main Ractor. They are referenced as default parameter
  # values (`def url_for(..., url_strategy = UNKNOWN, ...)`) and inside
  # `path_for`/`define_url_helper`. Reading those constants from a worker
  # Ractor raises `Ractor::IsolationError: can not access non-shareable
  # objects in constant ...UNKNOWN`. Replace them with the shareable
  # Callable objects (which perform the identical `ActionDispatch::Http::URL`
  # lookups) so workers read a shareable constant instead of an unshareable
  # lambda. Behaviour is unchanged in main (same `#call(options)` contract).
  unless rs.const_defined?(:PATH) && Ractor.shareable?(rs.const_get(:PATH))
    verbose = $VERBOSE
    $VERBOSE = nil
    rs.const_set(:PATH, RactorRailsShim::AVPathStrategy)
    rs.const_set(:UNKNOWN, RactorRailsShim::AVUnknownStrategy)
    $VERBOSE = verbose if defined?(verbose)
  end
  # `ActionDispatch::Journey::Router::Utils::ENCODER` (`UriEncoder.new`) and
  # its sibling constants (`DEC2HEX`, `EMPTY`, `US_ASCII`, the unreserved/
  # segment regexes, ...) are referenced by `escape_path`/`escape_segment`,
  # which the journey URL formatter invokes while building a path in a worker
  # Ractor. An unfrozen object/array/string held in a constant is unshareable,
  # so workers reading it raise IsolationError. Freeze each constant in place
  # (via `Ractor.make_shareable`) so the shareable-frozen values are readable
  # from any Ractor.
  if defined?(::ActionDispatch::Journey::Router::Utils)
    utu = ::ActionDispatch::Journey::Router::Utils
    utu.constants.each do |c|
      begin
        v = utu.const_get(c)
        Ractor.make_shareable(v) if v && !Ractor.shareable?(v)
      rescue StandardError
        nil
      end
    end
  end
  # `RouteSet::RESERVED_OPTIONS` (route_set.rb:838) is a mutable Array used
  # as a default parameter value in `url_for`/`path_for`. A non-frozen Array
  # is unshareable, so workers reading the constant raise IsolationError.
  # Freeze it in place so the constant becomes shareable.
  begin
    Ractor.make_shareable(rs.const_get(:RESERVED_OPTIONS))
  rescue StandardError
    nil
  end
  # Warm the lazy (memoized) caches on every Journey route and its
  # Path::Pattern BEFORE `make_app_shareable!` deep-freezes them. Several of
  # these caches are filled with `||=` (e.g. `Route#parts`,
  # `Route#required_parts`, `Route#required_defaults`,
  # `Path::Pattern#requirements_for_missing_keys_check`, `#to_regexp`,
  # `#offsets`, `#required_names`, `#optional_names`). They are computed
  # deterministically, but assigning the memoized ivar on a frozen object
  # from a worker Ractor raises FrozenError. Computing them here (in main,
  # while the objects are still mutable) populates the ivars so the frozen,
  # shared copies already hold the values and workers only read them.
  if Ractor.main? && defined?(::Rails) && ::Rails.application
    _swallow("warm journey routes") do
      rset = ::Rails.application.routes
      all = []
      all.concat(rset.named_routes.send(:routes).values) rescue nil
      all.concat(rset.set.routes) rescue nil
      all.uniq.each do |route|
        next unless route.respond_to?(:path)
        route.parts rescue nil
        route.required_parts rescue nil
        route.required_defaults rescue nil
        p = route.path
        p.requirements_for_missing_keys_check rescue nil
        p.to_regexp rescue nil
        p.offsets rescue nil
        p.required_names rescue nil
        p.optional_names rescue nil
      end
    end
  end
  # Capture the (shareable) RouteSet so workers can build URLs without
  # calling `#_routes` — which is `define_method(:_routes) { @_routes ||
  # routes }` (route_set.rb:612), a block capturing the main Ractor's
  # `routes` reference. Calling that block from a worker raises
  # "defined with an un-shareable Proc in a different Ractor". We stash the
  # RouteSet as a shareable constant and point `_routes` at it.
  if Ractor.main?
    begin
      routes = Rails.application.routes if defined?(::Rails) && ::Rails.application
      unless routes.nil?
        verbose = $VERBOSE
        $VERBOSE = nil
        RactorRailsShim.const_set(:SHAREABLE_ROUTES, routes) unless RactorRailsShim.const_defined?(:SHAREABLE_ROUTES)
      end
    rescue StandardError
      nil
    ensure
      $VERBOSE = verbose if defined?(verbose)
    end
  end

  # RouteSet#url_for receives url_strategy (the captured PATH/UNKNOWN
  # lambda) and calls `url_strategy.call options` internally. Coerce a
  # non-shareable strategy to the shareable Callable before delegating.
  unless rs.method_defined?(:url_for_without_shim)
    rs.alias_method(:url_for_without_shim, :url_for)
  end
  rs.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def url_for(options, route_name = nil, url_strategy = UNKNOWN, method_name = nil, reserved = RESERVED_OPTIONS)
      url_strategy = RactorRailsShim::AVUnknownStrategy unless Ractor.shareable?(url_strategy)
      url_for_without_shim(options, route_name, url_strategy, method_name, reserved)
    end
  RUBY

  # OptimizedUrlHelper#call invokes `url_strategy.call options` DIRECTLY
  # (route_set.rb:228) without going through url_for, so the coercion above
  # doesn't cover it. Redefine it to call the shareable strategy Callable
  # (the one passed in by our redefined helper methods), replicating the
  # original body exactly otherwise.
  ::ActionDispatch::Routing::RouteSet::NamedRouteCollection::UrlHelper::OptimizedUrlHelper.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def call(t, method_name, args, inner_options, url_strategy)
      if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
        options = t.url_options.merge @options
        path = optimized_helper(args)
        path << "/" if options[:trailing_slash] && !path.end_with?("/")
        options[:path] = path
        original_script_name = options.delete(:original_script_name)
        script_name = t._routes.find_script_name(options)
        if original_script_name
          script_name = original_script_name + script_name
        end
        options[:script_name] = script_name
        strat = Ractor.shareable?(url_strategy) ? url_strategy : RactorRailsShim::AVPathStrategy
        strat.call(options)
      else
        super
      end
    end
  RUBY

  # The base (non-optimized) `UrlHelper#call` (route_set.rb:278) is hit
  # whenever a helper is generated as a plain `UrlHelper` (e.g. our re-run
  # loop) or when optimization is skipped. The original reads `t.url_options`
  # and `t._routes`, both of which assume `t` is a controller/view context
  # whose `_routes`/`url_options` are reachable from a worker. In practice
  # `t` may be the `NamedRouteCollection` (helpers proxy) or any object that
  # lacks these. Route both through the shareable RouteSet / url-options
  # snapshot, falling back to `t`'s own accessors only when it actually
  # provides them (real controller/view). This makes path generation
  # (host-independent) work from any Ractor regardless of `t`.
  ::ActionDispatch::Routing::RouteSet::NamedRouteCollection::UrlHelper.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def call(t, method_name, args, inner_options, url_strategy)
      begin
        controller_options = t.url_options
      rescue StandardError
        controller_options = RactorRailsShim::URL_OPTIONS_DEFAULTS || {}
      end
      options = controller_options.merge @options
      hash = handle_positional_args(controller_options, inner_options || {}, args, options, @segment_keys)
      begin
        routes = t._routes
      rescue StandardError
        routes = RactorRailsShim::SHAREABLE_ROUTES
      end
      routes.url_for(hash, route_name, url_strategy, method_name)
    end
  RUBY

  # Named route helpers (`post_path`, `post_url`, ...) are generated by
  # `NamedRouteCollection#define_url_helper` (route_set.rb:333) via
  # `mod.define_method(name) { |*args| ... helper.call(...) }` — a BLOCK that
  # captures the `helper` object (an OptimizedUrlHelper holding the route)
  # and the `url_strategy` lambda (PATH/UNKNOWN, both defined in main).
  # Calling that block from a worker Ractor raises
  # "defined with an un-shareable Proc in a different Ractor" before any
  # code runs. Patch `define_url_helper` to (a) make the helper shareable
  # via `Ractor.make_shareable` (deep-freeze; routes are read-only after
  # boot) and stash it in a shareable Hash keyed by name, and (b) define the
  # method with a STRING (no captured binding) that references the Hash and
  # the shareable strategy Callable directly.
  unless RactorRailsShim.const_defined?(:URL_HELPERS)
    RactorRailsShim.const_set(:URL_HELPERS, {})
  end
  nrc = ::ActionDispatch::Routing::RouteSet::NamedRouteCollection
  unless nrc.method_defined?(:define_url_helper_without_shim)
    nrc.alias_method(:define_url_helper_without_shim, :define_url_helper)
  end
  nrc.define_method(:define_url_helper) do |mod, name, helper, url_strategy|
    # Detach the helper from the live route object before deep-freezing
    # it for cross-Ractor sharing. The non-optimized UrlHelper#call only
    # needs @options / @segment_keys / @route_name to build the options
    # hash and then delegates to `t._routes.url_for(route_name, ...)`,
    # which looks the route up in the (shareable) RouteSet by name. The
    # @route reference would pull the whole route graph into the freeze,
    # freezing objects that make_app_shareable! must still be able to
    # mutate (e.g. Devise route constraints) -> FrozenError.
    RactorRailsShim::Funnel.swallow("freeze url helper") do
      if helper.respond_to?(:instance_variable_get)
        helper.instance_variable_set(:@route, nil) rescue nil
        opts = helper.instance_variable_get(:@options)
        helper.instance_variable_set(:@options, opts.dup.freeze) rescue nil
        segs = helper.instance_variable_get(:@segment_keys)
        helper.instance_variable_set(:@segment_keys, segs.dup.freeze) rescue nil
      end
      helper = Ractor.make_shareable(helper)
    end
    RactorRailsShim::URL_HELPERS[name] = helper
    strategy_const = url_strategy.equal?(::ActionDispatch::Routing::RouteSet::PATH) ?
      "AVPathStrategy" : "AVUnknownStrategy"
    mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{name}(*args)
        last = args.last
        options = \\
          case last
          when ::Hash
            args.pop
          when ::ActionController::Parameters
            args.pop.to_h
          end
        RactorRailsShim::URL_HELPERS[#{name.inspect}].call(
          self, #{name.inspect}, args, options, RactorRailsShim::#{strategy_const})
      end
    RUBY
  end

  # Re-run the (now patched) helper generation for every route already
  # drawn at boot, so the helpers the app actually uses are worker-safe.
  # Routes are drawn during `Rails.application.initialize!`, which runs
  # before `prepare_for_ractors!`, so the originals are still block-based.
  if Ractor.main? && defined?(::Rails) && ::Rails.application
    begin
      named = ::Rails.application.routes.named_routes
      path_mod = named.instance_variable_get(:@path_helpers_module)
      url_mod = named.instance_variable_get(:@url_helpers_module)
      named.send(:routes).each do |route_name, route|
        # Build the helper directly via `UrlHelper.new` (NOT `UrlHelper.create`,
        # which calls `optimize_helper?` -> `route.glob?` -> `route.path.ast.glob?`
        # and `route.path.ast` is nil by the time routes are finalized post-boot).
        helper = ::ActionDispatch::Routing::RouteSet::NamedRouteCollection::UrlHelper.new(
          route, route.defaults, route_name)
        named.define_url_helper(path_mod, :"#{route_name}_path", helper, ::ActionDispatch::Routing::RouteSet::PATH) if path_mod
        named.define_url_helper(url_mod, :"#{route_name}_url", helper, ::ActionDispatch::Routing::RouteSet::UNKNOWN) if url_mod
      end
      verbose = $VERBOSE
      $VERBOSE = nil
      RactorRailsShim.const_set(:URL_HELPERS, Ractor.make_shareable(RactorRailsShim::URL_HELPERS)) unless Ractor.shareable?(RactorRailsShim::URL_HELPERS)
    rescue StandardError
      nil
    ensure
      $VERBOSE = verbose if defined?(verbose)
    end
  end

  # `ActionController::UrlFor#url_options` (action_controller/metal/url_for.rb:45)
  # builds its option hash from `request.host` / `request.optional_port` /
  # `request.protocol` / `request.path_parameters` and merges in
  # `default_url_options`. The controller instance rendered in a worker Ractor
  # has a `request` built from the shared Rack env, but the values it returns
  # (and the `default_url_options` class value, an unshareable Hash stored as a
  # class ivar on ActionController::Base) cannot be read/called from a worker
  # without raising Ractor isolation errors. For path-only helpers (the common
  # case in views) the host/port/protocol are irrelevant, and `default_url_options`
  # is the same deterministic value everywhere, so capture it once in main as a
  # shareable snapshot and have workers use it directly, skipping the
  # request-derived portion.
  unless RactorRailsShim.const_defined?(:URL_OPTIONS_DEFAULTS)
    begin
      if Ractor.main? && defined?(::ActionController::Base)
        defaults = ::ActionController::Base.default_url_options
        defaults = defaults.dup.freeze if defaults.respond_to?(:freeze)
        RactorRailsShim.const_set(:URL_OPTIONS_DEFAULTS, Ractor.make_shareable(defaults))
      end
    rescue StandardError
      nil
    end
  end
  if defined?(::ActionController::UrlFor)
    ::ActionController::UrlFor.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def url_options
        return super if Ractor.main?
        @_url_options ||= begin
          opts = (RactorRailsShim::URL_OPTIONS_DEFAULTS || {}).dup
          begin
            req = request if respond_to?(:request)
            if req
              opts[:host] = req.host if opts[:host].nil? && req.respond_to?(:host)
              opts[:protocol] = req.protocol if opts[:protocol].nil? && req.respond_to?(:protocol)
              opts[:port] = req.port if opts[:port].nil? && req.respond_to?(:port)
              opts[:_recall] = req.path_parameters if req.respond_to?(:path_parameters)
            end
          rescue StandardError
            nil
          end
          opts.freeze
        end
      end
    RUBY
  end

  # NOTE: the block-based `_routes` accessors that break workers are now
  # fixed at their source by `_install_url_helpers_patch` (patches/
  # url_helpers.rb), which intercepts `Module#redefine_singleton_method`
  # / `Module#define_method` for `:_routes` and replaces the main-Ractor
  # block with a string-eval'd method returning `Rails.application.routes`.
  # No per-class enumeration needed.
end

._install_action_view_field_type_patchObject

Patch ActionView::Helpers::Tags::TextField.field_type (and the subclasses EmailField/PasswordField/... that inherit it). The original memoizes its computed String in a lazy class ivar (@field_type ||= name...). The class ivar is per-subclass and unshareable-writable from a worker Ractor. Route the cache through IsolatedExecutionState keyed by the class name so each Ractor builds its own copy; the computation is deterministic.



645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 645

def _install_action_view_field_type_patch
  return if @action_view_field_type_patched
  @action_view_field_type_patched = true
  _register_patch :action_view_field_type, "8.1"
  return unless defined?(::ActionView::Helpers::Tags::TextField)
  tf = ::ActionView::Helpers::Tags::TextField
  tf.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def field_type
      key = :"ractor_rails_shim_field_type_\#{name}"
      v = RactorRailsShim.storage[key]
      return v if v
      ft = name.split("::").last.sub("Field", "").downcase
      RactorRailsShim.storage[key] = ft
      ft
    end
  RUBY
end

._install_action_view_partial_path_patchObject

Patch ActionView::AbstractRenderer::ObjectRendering#partial_path. The original reads PREFIXED_PARTIAL_NAMES — a Concurrent::Map constant (nested Concurrent::Maps) — and writes a nested entry via PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= .... Concurrent::Map is intrinsically unshareable (it refuses #freeze), so a worker Ractor cannot read the constant NOR write to it. Redefine the method to use a per-Ractor Hash via IsolatedExecutionState (each Ractor builds its own cache from merge_prefix_into_object_path, which is deterministic).



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 615

def _install_action_view_partial_path_patch
  return if @action_view_partial_path_patched
  @action_view_partial_path_patched = true
  _register_patch :action_view_partial_path, "8.1"
  return unless defined?(::ActionView::AbstractRenderer)
  ::ActionView::AbstractRenderer::ObjectRendering.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def partial_path(object, view)
      object = object.to_model if object.respond_to?(:to_model)
      path = if object.respond_to?(:to_partial_path)
        object.to_partial_path
      else
        raise ArgumentError.new("\#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
      end
      if view.prefix_partial_path_with_controller_namespace
        cache = (RactorRailsShim.storage[:ractor_rails_shim_prefixed_partial_names] ||= {})
        cache[@context_prefix] ||= {}
        cache[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
      else
        path
      end
    end
  RUBY
end

._install_action_view_resolver_patchObject

Patch ActionView::FileSystemResolver#_find_all. The original reads the resolver's @unbound_templates cache, which make_app_shareable! rewrites from a Concurrent::Map into a frozen Hash (Concurrent::Map refuses #freeze). The original then calls cache.compute_if_absent (a Concurrent::Map API) on it, which a frozen Hash lacks -> NoMethodError in a worker Ractor. Route the per-virtual-path cache through IsolatedExecutionState instead: each Ractor builds its own mutable Hash (deterministic from disk via unbound_templates_from_path), so the frozen shareable app graph is never mutated.



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 497

def _install_action_view_resolver_patch
  return if @action_view_resolver_patched
  @action_view_resolver_patched = true
  _register_patch :action_view_resolver, "8.1"
  return unless defined?(::ActionView::FileSystemResolver)
  # @unbound_templates is a Concurrent::Map (resolver.rb). Ractor.make_shareable!
  # cannot freeze a Concurrent::Map ("undefined method 'freeze' for an
  # instance of Concurrent::Map"), so the entire view_paths PathSet fails to
  # become shareable and workers fall back to an EMPTY view-path Hash -> every
  # request renders "No template found". The per-virtual-path lookup cache is
  # already routed through IsolatedExecutionState by the _find_all patch
  # below, so @unbound_templates is never read at request time. Replace it
  # with a plain (freezable) Hash so make_shareable! can deep-freeze each
  # resolver and the PathSet becomes shareable across Ractors.
  ::ActionView::FileSystemResolver.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def initialize(path)
      raise ArgumentError, "path already is a Resolver class" if path.is_a?(::ActionView::Resolver)
      @unbound_templates = {}
      @path_parser = ::ActionView::Resolver::PathParser.new
      @path = ::File.expand_path(path)
      super()
    end
  RUBY
  if Ractor.main?
    # The resolvers already created during boot live in the PathRegistry's
    # @view_paths_by_class / @file_system_resolvers class ivars (keyed by
    # class name). They were built with the original initialize, so their
    # @unbound_templates is a Concurrent::Map. Convert every such resolver
    # to a plain (freezable) Hash so make_app_shareable! can deep-freeze the
    # PathSet and workers get a non-empty view-path fallback.
    resolvers = []
    if ::ActionView::PathRegistry.instance_variable_defined?(:@view_paths_by_class)
      ::ActionView::PathRegistry.instance_variable_get(:@view_paths_by_class).each_value do |ps|
        resolvers.concat(ps.to_a) if ps.respond_to?(:to_a)
      end
    end
    if ::ActionView::PathRegistry.instance_variable_defined?(:@file_system_resolvers)
      ::ActionView::PathRegistry.instance_variable_get(:@file_system_resolvers).each_value do |v|
        resolvers.concat(v.to_a) if v.respond_to?(:to_a)
      end
    end
    converted = 0
    resolvers.uniq.each do |r|
      if r.instance_variable_defined?(:@unbound_templates) &&
         r.instance_variable_get(:@unbound_templates).is_a?(::Concurrent::Map)
        r.instance_variable_set(:@unbound_templates, {})
        converted += 1
      end
    end
  end
  # Eager-load nested constants referenced below (workers can't autoload).
  if Ractor.main?
    ::ActionView::TemplateDetails rescue nil
    ::ActionView::TemplatePath rescue nil
  end
  ::ActionView::FileSystemResolver.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def _find_all(name, prefix, partial, details, key, locals)
      requested_details = key || ::ActionView::TemplateDetails::Requested.new(**details)
      virtual = ::ActionView::TemplatePath.virtual(name, prefix, partial)
      # Key the cache by resolver path AND virtual path: each resolver
      # (app/views, each gem) has its own @path and its own templates.
      # Keying only by virtual path would let the first resolver poison
      # the cache for all others (e.g. app/views caches [] for
      # devise/sessions/new, hiding the template that lives in the
      # devise gem resolver).
      cache = (RactorRailsShim.storage[:ractor_rails_shim_resolver_cache] ||= {})
      cache_key = [@path, virtual]
      unbound_templates =
        if cache.key?(cache_key)
          cache[cache_key]
        else
          path = ::ActionView::TemplatePath.build(name, prefix, partial)
          tmpls = unbound_templates_from_path(path)
          cache[cache_key] = tmpls
          tmpls
        end
      filter_and_sort_by_details(unbound_templates, requested_details).map do |unbound_template|
        unbound_template.bind_locals(locals)
      end
    end
  RUBY

  # Patch ActionView::Resolver::PathParser#parse. The resolver's
  # @path_parser instance is part of the shareable app graph frozen by
  # make_app_shareable!, and the original method memoizes its compiled
  # regex in `@regex ||= build_path_regex` — assigning @regex on a frozen
  # object raises FrozenError in a worker Ractor. Route the memoization
  # through IsolatedExecutionState keyed by the parser's object_id, so each
  # Ractor compiles its own regex once without mutating the frozen object.
  if defined?(::ActionView::Resolver::PathParser)
    pp = ::ActionView::Resolver::PathParser
    pp_key = :ractor_rails_shim_path_parser_regex
    pp_key_str = pp_key.inspect
    pp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def parse(path)
        regex = RactorRailsShim.storage[:"#{pp_key_str}_\#{object_id}"] ||= build_path_regex
        match = regex.match(path)
        path = ::ActionView::TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
        details = ::ActionView::TemplateDetails.new(
          match[:locale]&.to_sym,
          match[:handler]&.to_sym,
          match[:format]&.to_sym,
          match[:variant]&.to_sym
        )
        ::ActionView::Resolver::PathParser::ParsedPath.new(path, details)
      end
    RUBY
  end
end

._install_action_view_safe_join_patchObject

Patch ActionView::Helpers::OutputSafetyHelper#safe_join. Its default separator parameter is sep = $, — a reference to the $ global, which a worker Ractor cannot read (Ractor::IsolationError: can not access global variable $,). The $ global is nil in every normal Rails process, so defaulting to nil reproduces the identical behaviour without touching the global.



669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 669

def _install_action_view_safe_join_patch
  return if @action_view_safe_join_patched
  @action_view_safe_join_patched = true
  _register_patch :action_view_safe_join, "8.1"
  return unless defined?(::ActionView::Helpers::OutputSafetyHelper)
  ::ActionView::Helpers::OutputSafetyHelper.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def safe_join(array, sep = nil)
      sep = ERB::Util.unwrapped_html_escape(sep)
      array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
    end
  RUBY
end

._install_action_view_sanitize_patchObject

ActionView::Helpers::SanitizeHelper::ClassMethods memoizes its sanitizer instances as class ivars on the (shared, frozen) module (@safe_list_sanitizer / @full_sanitizer / @link_sanitizer). A non-main worker Ractor cannot set an ivar on a class/module defined in the main Ractor, so sanitize / simple_format raise "can not set instance variables of classes/modules by non-main Ractors". Route the memoization through a per-worker cache (Ractor.current) and build the sanitizer lazily inside the worker — the value never has to cross the Ractor boundary, so no shareability gymnastics are needed.



691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 691

def _install_action_view_sanitize_patch
  return if @action_view_sanitize_patched
  @action_view_sanitize_patched = true
  _register_patch :action_view_sanitize, "8.1"
  return unless defined?(::ActionView::Helpers::SanitizeHelper::ClassMethods)

  mod = ::ActionView::Helpers::SanitizeHelper::ClassMethods
  mod.module_eval do
    def safe_list_sanitizer
      Ractor.current[:__rrs_safe_list_sanitizer__] ||= sanitizer_vendor.safe_list_sanitizer.new
    end
    def full_sanitizer
      Ractor.current[:__rrs_full_sanitizer__] ||= sanitizer_vendor.full_sanitizer.new
    end
    def link_sanitizer
      Ractor.current[:__rrs_link_sanitizer__] ||= sanitizer_vendor.link_sanitizer.new
    end
  end
end

._install_actionmailer_mailer_name_patchObject

--- ActionMailer::Base.mailer_name (raw class ivar on the mailer subclass) --- mailer_name (aliased to controller_path) computes the mailer's view path prefix from its class name, but reads the lazy @mailer_name class ivar — unreadable from a worker Ractor ("can not get unshareable values from instance variables of classes/modules from non-main Ractors"). The value defaults to name.underscore, derivable from the (shareable) class name.

Redefining mailer_name/controller_path directly does NOT help: every internal caller captures the original method object through a stale cross-Ractor inline cache, and controller_path is an alias bound to that original method object. Instead we override the three internal callers (each a small, stable method) via string-eval def, so the methods the worker actually invokes derive the prefix from the shareable class name directly — never touching @mailer_name.



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/ractor_rails_shim/patches/mail.rb', line 421

def self._install_actionmailer_mailer_name_patch
  return unless defined?(::ActionMailer::Base)
  return if @am_mailer_name_patched
  @am_mailer_name_patched = true

  # `ActionView::ViewPaths::ClassMethods#local_prefixes` (used by the mailer
  # view-path lookup) returns `[controller_path]`, and `controller_path` is an
  # alias for `ActionMailer::Base#mailer_name`. Override `local_prefixes`
  # itself (the method actually in the call stack) so mailer view prefixes are
  # derived from the shareable class `name` directly — never touching
  # `@mailer_name`.
  if defined?(::ActionView::ViewPaths::ClassMethods)
    amod = ::ActionView::ViewPaths::ClassMethods
    unless amod.private_instance_methods.include?(:rrs_original_local_prefixes)
      amod.alias_method :rrs_original_local_prefixes, :local_prefixes
    end
    amod.class_eval do
      def local_prefixes
        if defined?(::ActionMailer::Base) && self <= ::ActionMailer::Base
          [name.underscore]
        else
          rrs_original_local_prefixes
        end
      end
    end
  end

  # The three internal callers of `mailer_name`:
  #   * `collect_responses_from_templates` — template path lookup
  #   * `default_i18n_subject`             — I18n scope
  #   * `instrument_payload`               — ActiveSupport::Notifications payload
  # Re-implement each to use `self.class.name.underscore` (the same value
  # `mailer_name` would produce) without ever reading `@mailer_name`.
  #
  # Also make `PROTECTED_IVARS` (an unfrozen Array constant built at class-body
  # eval) shareable — otherwise a worker Ractor raises IsolationError reading
  # the non-shareable constant from `_protected_ivars`.
  unless ::Ractor.shareable?(::ActionMailer::Base::PROTECTED_IVARS)
    ::ActionMailer::Base.const_set(:PROTECTED_IVARS,
      ::Ractor.make_shareable(::ActionMailer::Base::PROTECTED_IVARS))
  end
  ::ActionMailer::Base.class_eval do
    def collect_responses_from_templates(headers)
      templates_path = headers[:template_path] ||
        (self.class.anonymous? ? "anonymous" : self.class.name.underscore)
      templates_name = headers[:template_name] || action_name

      each_template(Array(templates_path), templates_name).map do |template|
        format = template.format || self.formats.first
        {
          body: render(template: template, formats: [format]),
          content_type: Mime[format].to_s
        }
      end
    end

    def default_i18n_subject(interpolations = {})
      mailer_scope = (self.class.anonymous? ? "anonymous" : self.class.name.underscore).tr("/", ".")
      I18n.t(:subject, **interpolations, scope: [mailer_scope, action_name], default: action_name.humanize)
    end

    def instrument_payload(key)
      {
        mailer: self.class.anonymous? ? "anonymous" : self.class.name.underscore,
        key: key
      }
    end

    # `ActionMailer::Base#config` (a `class_attribute` on
    # `AbstractController::Base`) resolves to `nil` inside a worker Ractor
    # because the shim's frozen shareable fallback doesn't cover this mailer
    # instance receiver. `ActionView::Helpers::ControllerHelper#assign_controller`
    # then calls `controller.config.inheritable_copy` on nil and raises. Fall
    # back to an empty `OrderedOptions` (the class_attribute's own default) so
    # mailer view setup proceeds; the real config is only needed for delivery,
    # which routes through `Mail.delivery_method` (patched separately).
    def config
      super || ActiveSupport::OrderedOptions.new
    end
  end
end

._install_active_model_attribute_method_patterns_patchObject

ActiveModel::AttributeMethods::ClassMethods#attribute_method_patterns_cache stores a mutable Concurrent::Map in a CLASS instance variable (@attribute_method_patterns_cache). That ivar is unshareable, so reading it from a worker Ractor raises Ractor::IsolationError ("can not get unshareable values from instance variables of classes/modules from non-main Ractors") — hit on the write path via redirect_to @post -> respond_to? -> matched_attribute_method -> attribute_method_patterns_cache.

Unlike @relation_delegate_cache (populated once, freezable), this map is mutated lazily per method_name (compute_if_absent) during request handling, so it cannot be frozen. Instead route it through Ractor-local storage: each Ractor gets its own Concurrent::Map, shared by all of its threads. The cache content is deterministic (a pure function of the class's attribute_method_patterns), so per-Ractor recomputation is correct.



353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 353

def _install_active_model_attribute_method_patterns_patch
  return if @am_amp_patched
  @am_amp_patched = true
  _register_patch :active_model_attribute_method_patterns, "8.1"
  return unless defined?(::ActiveModel::AttributeMethods)

  mod = ::ActiveModel::AttributeMethods::ClassMethods
  mod.module_eval do
    def attribute_method_patterns_cache
      store = Ractor.current[:__am_attribute_method_patterns_cache__] ||= {}
      store[object_id] ||= Concurrent::Map.new(initial_capacity: 4)
    end
  end
end

._install_active_model_attribute_patchObject

See patches/active_model_attribute.rb. When the frozen :ractor graph is built, each model class's _default_attributes template (and the FromDatabase instances within it) is deep-frozen. Attribute#dup_or_share returns self for immutable column types, so a worker's NEW record would share a frozen Attribute and raise FrozenError on first read/write. This patch makes a frozen receiver yield a fresh, mutable Attribute so writes (POST/create) work in workers. No-op in normal (unfrozen) Rails. Delegates to RactorRailsShim::Patches::ActiveModelAttribute.install (extracted Step 22.2, Issue #22). The idempotency flag now lives on the role object. See Patches::ActiveModelAttribute for the contract (the three prepend targets + the ActiveModel::Attribute guard).



326
327
328
# File 'lib/ractor_rails_shim/patches/core.rb', line 326

def _install_active_model_attribute_patch
  Patches::ActiveModelAttribute.install
end

._install_active_model_conversion_patchObject

Patch ActiveModel::Conversion::ClassMethods#_to_partial_path to route its lazy class-ivar cache (@_to_partial_path ||= ...) through IsolatedExecutionState. The cache holds a deterministic String derived from model_name, so each Ractor can build its own. Without this, the first render @posts / render post in a worker Ractor writes the class ivar and dies with Ractor::IsolationError: can not set instance variables of classes/modules by non-main Ractors. Seen via ActionView's CollectionRenderer#render_collection_derive_partial -> to_partial_path.



1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1289

def _install_active_model_conversion_patch
  return if @active_model_conversion_patched
  @active_model_conversion_patched = true
  _register_patch :active_model_conversion, "8.1"
  return unless defined?(::ActiveModel::Conversion)
  amc = ::ActiveModel::Conversion
  amc.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    module ClassMethods
      def _to_partial_path
        key = :"ractor_rails_shim_to_partial_path_\#{name}"
        v = RactorRailsShim.storage[key]
        return v if v
        path = if respond_to?(:model_name)
          "\#{model_name.collection}/\#{model_name.element}"
        else
          element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
          collection = ActiveSupport::Inflector.tableize(name)
          "\#{collection}/\#{element}"
        end
        path = path.freeze
        RactorRailsShim.storage[key] = path
        path
      end
    end
  RUBY
end

._install_active_model_naming_patchObject

(an ActiveModel::Name holding unfrozen, unshareable Strings) on the model class. From a worker Ractor that write raises Ractor::IsolationError ("can not set instance variables of classes/modules by non-main Ractors") and reading the unshareable value raises too. Route the cache through IsolatedExecutionState (keyed by model object_id) so each Ractor builds and keeps its own ActiveModel::Name without touching the shared class ivar.



1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1076

def _install_active_model_naming_patch
  return if @am_naming_patched
  @am_naming_patched = true
  _register_patch :active_model_naming, "8.1"
  return unless defined?(::ActiveModel::Naming)
  mod = ::ActiveModel::Naming
  mod.module_eval do
    def model_name
      if Ractor.main?
        @_model_name ||= _rrs_compute_model_name
      else
        store = (RactorRailsShim.storage[:rrs_model_names] ||= {})
        store[object_id] ||= _rrs_compute_model_name
      end
    end

    private

    def _rrs_compute_model_name
      namespace = module_parents.detect do |n|
        n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
      end
      ::ActiveModel::Name.new(self, namespace)
    end
  end
end

._install_active_record_core_patchObject

Patch ActiveRecord::Core::ClassMethods#arel_table / #predicate_builder / #type_caster. Each memoizes an unshareable value (@arel_table is an Arel::Table, @predicate_builder a PredicateBuilder) on the shared model class. From a worker Ractor the ||= write raises Ractor::IsolationError, and reading the unshareable cached value also raises. Build + cache each per-Ractor via IsolatedExecutionState (keyed by model object_id); main keeps the original class-ivar behavior.



985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 985

def _install_active_record_core_patch
  return if @ar_core_patched
  @ar_core_patched = true
  _register_patch :active_record_core, "8.1"
  return unless defined?(::ActiveRecord::Core::ClassMethods)
  mod = ::ActiveRecord::Core::ClassMethods
  mod.module_eval do
    def arel_table
      if Ractor.main?
        @arel_table ||= ::Arel::Table.new(table_name, klass: self)
      else
        store = (RactorRailsShim.storage[:rrs_arel_tables] ||= {})
        store.fetch(object_id) { store[object_id] = ::Arel::Table.new(table_name, klass: self) }
      end
    end

    def predicate_builder
      if Ractor.main?
        @predicate_builder ||= ::ActiveRecord::PredicateBuilder.new(
          ::ActiveRecord::TableMetadata.new(self, arel_table))
      else
        store = (RactorRailsShim.storage[:rrs_predicate_builders] ||= {})
        store.fetch(object_id) do
          store[object_id] = ::ActiveRecord::PredicateBuilder.new(
            ::ActiveRecord::TableMetadata.new(self, arel_table))
        end
      end
    end

    def type_caster
      if Ractor.main?
        @type_caster ||= ::ActiveRecord::TypeCaster::Map.new(self)
      else
        store = (RactorRailsShim.storage[:rrs_type_casters] ||= {})
        store.fetch(object_id) { store[object_id] = ::ActiveRecord::TypeCaster::Map.new(self) }
      end
    end

    # inspection_filter memoizes @inspection_filter as a class ivar on the
    # (shared, frozen) model class. From a worker Ractor that write raises
    # Ractor::IsolationError ("can not set instance variables of
    # classes/modules by non-main Ractors") — e.g. whenever a model is
    # inspected during a request (backtraces, journey route-formatter
    # Hash#inspect, debug output). Route the memoization through
    # IsolatedExecutionState keyed by the model class instead, preserving
    # the original superclass-delegation semantics.
    def inspection_filter
      store = (RactorRailsShim.storage[:rrs_inspection_filters] ||= {})
      return store[self] if store.key?(self)
      result = if @filter_attributes.nil?
        superclass.inspection_filter
      else
        mask = ::ActiveRecord::Core.const_get(:InspectionMask).new(ActiveSupport::ParameterFilter::FILTERED)
        ActiveSupport::ParameterFilter.new(@filter_attributes, mask: mask)
      end
      store[self] = result
    end
  end
end

._install_active_record_inheritance_patchObject

Patch ActiveRecord::Inheritance::ClassMethods#finder_needs_type_condition?. It memoizes @finder_needs_type_condition (a Symbol) on the shared model class via @ivar ||=. From a worker Ractor that write raises Ractor::IsolationError. Route the value through IsolatedExecutionState (keyed by model object_id); main keeps the original class-ivar behavior.



1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1050

def _install_active_record_inheritance_patch
  return if @ar_inheritance_patched
  @ar_inheritance_patched = true
  _register_patch :active_record_inheritance, "8.1"
  return unless defined?(::ActiveRecord::Inheritance::ClassMethods)
  mod = ::ActiveRecord::Inheritance::ClassMethods
  mod.module_eval do
    def finder_needs_type_condition?
      if Ractor.main?
        :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
      else
        store = (RactorRailsShim.storage[:rrs_finder_type_cond] ||= {})
        store.fetch(object_id) do
          store[object_id] = descends_from_active_record? ? false : true
        end
      end
    end
  end
end

._install_active_record_model_schema_patchObject

Patch ActiveRecord::ModelSchema::ClassMethods so worker Ractors do not write the @table_name (and related) class ivars on the shared model class. table_name memoizes via reset_table_name unless defined?(@table_name), and reset_table_name calls self.table_name = which writes @table_name/@arel_table/etc. From a worker that write is Ractor::IsolationError. Route the value through IsolatedExecutionState (keyed by model object_id); main keeps the original class-ivar behavior.

NOTE on naming: there are TWO ModelSchema-related install methods, with deliberately different scopes:

* `_install_active_record_model_schema_patch`  (this method) — patches
`table_name` / `table_name=` / `reset_table_name` only.
* `_install_activerecord_model_schema_patch`  (below) — patches the
*column-derived* lazy caches `symbol_column_to_string`,
`content_columns` (and previously `column_defaults`, now provided
by the prepended `ActiveRecordModelSchemaPatch`).

Both target ActiveRecord::ModelSchema::ClassMethods but patch disjoint method sets; do not collapse them without auditing every caller.



837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 837

def _install_active_record_model_schema_patch
  return if @ar_model_schema_patched
  @ar_model_schema_patched = true
  _register_patch :active_record_model_schema, "8.1"
  return unless defined?(::ActiveRecord::ModelSchema::ClassMethods)
  mod = ::ActiveRecord::ModelSchema::ClassMethods
  mod.module_eval do
    # `columns_hash` reads `@columns_hash`. The original only loads via
    # `load_schema` when `@columns_hash` is nil. In a worker Ractor, class
    # instance variables are NOT shared with the main Ractor (reading
    # `@columns_hash` set in main raises Ractor::IsolationError, or — under
    # kino's worker Ractors, which do not share main's class-ivar space —
    # returns the worker's own nil view). So `load_schema!` (patched to a
    # no-op in workers) never populates it, and the model's schema is
    # permanently nil in workers. Mirror the worker `table_name` patch
    # (below): cache the loaded schema per-Ractor in IES
    # (ActiveSupport::IsolatedExecutionState, readable from every worker
    # Ractor) instead of the un-shareable class ivar. Main keeps the
    # original class-ivar path (frozen into the shared graph).
    def columns_hash
      if ::Ractor.main? || abstract_class?
        load_schema if @columns_hash.nil?
        @columns_hash
      else
        store = (RactorRailsShim.storage[:rrs_columns_hash] ||= {})
        cached = store[self.object_id]
        return cached if cached

        cols = connection_pool.schema_cache.columns_hash(table_name)
        if cols.nil? || cols.empty?
          # Cache was cold (fresh worker pool). Force a load from the DB
          # via the (lazily established) worker connection.
          begin
            cols = connection.columns(table_name).index_by(&:name)
          rescue StandardError
            cols = nil
          end
        end
        cols = cols.freeze if cols
        store[self.object_id] = cols if cols
        cols
      end
    end

    def table_name
      if Ractor.main?
        reset_table_name unless defined?(@table_name)
        @table_name
      else
        store = (RactorRailsShim.storage[:rrs_table_names] ||= {})
        store.fetch(object_id) { store[object_id] = compute_table_name }
      end
    end

    def table_name=(value)
      value = value && value.to_s
      if Ractor.main?
        if defined?(@table_name)
          return if value == @table_name
          reset_column_information if connected?
        end
        @table_name        = value
        @arel_table        = nil
        @sequence_name     = nil unless @explicit_sequence_name
        @predicate_builder = nil
      else
        (RactorRailsShim.storage[:rrs_table_names] ||= {})[object_id] = value
      end
    end

    def reset_table_name
      if Ractor.main?
        super
      else
        table_name
      end
    end

    # `full_table_name_prefix` / `full_table_name_suffix` are reached from
    # `compute_table_name` whenever a model's `table_name` is NOT already
    # cached in the worker (e.g. ActiveStorage::Attachment / Blob, whose
    # explicit `table_name=` is set in main and so is invisible to a
    # worker's per-Ractor IES). Upstream they use
    # `module_parents.detect { |p| p.respond_to?(:table_name_prefix) }` —
    # the literal block is compiled in the main Ractor and is un-shareable,
    # so invoking it from a worker raises "defined with an un-shareable Proc
    # in a different Ractor". Reimplement with a plain `while` loop (no
    # block) that yields identical results.
    def full_table_name_prefix
      parents = module_parents
      i = 0
      while i < parents.length
        p = parents[i]
        return p.table_name_prefix if p.respond_to?(:table_name_prefix)
        i += 1
      end
      table_name_prefix || ""
    end

    def full_table_name_suffix
      parents = module_parents
      i = 0
      while i < parents.length
        p = parents[i]
        return p.table_name_suffix if p.respond_to?(:table_name_suffix)
        i += 1
      end
      table_name_suffix || ""
    end
  end

  # `attribute_names` is defined on `ActiveRecord::AttributeMethods::
  # ClassMethods` (NOT ModelSchema), and that module sits earlier in the
  # ancestor chain, so a ModelSchema-side patch would be shadowed. Its main
  # path memoizes via `attribute_types` (an unshareable class ivar) and
  # `table_exists?` (writes `@table_exists`), both of which raise from a
  # worker Ractor. Patch it directly: main keeps the original via `super`;
  # workers use `columns_hash.keys` (already routed through per-Ractor
  # IES), a faithful substitute for the DB-backed attribute names.
  if defined?(::ActiveRecord::AttributeMethods::ClassMethods)
    am_mod = ::ActiveRecord::AttributeMethods::ClassMethods
    unless am_mod.method_defined?(:_rrs_orig_attribute_names)
      am_mod.alias_method(:_rrs_orig_attribute_names, :attribute_names)
    end
    am_mod.module_eval do
      def attribute_names
        if ::Ractor.main?
          _rrs_orig_attribute_names
        else
          key = :"rrs_attribute_names_#{object_id}"
          RactorRailsShim.storage[key] ||= if abstract_class?
            []
          else
            columns_hash.keys
          end.freeze
        end
      end
    end
  end
end

._install_active_record_store_patchObject



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ractor_rails_shim/patches/active_record_store.rb', line 150

def self._install_active_record_store_patch
  return if @active_record_store_patched
  @active_record_store_patched = true
  # `ActiveRecord::Store` may not be loaded yet when this runs in a worker
  # Ractor (separate process) where ActiveRecord::Base is not defined at
  # install time. Force-load it so we can prepend BEFORE any model
  # (e.g. ActiveStorage::Blob) calls `store`, which generates the accessors
  # at class-definition time. Requiring it here is idempotent and harmless
  # for apps that don't use ActiveRecord.
  begin
    require "active_record/store"
  rescue LoadError
    return
  end
  return unless defined?(::ActiveRecord::Store::ClassMethods)
  ::ActiveRecord::Store::ClassMethods.prepend(ActiveRecordStorePatch)
  ::ActiveRecord::Store.prepend(ActiveRecordStoreInstancePatch)
  _register_patch :active_record_store, "8.1"
end

._install_active_storage_patchObject



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 247

def _install_active_storage_patch
  # Freeze the SecureRandom alphabets on every dispatch: they are defined
  # lazily during boot, so the first dispatch (early install) may run before
  # they exist while a later dispatch (prepare_for_ractors!, after boot)
  # finds them defined. `_install_secure_random_alphabets!` is itself
  # idempotent and only acts when needed.
  _install_secure_random_alphabets!
  return if @active_storage_patched
  @active_storage_patched = true
  @_as_patched_macros = false
  @_as_patched_blob = false

  _maybe_apply_active_storage_patch

  unless @_as_patched_macros && @_as_patched_blob
    # ActiveStorage's `:active_storage` load hook does not reliably fire in
    # every boot (e.g. a bare `config/application` + initialize! boot), and
    # the macro module must be patched BEFORE any app model calls
    # `has_one_attached`/`has_many_attached` (during eager-load). Watch for
    # the relevant modules to be opened with a TracePoint(:class); once each
    # constant is defined, prepend. ActiveStorage::Blob loads *after*
    # ActiveStorage::Attached::Model::ClassMethods, so it is patched in a
    # later trace event.
    @_as_tp = TracePoint.new(:class) do |tp|
      _maybe_apply_active_storage_patch
      if @_as_patched_macros && @_as_patched_blob
        @_as_tp.disable
        @_as_tp = nil
      end
    end
    @_as_tp.enable
  end
end

._install_active_support_error_reporter_patchObject

Patch ActiveSupport module's @error_reporter class ivar (defined via singleton_class.attr_accessor :error_reporter in active_support.rb:109) to not read from a worker Ractor. ExecutionWrapper.error_reporter delegates to ActiveSupport.error_reporter, which reads the @error_reporter ivar on the ActiveSupport module. Workers get a fresh ErrorReporter (no subscribers — correct for a read-only shared app where error reporting already ran in main via the Rails.error mechanism). Called per-request via ActionDispatch::Executor middleware.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 212

def _install_active_support_error_reporter_patch
  return if @error_reporter_patched
  @error_reporter_patched = true
  _register_patch :error_reporter, "8.1"
  return unless defined?(::ActiveSupport)
  er_key = :ractor_rails_shim_active_support_error_reporter
  er_key_str = er_key.inspect
  ::ActiveSupport.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def error_reporter
      v = RactorRailsShim.storage[#{er_key_str}]
      return v if RactorRailsShim.storage.key?(#{er_key_str})
      if Ractor.main? && instance_variable_defined?(:@error_reporter)
        @error_reporter
      else
        built = ActiveSupport::ErrorReporter.new
        RactorRailsShim.storage[#{er_key_str}] = built
        built
      end
    end
    def error_reporter=(val)
      RactorRailsShim.storage[#{er_key_str}] = val
      @error_reporter = val if Ractor.main?
      val
    end
  RUBY
end

._install_activerecord_autosave_patchObject

Patch ActiveRecord::AutosaveAssociation to redefine the autosave_associated_records_for_<assoc> and validate_associated_records_for_<assoc> methods via string eval (compiled def, no captured binding) instead of Rails' define_method(&block).

The original block captures a binding from the main Ractor (where add_autosave_association_callbacks runs during eager load), so calling the method from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor". The reflection object itself IS shareable (frozen as part of the shared app graph), so we store it in a frozen shareable registry and emit a string-eval'd def that looks it up at call time. The cyclic-guard logic from define_non_cyclic_method is inlined into the string body.

MUST install BEFORE models are eager-loaded, because add_autosave_association_callbacks fires during belongs_to/has_many/ has_one evaluation at boot. We alias the original method, call it (which registers the callback AND creates the unshareable method), then immediately overwrite the method with a string-eval'd version.



2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2505

def _install_activerecord_autosave_patch
  return if @ar_autosave_patched
  @ar_autosave_patched = true
  _register_patch :activerecord_autosave, "8.1"

  # Frozen shareable registry: { [model_name, method_name] => reflection }.
  # Rebuilt atomically (like SCOPE_SOURCE_CODES) as each association is
  # declared during boot.
  unless RactorRailsShim.const_defined?(:SHAREABLE_AUTOSAVE_REFLECTIONS, false)
    RactorRailsShim.const_set(:SHAREABLE_AUTOSAVE_REFLECTIONS, Ractor.make_shareable({}))
  end

  _apply_activerecord_autosave_patch
end

._install_activerecord_configurations_patchObject

Patch ActiveRecord::Core.configurations / configurations= to route the raw @@configurations class variable (which a non-main Ractor cannot read or write) through IsolatedExecutionState, with a shareable (deep-frozen) fallback for worker Ractors. Connection establishment in a worker (ConnectionHandler#establish_connection -> resolve_pool_config -> ActiveRecord::Base.configurations) otherwise dies on the class variable. Captured in the main Ractor at prepare/make-shareable time.



1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1323

def _install_activerecord_configurations_patch
  return if @ar_configurations_patched
  @ar_configurations_patched = true
  _register_patch :activerecord_configurations, "8.1"
  return unless defined?(::ActiveRecord::Core)

  if Ractor.main?
    begin
      cfg = ::ActiveRecord::Base.configurations
      cfg = Ractor.make_shareable(cfg) if cfg
      if cfg
        _reassign_shareable_const(:AR_CONFIGURATIONS_SHAREABLE, cfg)
      end
    rescue StandardError => e
      # best-effort
    end
  end

  key = :ractor_rails_shim_ar_configurations
  key_str = key.inspect
  # ActiveRecord::Base gets `configurations` via ActiveSupport::Concern's
  # `class_methods` (it copies the method onto Base's singleton class), so
  # patching Core.singleton_class alone is not enough — redefine on Base's
  # singleton class directly. `@@configurations` resolves through the
  # include chain (Core's class var) in the main ractor; the worker branch
  # never touches the class var.
  ::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def configurations
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      if Ractor.main?
        ActiveRecord::Core.class_variable_get(:@@configurations)
      else
        RactorRailsShim::AR_CONFIGURATIONS_SHAREABLE
      end
    end
    def configurations=(config)
      RactorRailsShim.storage[#{key_str}] = config
      ActiveRecord::Core.class_variable_set(:@@configurations, config) if Ractor.main?
    end
  RUBY
end

._install_activerecord_connection_handler_patchObject

Patch ActiveRecord::Base to route default_connection_handler through IES, and ensure connection_handler returns the per-Ractor handler. In the main ractor, falls back to the original default_connection_handler (set at core.rb:248). In workers, falls back to nil (correct — workers must call init_worker_ar_connections! to establish their own handler). Also patches retrieve_connection / connected? / connection_pool to tolerate nil handler (raise a clear error message instead of NoMethodError on nil).



1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1773

def _install_activerecord_connection_handler_patch
  return if @ar_conn_handler_patched
  @ar_conn_handler_patched = true
  _register_patch :activerecord_connection_handler, "8.1"
  return unless defined?(::ActiveRecord::ConnectionHandling)

  # Capture configs at install time if AR is already loaded (main ractor).
  _capture_ar_configurations! if Ractor.main?

  # Capture the main ractor's default_connection_handler value BEFORE we
  # override the method below (the override shadows the original
  # class_attribute reader). The class_attribute reader (already patched
  # by the shim) routes through IES, so read the value now. Store it in
  # CLASS_ATTR_VALUES so the patched reader can find it, and seed IES so
  # connection_handler finds it immediately.
  dch_key = :ractor_rails_shim_ar_default_connection_handler
  dch_key_str = dch_key.inspect
  if Ractor.main?
    begin
      orig_handler = ::ActiveRecord::Base.default_connection_handler
      if orig_handler
        RactorRailsShim::Registry.class_attr_values[:__ractor_rails_shim_ar_default_connection_handler__] = orig_handler
        RactorRailsShim.storage[dch_key] = orig_handler
      end
    rescue StandardError => e
      # Best-effort
    end
  end

  # Patch default_connection_handler to route through IES.
  # The class_attribute reader for default_connection_handler is already
  # patched by the shim (it's in the known-unshareable skip list → nil
  # in workers). We override the class method to return the per-Ractor
  # handler if set, then fall back to the original (main only) or nil.
  ::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def default_connection_handler
      v = RactorRailsShim.storage[#{dch_key_str}]
      return v if RactorRailsShim.storage.key?(#{dch_key_str})
      if Ractor.main?
        cv = RactorRailsShim::CLASS_ATTR_VALUES[:__ractor_rails_shim_ar_default_connection_handler__]
        return cv if cv
      end
      nil
    end
    def default_connection_handler=(val)
      RactorRailsShim.storage[#{dch_key_str}] = val
    end
    # Route the per-Ractor handler through Ractor-local storage. IES is
    # per-thread, so a handler stored on the init thread is invisible to the
    # worker's other threads; Ractor.current is per-Ractor and shared by all
    # threads of the worker. Falls back to IES (legacy) then
    # default_connection_handler (main Ractor only).
    # NOTE: uses nil-sentinel (`unless v.nil?`) rather than key?-based
    # detection because Ractor#[] has no `key?` method (Ractor local
    # storage returns nil for both unset and set-to-nil). Safe here
    # because `connection_handler=` is only ever called with a real
    # ConnectionHandler instance, never nil — so nil unambiguously
    # means "unset, fall through to default_connection_handler".
    def connection_handler
      v = Ractor.current[:active_record_connection_handler]
      return v unless v.nil?
      RactorRailsShim.storage[:active_record_connection_handler] || default_connection_handler
    end
    def connection_handler=(handler)
      Ractor.current[:active_record_connection_handler] = handler
    end
  RUBY

  # Also ensure connection_handler (which Rails already routes through IES
  # at core.rb:132-138) works. Rails' implementation:
  #   def self.connection_handler
  #     RactorRailsShim.storage[:active_record_connection_handler] || default_connection_handler
  #   end
  # This is already correct — if the worker sets the IES key via
  # init_worker_ar_connections!, connection_handler returns it. If not,
  # it falls back to default_connection_handler (nil in workers).
  #
  # We just need to make sure connection_pool / retrieve_connection
  # give a clear error message when the handler is nil (instead of
  # NoMethodError: undefined method `retrieve_connection_pool' for nil).
  ::ActiveRecord::ConnectionHandling.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def connection_pool
      handler = connection_handler
      unless handler
        raise ActiveRecord::ConnectionNotEstablished,
          "No connection handler for Ractor \#{Ractor.current.object_id}. " \
          "Call RactorRailsShim.init_worker_ar_connections! in each worker " \
          "Ractor before serving requests."
      end
      handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_shard, strict: true)
    end
    def retrieve_connection
      handler = connection_handler
      unless handler
        raise ActiveRecord::ConnectionNotEstablished,
          "No connection handler for Ractor \#{Ractor.current.object_id}. " \
          "Call RactorRailsShim.init_worker_ar_connections! in each worker " \
          "Ractor before serving requests."
      end
      handler.retrieve_connection(connection_specification_name, role: current_role, shard: current_shard)
    end
    def connected?
      handler = connection_handler
      return false unless handler
      handler.connected?(connection_specification_name, role: current_role, shard: current_shard)
    end
  RUBY
end

._install_activerecord_db_config_handlers_patchObject

Patch ActiveRecord::DatabaseConfigurations.db_config_handlers (a singleton_class.attr_accessor, i.e. a class instance variable on the DatabaseConfigurations class) to route through IES with a shareable fallback. The value is an Array of adapter-registered handler Procs (register_db_config_handler { |env,name,url,config| ... }). Class instance variables are per-Ractor, so a worker's slot is empty even if main set one — and the worker cannot read main's. The handler Procs themselves CAN be made shareable (verified: the sqlite3 handler captures only the shareable HashConfig constant), so we deep-freeze the Array + each Proc in main and expose it as a shareable constant the worker reads. ConnectionHandler#establish_connection -> resolve_pool_config -> DatabaseConfigurations#resolve -> build_db_config_from_hash calls these Procs, so they must be shareable AND callable cross-Ractor.



1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1379

def _install_activerecord_db_config_handlers_patch
  return if @ar_dbch_patched
  @ar_dbch_patched = true
  _register_patch :activerecord_db_config_handlers, "8.1"
  return unless defined?(::ActiveRecord::DatabaseConfigurations)

  if Ractor.main?
    begin
      handlers = ::ActiveRecord::DatabaseConfigurations.db_config_handlers
      # Make each handler Proc shareable (freezes its binding). A shareable
      # Proc is callable from any Ractor.
      handlers.each { |h| _swallow("make ar db config handler shareable") { Ractor.make_shareable(h) } }
      shareable = Ractor.make_shareable(handlers.dup)
      _reassign_shareable_const(:AR_DB_CONFIG_HANDLERS_SHAREABLE, shareable)
    rescue StandardError => e
      # best-effort
    end
  end

  key = :ractor_rails_shim_ar_db_config_handlers
  key_str = key.inspect
  ::ActiveRecord::DatabaseConfigurations.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def db_config_handlers
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      if Ractor.main?
        @db_config_handlers
      else
        RactorRailsShim::AR_DB_CONFIG_HANDLERS_SHAREABLE
      end
    end
    def db_config_handlers=(val)
      RactorRailsShim.storage[#{key_str}] = val
      @db_config_handlers = val if Ractor.main?
    end
  RUBY
end

._install_activerecord_deduplicable_patchObject

Patch Deduplicable::ClassMethods#registry to route the lazy class instance variable @registry through IES. registry returns @registry ||= {} — a mutable Hash used to deduplicate column metadata objects. It's called during schema introspection (Post.all -> columns -> new_column_from_field -> fetch_type_metadata -> Deduplicable.new -> deduplicate -> registry). The class instance variable write fails from a non-main Ractor. Fix: route through IES so each Ractor builds its own registry Hash.



1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1522

def _install_activerecord_deduplicable_patch
  return if @ar_deduplicable_patched
  @ar_deduplicable_patched = true
  _register_patch :activerecord_deduplicable, "8.1"
  return unless defined?(::ActiveRecord::ConnectionAdapters::Deduplicable)

  ::ActiveRecord::ConnectionAdapters::Deduplicable::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def registry
      key = :"ractor_rails_shim_dedup_registry_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      h = {}
      RactorRailsShim.storage[key] = h
      h
    end
  RUBY
end

._install_activerecord_define_attribute_methods_patchObject

Patch ActiveRecord::AttributeMethods::ClassMethods#define_attribute_methods to no-op in worker Ractors. The method writes @attribute_methods_generated (a class ivar) and calls load_schema + super(attribute_names) which generate methods on the class. In a worker, all of these either write class ivars (IsolationError) or try to define methods on a frozen class (FrozenError). The attribute methods were already generated in main by generate_ar_attribute_methods!, so workers just need to skip this.



798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 798

def _install_activerecord_define_attribute_methods_patch
  return if @ar_define_attr_methods_patched
  @ar_define_attr_methods_patched = true
  _register_patch :activerecord_define_attribute_methods, "8.1"
  return unless defined?(::ActiveRecord::AttributeMethods::ClassMethods)

  ::ActiveRecord::AttributeMethods::ClassMethods.module_eval do
    alias_method :_rrs_orig_define_attribute_methods, :define_attribute_methods
    def define_attribute_methods
      if Ractor.main?
        _rrs_orig_define_attribute_methods
      else
        # Worker: attribute methods were generated in main. Return true
        # (the original returns true on success) without writing class ivars.
        true
      end
    end
  end
end

._install_activerecord_delegation_patchObject

Patch ActiveRecord::Delegation.uncacheable_methods to route the lazy class instance variable @uncacheable_methods through IES.

uncacheable_methods is a class method on the Delegation module: `@uncacheable_methods ||= (delegated_classes.flat_map(&:public_instance_methods)

  • Relation.public_instance_methods).to_set.freeze. It's read during method_missingon relation delegate classes (e.g. when Kaminari callsPost.page(1).per(10)perisn't a standard Relation method, soClassSpecificRelation#method_missingchecksuncacheable_methods` to decide whether to delegate). The class instance variable write fails from a non-main Ractor (Ractor::IsolationError).

Fix: route through IES so each Ractor computes + caches its own Set. The computation is deterministic (same delegated_classes everywhere). String-eval'd (no captured binding), callable from any Ractor.



1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1931

def _install_activerecord_delegation_patch
  return if @ar_delegation_patched
  @ar_delegation_patched = true
  _register_patch :activerecord_delegation, "8.1"
  return unless defined?(::ActiveRecord::Delegation)

  ::ActiveRecord::Delegation.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def uncacheable_methods
      key = :ractor_rails_shim_ar_uncacheable_methods
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      result = (
        delegated_classes.flat_map(&:public_instance_methods) - ActiveRecord::Relation.public_instance_methods
      ).to_set.freeze
      RactorRailsShim.storage[key] = result
      result
    end
  RUBY
end

._install_activerecord_find_by_cache_patchObject

ActiveRecord::Base#cached_find_by_statement reads @find_by_statement_cache[connection.prepared_statements] (a Hash whose values are Concurrent::Maps) and calls cache.compute_if_absent(key). Concurrent::Map is unshareable, so make_app_shareable! replaces the maps with frozen Hashes whose values end up nil — and Hash has no compute_if_absent anyway. In a worker Ractor this raises NoMethodError: undefined method 'compute_if_absent' for nil, breaking find / find_by / take (but not where, which doesn't use the cache). Fix: in non-main Ractors, build the per-find-statement cache in IsolatedExecutionState (per-Ractor, mutable) keyed by the model class and connection.prepared_statements. Main keeps the original Concurrent::Map-backed behavior via super.



2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2029

def _install_activerecord_find_by_cache_patch
  return if @activerecord_find_by_cache_patched
  @activerecord_find_by_cache_patched = true
  _register_patch :activerecord_find_by_cache, "8.1"
  return unless defined?(::ActiveRecord::Base)

  ::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def cached_find_by_statement(connection, key, &block)
      return super if Ractor.main?
      cache = (RactorRailsShim.storage[:"ractor_rails_shim_find_by_cache_\#{object_id}"] ||= {})
      prepared = connection.prepared_statements
      sub = (cache[prepared] ||= {})
      if sub.key?(key)
        sub[key]
      else
        sub[key] = ::ActiveRecord::StatementCache.create(connection, &block)
      end
    end
  RUBY
end

._install_activerecord_migration_patchObject

Patch ActiveRecord::Migrator.migrations_paths (a singleton attr_accessor reading the @migrations_paths class ivar) and ActiveRecord::Migration::CheckPending (the dev pending-migration middleware) to be Ractor-safe. In dev, CheckPending runs on every request and reads Migrator.migrations_paths + mutates its own @watcher / @needs_check ivars on the (frozen, shared) middleware instance. Route the class ivar and the instance ivars through IsolatedExecutionState so each worker reads the main Ractor's migrations paths and builds its own watcher. This keeps the dev pending-migration guard working under kino :ractor instead of stripping the middleware.



2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2183

def _install_activerecord_migration_patch
  return if @activerecord_migration_patched
  @activerecord_migration_patched = true
  _register_patch :activerecord_migration, "8.1"
  return unless defined?(::ActiveRecord::Migrator)

  mig = ::ActiveRecord::Migrator
  mp_key = :ractor_rails_shim_migrator_migrations_paths
  mp_key_str = mp_key.inspect
  mig.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def migrations_paths
      v = RactorRailsShim.storage[#{mp_key_str}]
      return v if RactorRailsShim.storage.key?(#{mp_key_str})
      if Ractor.main? && instance_variable_defined?(:@migrations_paths)
        v = @migrations_paths
        RactorRailsShim.storage[#{mp_key_str}] = v
        v
      else
        ["db/migrate"].freeze
      end
    end
    def migrations_paths=(val)
      RactorRailsShim.storage[#{mp_key_str}] = val
      @migrations_paths = val if Ractor.main?
      val
    end
  RUBY

  return unless defined?(::ActiveRecord::Migration::CheckPending)
  cp = ::ActiveRecord::Migration::CheckPending
  w_key = :ractor_rails_shim_check_pending_watcher
  nc_key = :ractor_rails_shim_check_pending_needs_check
  w_key_str = w_key.inspect
  nc_key_str = nc_key.inspect
  cp.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def call(env)
      @mutex.synchronize do
        watcher = RactorRailsShim.storage[#{w_key_str}]
        if watcher.nil?
          watcher = RactorRailsShim.storage[#{w_key_str}] = build_watcher do
            RactorRailsShim.storage[#{nc_key_str}] = true
            ::ActiveRecord::Migration.check_pending_migrations
            RactorRailsShim.storage[#{nc_key_str}] = false
          end
        end
        needs_check = RactorRailsShim.storage[#{nc_key_str}]
        needs_check = true if needs_check.nil?
        if needs_check
          watcher.execute
        else
          watcher.execute_if_updated
        end
      end
      @app.call(env)
    end
  RUBY
end

._install_activerecord_model_classes_patchObject

Register + run the model-class shareability patch (Blocker: AR model class lazy class-ivar initialization from workers).



782
783
784
785
786
787
788
789
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 782

def _install_activerecord_model_classes_patch
  return if @ar_model_classes_patched
  @ar_model_classes_patched = true
  _register_patch :activerecord_model_classes, "8.1"
  return unless defined?(::ActiveRecord::Base)
   if Ractor.main?
   if Ractor.main?
end

._install_activerecord_model_schema_patchObject

Patch ActiveRecord::ModelSchema::ClassMethods lazy class-ivar caches (symbol_column_to_string, content_columns, column_defaults) to route through IsolatedExecutionState. Each Ractor builds its own cache (deterministic from columns/columns_hash, which are warmed in main and read-only in workers). Without this, the first worker call that misses the cache tries to WRITE the class ivar (@symbol_column_to_string_name_hash ||= ...) and dies with Ractor::IsolationError: can not set instance variables of classes/modules by non-main Ractors. Seen via Devise's clean_up_passwords -> respond_to? -> symbol_column_to_string.



1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1112

def _install_activerecord_model_schema_patch
  return if @ar_model_schema_symbol_patched
  @ar_model_schema_symbol_patched = true
  _register_patch :activerecord_model_schema, "8.1"
  return unless defined?(::ActiveRecord::ModelSchema)

  ::ActiveRecord::ModelSchema::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def symbol_column_to_string(name_symbol)
      key = :"ractor_rails_shim_symbol_column_to_string_\#{self.name}"
      v = RactorRailsShim.storage[key]
      return v[name_symbol] if v
      hash = column_names.index_by(&:to_sym)
      RactorRailsShim.storage[key] = hash
      hash[name_symbol]
    end

    def content_columns
      key = :"ractor_rails_shim_content_columns_\#{self.name}"
      v = RactorRailsShim.storage[key]
      return v if v
      cols = columns.reject do |c|
        c.name == primary_key ||
        c.name == inheritance_column ||
        c.name.end_with?("_id", "_count")
      end.freeze
      RactorRailsShim.storage[key] = cols
      cols
    end

    # In a worker Ractor, load_schema and load_schema! must not write
    # class ivars (@columns_hash, @schema_loaded, etc.). The schema was
    # loaded in the main Ractor and the values are either frozen in the
    # shared graph or routed through IES. Patch load_schema to no-op in
    # workers (the schema is already loaded), and load_schema! to be a
    # no-op (never called from a worker because load_schema short-circuits).
    alias_method :_rrs_orig_load_schema, :load_schema
    def load_schema
      return unless Ractor.main?
      _rrs_orig_load_schema
    end

    alias_method :_rrs_orig_load_schema_bang, :load_schema!
    def load_schema!
      return unless Ractor.main?
      _rrs_orig_load_schema_bang
    end

    # NOTE: `column_defaults` is intentionally NOT redefined here. It is
    # installed by the prepended `ActiveRecordModelSchemaPatch` (see
    # `active_record_model_schema.rb`), which is prepended onto
    # `ModelSchema::ClassMethods` by `_install_active_model_attribute_patch`
    # and therefore sits in front of this module_eval heredoc in the
    # method lookup chain. A second definition here would be dead code and
    # a maintenance trap.

    # `reload_schema_from_cache` (called by `load_schema`/`reload_schema!`)
    # writes a batch of class ivars to `nil` to invalidate schema caches:
    #   @_returning_columns_for_insert, @arel_table, @column_names,
    #   @symbol_column_to_string_name_hash, @content_columns, @column_defaults
    # (ModelSchema) plus @timestamp_attributes_for_create_in_model,
    # @timestamp_attributes_for_update_in_model,
    # @all_timestamp_attributes_in_model (Timestamp#reload_schema_from_cache
    # via super). In a worker Ractor those writes raise
    # Ractor::IsolationError ("can not set instance variables of
    # classes/modules by non-main Ractors"). The shim already routes the
    # *readers* for these caches through IES, so in a worker we only need
    # to clear the IES slots — the next read rebuilds lazily. In main we
    # keep the original class-ivar-clearing behavior.
    alias_method :_rrs_orig_reload_schema_from_cache, :reload_schema_from_cache
    def reload_schema_from_cache(recursive = true)
      if Ractor.main?
        # During warming (prepare_for_ractors!/make_app_shareable!),
        # reload_schema_from_cache on abstract parents recursively resets
        # @schema_loaded = false and @columns_hash = nil on all descendants,
        # destroying schema data that workers need. Use a module-level flag
        # to suppress the recursive reset during the warming phase. The
        # flag is set by _share_model_classes! and generate_ar_attribute_methods!.
        unless RactorRailsShim.instance_variable_get(:@_rrs_schema_warming)
          _rrs_orig_reload_schema_from_cache(recursive)
        end
      else
        # Clear this Ractor's IES slots for the IES-routed schema caches.
        # Use `next` over an explicit list (not `IES.clear`) to avoid
        # wiping unrelated slots. Keys mirror the readers above + the
        # prepended ActiveRecordModelSchemaPatch (active_record_model_schema.rb)
        # + ActiveModelAttributeRegistrationPatch (active_model_attribute.rb).
        name_str = self.name
        [
          :"ractor_rails_shim_symbol_column_to_string_\#{name_str}",
          :"ractor_rails_shim_content_columns_\#{name_str}",
          :"rrs_column_defaults_\#{object_id}",
          :"rrs_attributes_builder_\#{object_id}",
          :"rrs_yaml_encoder_\#{object_id}",
          :"rrs_returning_cols_\#{object_id}",
          :"rrs_default_attributes_\#{object_id}",
          :"rrs_attribute_types_\#{object_id}",
          :"rrs_table_names",
          :"rrs_arel_tables",
          :"rrs_predicate_builders",
          :"rrs_type_casters",
        ].each do |k| RactorRailsShim.storage.delete(k) end
        # The Timestamp subclass override calls `super` (this method); it
        # has already run by the time we get here via the super chain, so
        # its ivar-clears were intercepted by the worker branch of THIS
        # override (no-op). No further action needed.
      end
    end
  RUBY

  # Patch ActiveRecord::Timestamp::ClassMethods#reload_schema_from_cache
  # the same way: in main, keep the original ivar-clearing; in a worker,
  # skip the class-ivar writes (the IES slots are cleared by the
  # ModelSchema#reload_schema_from_cache override above via super).
  if defined?(::ActiveRecord::Timestamp::ClassMethods)
    ::ActiveRecord::Timestamp::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def reload_schema_from_cache(recursive = true)
        if Ractor.main?
          @timestamp_attributes_for_create_in_model = nil
          @timestamp_attributes_for_update_in_model = nil
          @all_timestamp_attributes_in_model = nil
          super
        else
          # Worker: skip the class-ivar writes (would raise
          # IsolationError). The ModelSchema super clears the IES slots.
          super
        end
      end

      # The three timestamp-attribute readers memoize their result on the
      # (shared, frozen) model class via @timestamp_attributes_for_* class
      # ivars. From a worker Ractor that write raises Ractor::IsolationError
      # (this is exactly the failure hit on Comment#save ->
      # _create_record -> all_timestamp_attributes_in_model). Route the
      # memoization through a per-worker cache in workers; keep the original
      # class-ivar behavior in main (where the value is frozen into the
      # shared graph and the ivar can still be written before freeze).
      def timestamp_attributes_for_create_in_model
        if Ractor.main?
          @timestamp_attributes_for_create_in_model ||=
            (timestamp_attributes_for_create & column_names).freeze
        else
          (Ractor.current[:__rrs_ts_cache__] ||= {})[[object_id, :ts_create]] ||=
            (timestamp_attributes_for_create & column_names).freeze
        end
      end

      def timestamp_attributes_for_update_in_model
        if Ractor.main?
          @timestamp_attributes_for_update_in_model ||=
            (timestamp_attributes_for_update & column_names).freeze
        else
          (Ractor.current[:__rrs_ts_cache__] ||= {})[[object_id, :ts_update]] ||=
            (timestamp_attributes_for_update & column_names).freeze
        end
      end

      def all_timestamp_attributes_in_model
        if Ractor.main?
          @all_timestamp_attributes_in_model ||=
            (timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model).freeze
        else
          (Ractor.current[:__rrs_ts_cache__] ||= {})[[object_id, :ts_all]] ||=
            (timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model).freeze
        end
      end
    RUBY
  end
end

._install_activerecord_module_attrs_patchObject

Patch ActiveRecord module-level singleton_class.attr_accessor attributes (schema_cache_ignored_tables, database_cli, etc.) to route through IES with shareable fallbacks. These are class instance variables on the ActiveRecord module that workers can't read/write. Each is an Array or Hash of simple literals, so they can be deep-frozen and shared.



1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1471

def _install_activerecord_module_attrs_patch
  return if @ar_module_attrs_patched
  @ar_module_attrs_patched = true
  _register_patch :activerecord_module_attrs, "8.1"
  return unless defined?(::ActiveRecord)

  # [method_name, const_name] pairs. The const holds the shareable snapshot.
  attrs = [
    [:schema_cache_ignored_tables, :AR_SCHEMA_CACHE_IGNORED_TABLES_SHAREABLE],
    [:database_cli, :AR_DATABASE_CLI_SHAREABLE],
  ]

  attrs.each do |method_name, const_name|
    if Ractor.main?
      begin
        val = ::ActiveRecord.public_send(method_name)
        shareable = Ractor.make_shareable(val.is_a?(::Array) ? val.dup : val)
        _reassign_shareable_const(const_name, shareable)
      rescue StandardError => e
        # best-effort
      end
    end

    key = :"ractor_rails_shim_ar_#{method_name}"
    key_str = key.inspect
    const_str = const_name.to_s
    ::ActiveRecord.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{method_name}
        v = RactorRailsShim.storage[#{key_str}]
        return v if RactorRailsShim.storage.key?(#{key_str})
        if Ractor.main?
          @#{method_name}
        else
          RactorRailsShim::#{const_str}
        end
      end
      def #{method_name}=(val)
        RactorRailsShim.storage[#{key_str}] = val
        @#{method_name} = val if Ractor.main?
      end
    RUBY
  end
end

._install_activerecord_pool_config_patchObject

Patch ActiveRecord::ConnectionAdapters::PoolConfig#initialize to skip writing to the INSTANCES ObjectSpace::WeakMap registry in non-main Ractors. This is the first wall a worker hits when establishing a connection (ConnectionHandler#establish_connection -> resolve_pool_config -> PoolConfig.new -> INSTANCES[self] = self).

INSTANCES is a private_constant ObjectSpace::WeakMap. A WeakMap is intrinsically unshareable (it can't be frozen / made shareable), and a non-main Ractor cannot access the constant at all (Ractor::IsolationError: "can not access non-shareable objects in constant ... by non-main ractor").

The registry is ONLY used by the class methods discard_pools! and disconnect_all! (which iterate all pool configs to disconnect/reload). Those are called during reloading (dev) and explicit disconnect — never in a read-only production worker serving requests. So skipping the registry write in workers is safe: workers manage their own per-Ractor handler + pools, and the main ractor's registry stays intact for reload.

We redefine initialize via string eval (no captured binding) so it's callable from any Ractor. The body replicates the original exactly except the final INSTANCES[self] = self is guarded by Ractor.main?. The private INSTANCES constant is accessible via constant lookup because the method is defined on PoolConfig itself.



1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1597

def _install_activerecord_pool_config_patch
  return if @ar_pool_config_patched
  @ar_pool_config_patched = true
  _register_patch :activerecord_pool_config, "8.1"
  return unless defined?(::ActiveRecord::ConnectionAdapters::PoolConfig)

  ::ActiveRecord::ConnectionAdapters::PoolConfig.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def initialize(connection_class, db_config, role, shard)
      super()
      @server_version = nil
      self.connection_descriptor = connection_class
      @db_config = db_config
      @role = role
      @shard = shard
      @pool = nil
      INSTANCES[self] = self if Ractor.main?
    end
  RUBY
end

._install_activerecord_primary_key_patchObject

Patch ActiveRecord::AttributeMethods::PrimaryKey#primary_key and #composite_primary_key? to not read the PRIMARY_KEY_NOT_SET constant.

The original code: reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key) reads the constant on every call. PRIMARY_KEY_NOT_SET is a BasicObject (can't be frozen, can't be made shareable), so reading the constant from a worker Ractor raises Ractor::IsolationError — even if @primary_key is already set to the real value.

Fix: replace the sentinel check with a shareable-snapshot lookup. At _share_model_classes! time, each model's primary_key is warmed in main and stored in AR_PRIMARY_KEYS_SHAREABLE (a frozen Hash). The patched primary_key method checks IES first (per-Ractor), then the shareable snapshot, then falls back to the original logic in the main ractor. Workers never read the constant. String-eval'd (no captured binding).



1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1966

def _install_activerecord_primary_key_patch
  return if @ar_primary_key_patched
  @ar_primary_key_patched = true
  _register_patch :activerecord_primary_key, "8.1"
  return unless defined?(::ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods)

  mod = ::ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods
  mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def primary_key
      key = :"ractor_rails_shim_pk_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      if Ractor.main?
        reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
        v = @primary_key
        RactorRailsShim.storage[key] = v
        v
      else
        snap = RactorRailsShim::AR_PRIMARY_KEYS_SHAREABLE[name]
        return snap if snap
        # Fallback: derive the primary key from the schema cache via the
        # worker-safe table_name (compute_table_name). Reading the
        # @primary_key / @table_name class ivars raises
        # Ractor::IsolationError in workers, and the boot-time descendant
        # snapshot can miss lazy-loaded models, so compute it on demand.
        # `id` / `to_param` depend on primary_key, so a nil here breaks
        # URL helpers and record inspection in workers.
        begin
          pk = connection_pool.schema_cache.primary_keys(table_name)
          return pk if pk.is_a?(::String)
        rescue StandardError
          nil
        end
        nil
      end
    end
    def composite_primary_key?
      key = :"ractor_rails_shim_pk_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v.is_a?(::Array) if RactorRailsShim.storage.key?(key)
      if Ractor.main?
        reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
        @primary_key.is_a?(::Array)
      else
        pk = RactorRailsShim::AR_PRIMARY_KEYS_SHAREABLE[name]
        pk.is_a?(::Array)
      end
    end
  RUBY
end

._install_activerecord_query_constraints_patchObject

Patch Persistence::ClassMethods#query_constraints_list and #has_query_constraints? to route the lazy @query_constraints_list class ivar through IES. query_constraints_list does @query_constraints_list ||= <computation> — the class ivar write fails from a non-main Ractor. Called during Post.first -> ordered_relation -> _order_columns.



1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1545

def _install_activerecord_query_constraints_patch
  return if @ar_query_constraints_patched
  @ar_query_constraints_patched = true
  _register_patch :activerecord_query_constraints, "8.1"
  return unless defined?(::ActiveRecord::Persistence::ClassMethods)

  ::ActiveRecord::Persistence::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def query_constraints_list
      key = :"ractor_rails_shim_qcl_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      result = if base_class? || primary_key != base_class.primary_key
        primary_key if primary_key.is_a?(::Array)
      else
        base_class.query_constraints_list
      end
      RactorRailsShim.storage[key] = result
      result
    end
    def has_query_constraints?
      key = :"ractor_rails_shim_qcl_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return !v.nil? if RactorRailsShim.storage.key?(key)
      result = query_constraints_list
      !result.nil?
    end
  RUBY
end

._install_activerecord_query_logs_patchObject

Patch ActiveRecord::QueryLogs#tag_content. It reads the @handlers and @formatter class ivars (populated from config.active_record.query_log_tags during boot) on every SQL statement, so a worker Ractor raises Ractor::IsolationError. cached_comment is a thread_mattr_accessor (already Ractor-safe), so only the handlers/formatter need handling. Capture a shareable snapshot in main (snapshot_query_logs!, post-boot) and have workers build the comment from it — query-log tags then work in workers exactly as in dev's main Ractor.



2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2088

def _install_activerecord_query_logs_patch
  return if @query_logs_patched
  @query_logs_patched = true
  _register_patch :query_logs, "8.1"
  return unless defined?(::ActiveRecord::QueryLogs)
  ql = ::ActiveRecord::QueryLogs
  # tag_content is defined as a SINGLETON method on ActiveRecord::QueryLogs
  # (not an instance method), so alias the singleton method (not an
  # instance one) and fall back to it for the main Ractor / when the
  # snapshot is unavailable.
  ql.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    alias_method :__ractors_original_tag_content, :tag_content
    def tag_content(connection)
      return __ractors_original_tag_content(connection) if Ractor.main?
      snap = ::RactorRailsShim::QUERY_LOGS_SNAPSHOT
      return __ractors_original_tag_content(connection) unless snap
      format = snap[:format]
      formatter = case format
        when :sqlcommenter then ::ActiveRecord::QueryLogs::SQLCommenter
        else ::ActiveRecord::QueryLogs::LegacyFormatter
      end
      return nil unless formatter
      context = ActiveSupport::ExecutionContext.to_h
      context[:connection] ||= connection
      pairs = snap[:handlers].filter_map do |(key, kind, data)|
        val = case kind
          when :get_key then context[key]
          when :identity then data
          else nil
        end
        formatter.format(key, val) unless val.nil?
      end
      formatter.join(pairs)
    end
  RUBY
end

._install_activerecord_query_transformers_patchObject

Patch ActiveRecord.query_transformers to route through IES with a shareable fallback. query_transformers is a singleton_class .attr_accessor (a class instance variable on the ActiveRecord module) holding an Array of transformer objects (e.g. ActiveRecord::QueryLogs). DatabaseStatements#preprocess_query reads it on every query: ActiveRecord.query_transformers.each { |t| t.call(sql, self) }.

Class instance variables are per-Ractor, so a worker's @query_transformers is nil (set in main at boot via self.query_transformers = [], then << QueryLogs in the railtie). The transformer objects themselves ARE shareable (they're Classes/modules), so we deep-freeze the Array in main and expose it as a shareable constant the worker reads. Same pattern as _install_activerecord_db_config_handlers_patch.



1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1430

def _install_activerecord_query_transformers_patch
  return if @ar_query_transformers_patched
  @ar_query_transformers_patched = true
  _register_patch :activerecord_query_transformers, "8.1"
  return unless defined?(::ActiveRecord)

  if Ractor.main?
    begin
      transformers = ::ActiveRecord.query_transformers
      transformers.each { |t| _swallow("make ar query transformer shareable") { Ractor.make_shareable(t) } }
      shareable = Ractor.make_shareable(transformers.dup)
      _reassign_shareable_const(:AR_QUERY_TRANSFORMERS_SHAREABLE, shareable)
    rescue StandardError => e
      # best-effort
    end
  end

  key = :ractor_rails_shim_ar_query_transformers
  key_str = key.inspect
  ::ActiveRecord.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def query_transformers
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      if Ractor.main?
        @query_transformers
      else
        RactorRailsShim::AR_QUERY_TRANSFORMERS_SHAREABLE
      end
    end
    def query_transformers=(val)
      RactorRailsShim.storage[#{key_str}] = val
      @query_transformers = val if Ractor.main?
    end
  RUBY
end

._install_activerecord_quoting_cache_patchObject

Patch the adapter quoting caches (QUOTED_COLUMN_NAMES / QUOTED_TABLE_NAMES) to use per-Ractor storage instead of the unshareable Concurrent::Map constants.

Each adapter (SQLite3, MySQL, PostgreSQL) defines these as Concurrent::Map.new constants in its Quoting module, and the quote_column_name / quote_table_name class methods lazily populate them via MAP[name] ||= <quoting_logic>.freeze. Concurrent::Map is intrinsically unshareable (no #freeze), so a worker Ractor cannot access the constant at all (Ractor::IsolationError: "can not access non-shareable objects in constant ..."). This is the fourth wall a worker hits: during Post.count -> Arel traversal -> quote_table_name -> QUOTED_TABLE_NAMES[name] ||= ....

Fix: redefine quote_column_name / quote_table_name on each adapter's Quoting::ClassMethods module to use a per-Ractor Hash cache (stored in IES, keyed by the adapter class name). Each Ractor builds its own mutable cache on first access. The quoting logic is replicated per adapter (it's simple, stable string manipulation). String-eval'd (no captured binding), callable from any Ractor.



1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1719

def _install_activerecord_quoting_cache_patch
  return if @ar_quoting_patched
  @ar_quoting_patched = true
  _register_patch :activerecord_quoting_cache, "8.1"

  # [module_path, column_logic, table_logic] per adapter.
  adapters = [
    ["ActiveRecord::ConnectionAdapters::SQLite3::Quoting",
     %q{%Q("#{name.to_s.gsub('"', '""')}").freeze},
     %q{%Q("#{name.to_s.gsub('"', '""').gsub(".", "\".\"")}").freeze}],
    ["ActiveRecord::ConnectionAdapters::MySQL::Quoting",
     %q{"`#{name.to_s.gsub('`', '``')}`".freeze},
     %q{"`#{name.to_s.gsub('`', '``').gsub(".", "`.`")}`".freeze}],
    ["ActiveRecord::ConnectionAdapters::PostgreSQL::Quoting",
     %q{::PG::Connection.quote_ident(name.to_s).freeze},
     %q{::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s).quoted.freeze}],
  ]

  adapters.each do |mod_path, column_logic, table_logic|
    mod = begin
      mod_path.split("::").inject(Object) { |ns, n| ns.const_get(n, false) }
    rescue StandardError
      nil
    end
    next unless mod
    klass_mod = mod.const_get(:ClassMethods, false) rescue next
    next unless klass_mod.method_defined?(:quote_column_name)

    klass_mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def quote_column_name(name)
        key = :"ractor_rails_shim_quoted_cols_\#{self.name}"
        cache = RactorRailsShim.storage[key]
        cache ||= (RactorRailsShim.storage[key] = {})
        cache[name] ||= (#{column_logic})
      end

      def quote_table_name(name)
        key = :"ractor_rails_shim_quoted_tables_\#{self.name}"
        cache = RactorRailsShim.storage[key]
        cache ||= (RactorRailsShim.storage[key] = {})
        cache[name] ||= (#{table_logic})
      end
    RUBY
  end
end

._install_activerecord_reaper_patchObject

Patch ConnectionPool::Reaper#run to no-op in non-main Ractors.

ConnectionPool#initialize (connection_pool.rb:307) calls @reaper.run, which calls Reaper.register_pool (a class method). register_pool reads/writes the Reaper class's instance variables (@mutex, @pools, background reaper thread. Class instance variables are off-limits to non-main Ractors (Ractor::IsolationError), so this is the second wall a worker hits during establish_connection -> ConnectionPool.new.

The reaper is a background maintenance thread that periodically reaps dead-thread connections, flushes idle connections, and keepalives stale ones. In a worker Ractor this is neither safe (can't share the reaper thread or its class-ivar registry across Ractors) nor essential: each Ractor owns its own connection pool, and when the Ractor exits its pool is garbage-collected with it. Connection health for long-lived workers can be addressed later with a per-Ractor reaper if needed; for now, no-op'ing registration unblocks connection establishment.

register_pool is only called from Reaper#run, so patching run to return early in non-main Ractors fully prevents the class-ivar access and the thread spawn. The pool itself still functions normally.



1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1639

def _install_activerecord_reaper_patch
  return if @ar_reaper_patched
  @ar_reaper_patched = true
  _register_patch :activerecord_reaper, "8.1"
  return unless defined?(::ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper)

  ::ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def run
      return unless frequency && frequency > 0
      return unless Ractor.main?
      self.class.register_pool(pool, frequency)
    end
  RUBY
end

._install_activerecord_reflection_patchObject



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/ractor_rails_shim/patches/activerecord_reflection.rb', line 291

def _install_activerecord_reflection_patch
  return if @ar_reflection_patched
  @ar_reflection_patched = true
  # Apply now if AR is already loaded, otherwise hook active_record's load
  # (before eager-load builds the app's reflections). Never force a
  # `require "active_record"` here — that would double-load AR under
  # `bundle exec` and re-define constants after the shim froze them.
  if defined?(::ActiveRecord::Reflection::AbstractReflection)
    _apply_activerecord_reflection_patch
  else
    ActiveSupport.on_load(:active_record) do
      RactorRailsShim.__send__(:_apply_activerecord_reflection_patch)
    end
  end
end

._install_activerecord_relation_delegate_cache_patchObject

Blockers 3: ActiveRecord model classes cache relation-delegate classes in the @relation_delegate_cache class instance variable (set in DelegateCache#initialize_relation_delegate_cache, activerecord/relation/delegation.rb:31-44). The cache is a plain (mutable) Hash mapping each delegated class (ActiveRecord::Relation, etc.) to an anonymous delegate Class. From a worker Ractor, reading the class ivar raises Ractor::IsolationError ("can not get unshareable values from instance variables of classes/modules from non-main Ractors (@relation_delegate_cache from Post)") — even a plain Post.page(1).

The delegate Classes themselves ARE shareable (verified: Ractor.shareable?(Post::ActiveRecord_Relation) == true). Only the enclosing Hash is mutable (unshareable). So we deep-freeze the cache (Ractor.make_shareable) in the main Ractor at prepare/make-shareable time. A class ivar whose value is shareable is readable from a worker Ractor (unlike class variables, which always raise). Freezing is safe: the cache is populated once per class at load time and never mutated afterwards (each relation-delegate Class is const_set as a private constant on the model class).



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 298

def _install_activerecord_relation_delegate_cache_patch
  return if @ar_rdc_patched
  @ar_rdc_patched = true
  _register_patch :activerecord_relation_delegate_cache, "8.1"
  return unless defined?(::ActiveRecord::Base)

  mod = ::ActiveRecord::Delegation::DelegateCache
  mod.module_eval do
    # The default implementation reads the delegate class from the
    # `@relation_delegate_cache` *class instance variable* — an unshareable
    # Hash populated lazily on the model class. From a worker Ractor that
    # value is unreadable (Ractor::IsolationError: "can not get unshareable
    # values from instance variables ... (@relation_delegate_cache from
    # Post)"). `initialize_relation_delegate_cache` ALSO const_sets each
    # delegate class onto the model (e.g. `Post::ActiveRecord_Relation`),
    # and that constant is shareable. So read the delegate class via the
    # constant instead of the class ivar — Ractor-safe with no per-worker
    # rebuild.
    def relation_delegate_class(klass)
      const_get(klass.name.gsub("::", "_"))
    end

    # Keep building + const_setting the delegate classes (shareable), but
    # stop stashing them in the unshareable `@relation_delegate_cache` ivar.
    # The ivar is left a frozen empty Hash so any (now-unused) reader of it
    # is still Ractor-safe.
    def initialize_relation_delegate_cache
      @relation_delegate_cache = {}.freeze
      ::ActiveRecord::Delegation.delegated_classes.each do |k|
        delegate = Class.new(k) { include ::ActiveRecord::Delegation::ClassSpecificRelation }
        include_relation_methods(delegate)
        mangled_name = k.name.gsub("::", "_")
        const_set mangled_name, delegate
        private_constant mangled_name
      end
    end
  end

   if Ractor.main?
end

._install_activerecord_scope_patchObject

Patch ActiveRecord::Scoping::Named::ClassMethods#scope to use string eval instead of define_method. Rails defines scope methods via singleton_class.define_method(name) { |*args| scope = all._exec_scope(*args, &body) ... }. The block captures the body Proc (a lambda like -> { order(created_at: :desc) }) from the main Ractor. Calling that block from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor".

The body lambda itself CANNOT be made shareable (Proc#self is always the enclosing scope, which is the main Ractor). Fix: use Proc#source_location to read the body's source code at prepare time (main Ractor), store it in a shareable constant, and define the scope method via string eval that eval's the stored source code. This avoids ever calling the original lambda from a worker.



2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2281

def _install_activerecord_scope_patch
  return if @ar_scope_patched
  @ar_scope_patched = true
  _register_patch :activerecord_scope, "8.1"
  # The scope macro MUST be patched BEFORE the app's models are eager
  # loaded, so `scope :recent, -> { ... }` defines a worker-safe method
  # (string-eval'd body) instead of Rails' un-shareable define_method.
  # Apply now if AR is already loaded, otherwise hook active_record's load
  # (which fires during framework boot, before eager-load). Never force a
  # `require "active_record"` here — that would double-load AR under
  # `bundle exec` and re-define constants after the shim froze them.
  if defined?(::ActiveRecord::Scoping::Named::ClassMethods)
    _apply_activerecord_scope_patch
  else
    ActiveSupport.on_load(:active_record) do
      RactorRailsShim.__send__(:_apply_activerecord_scope_patch)
    end
  end
end

._install_activerecord_serialize_cast_value_patchObject

Patch ActiveModel::Type::SerializeCastValue::ClassMethods #serialize_cast_value_compatible? to route the lazy class instance variable @serialize_cast_value_compatible through IES.

This method lazily caches a boolean: return @x if defined?(@x); @x = <computation>. It's called during type-map initialization (every adapter's initialize_type_map creates type objects whose constructors eagerly call this to precompute the value). The class instance variable write fails from a non-main Ractor (Ractor::IsolationError: "can not set instance variables of classes/modules by non-main Ractors").

Fix: route through IES so each Ractor computes + caches its own value. The computation is deterministic (compares ancestor positions of two methods), so per-Ractor recomputation yields the same result. Each including class gets its own IES key (keyed by self.name). String-eval'd (no captured binding), callable from any Ractor.



1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1898

def _install_activerecord_serialize_cast_value_patch
  return if @ar_serialize_cast_patched
  @ar_serialize_cast_patched = true
  _register_patch :activerecord_serialize_cast_value, "8.1"
  return unless defined?(::ActiveModel::Type::SerializeCastValue)

  ::ActiveModel::Type::SerializeCastValue::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def serialize_cast_value_compatible?
      key = :"ractor_rails_shim_scv_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      result = ancestors.index(instance_method(:serialize_cast_value).owner) <= ancestors.index(instance_method(:serialize).owner)
      RactorRailsShim.storage[key] = result
      result
    end
  RUBY
end

._install_activerecord_transaction_callbacks_patchObject

In the shared :ractor graph, ActiveRecord model classes can end up with a nil __callbacks (the class_attribute value can't be made shareable when it holds unshareable callback Procs, so the shim's shareable fallback returns nil). has_transactional_callbacks? calls the generated _rollback_callbacks / _commit_callbacks / _before_commit_callbacks readers, which do __callbacks[:kind] directly — bypassing the run_callbacks_with_nil_safe guard. With a nil __callbacks that raises NoMethodError: undefined method '[]' for nil, breaking every save / update / destroy (they run inside a transaction). Guard it: a nil / empty callback table means no transactional callbacks.



2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2251

def _install_activerecord_transaction_callbacks_patch
  return if @activerecord_transaction_callbacks_patched
  @activerecord_transaction_callbacks_patched = true
  _register_patch :activerecord_transaction_callbacks, "8.1"
  return unless defined?(::ActiveRecord::Base)
  ar = ::ActiveRecord::Base
  ar.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def has_transactional_callbacks?
      cb = __callbacks
      return false unless cb
      !((cb[:rollback] || []).empty?) ||
        !((cb[:commit] || []).empty?) ||
        !((cb[:before_commit] || []).empty?)
    end
  RUBY
end

._install_arel_bind_block_patchObject



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 86

def _install_arel_bind_block_patch
  return if @arel_bind_block_patched
  @arel_bind_block_patched = true
  _register_patch :arel_bind_block, "8.1"

  # PostgreSQL: proc { |i| "$#{i}" }
  if defined?(::Arel::Visitors::PostgreSQL)
    ::Arel::Visitors::PostgreSQL.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def bind_block
        RactorRailsShim::PgBindBlock
      end
    RUBY
  end

  # ToSql (base): proc { "?" }
  if defined?(::Arel::Visitors::ToSql)
    ::Arel::Visitors::ToSql.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def bind_block
        RactorRailsShim::SqlBindBlock
      end
    RUBY
  end
end

._install_arel_visitor_dispatch_cache_patchObject

Patch Arel::Visitors::Visitor.dispatch_cache to route through IES.

dispatch_cache is a class method that lazily initializes a class instance variable: @dispatch_cache ||= Hash.new { |hash, klass| ... } .compare_by_identity. The cache maps AST node classes to visit method symbols (e.g. Arel::Nodes::SelectStatement -> :visit_Arel_Nodes_SelectStatement). It's read on every Arel traversal (every query) and written on cache-miss (default proc) and on method-not-found fallback (visit() rescue).

The class instance variable @dispatch_cache can't be read or written by a non-main Ractor (Ractor::IsolationError). The Hash also has a default Proc (which is intrinsically unshareable), so the value can't be frozen+shared. This is the third wall a worker hits during the first query: Post.count -> adapter creation -> arel_visitor -> ToSql#initialize -> Visitor#initialize -> get_dispatch_cache -> self.class.dispatch_cache -> @dispatch_cache ||= ....

Fix: route through per-class IES. Each Ractor builds its own mutable cache (with its own default proc) on first access. The key includes self.name so each visitor subclass (ToSql, SQLite3, etc.) gets its own cache — necessary because the method-not-found fallback (dispatch[object.class] = dispatch[superklass]) resolves differently per visitor class. The main ractor's existing @dispatch_cache (if any) is left orphaned; new visitors created after the patch use the IES cache. String-eval'd (no captured binding), callable from any Ractor.



1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 1679

def _install_arel_visitor_dispatch_cache_patch
  return if @arel_visitor_patched
  @arel_visitor_patched = true
  _register_patch :arel_visitor_dispatch_cache, "8.1"
  return unless defined?(::Arel::Visitors::Visitor)

  ::Arel::Visitors::Visitor.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def dispatch_cache
      key = :"ractor_rails_shim_arel_dispatch_\#{name || object_id}"
      v = RactorRailsShim.storage[key]
      return v if v
      cache = Hash.new do |hash, klass|
        hash[klass] = :"visit_\#{(klass.name || "").gsub("::", "_")}"
      end.compare_by_identity
      RactorRailsShim.storage[key] = cache
      cache
    end
  RUBY
end

._install_as_attribute_types_cache_reset!Object

ActiveStorage::Blob#metadata is declared via store :metadata, coder: ActiveRecord::Coders::JSON, which registers a Type::Serialized cast type through decorate_attributes (a lazily-memoized PendingDecorator). decorate_attributes only resets @default_attributes, NOT the separately memoized @attribute_types — so if @attribute_types was primed as a plain Type::Text/Type::Value by an earlier attribute access during class load (which happens when the class is freshly autoloaded in a worker Ractor's empty constant namespace), the decorated (Serialized) type is never recomputed and write_store_attribute later raises "the column 'metadata' has not been configured as a store". Patch decorate_attributes to also invalidate @attribute_types so the decorated type is picked up.



400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 400

def _install_as_attribute_types_cache_reset!
  return if @_as_attr_types_reset_installed
  @_as_attr_types_reset_installed = true
  return unless defined?(::ActiveRecord::Base)
  ::ActiveRecord::Base.singleton_class.prepend(Module.new do
    def decorate_attributes(names = nil, &decorator)
      super
      instance_variable_set(:@attribute_types, nil)
    rescue StandardError
      super
    end
  end)
end

._install_caching_key_generator_patchObject

Patch ActiveSupport::CachingKeyGenerator#generate_key. Its @cache_keys ivar is a Concurrent::Map; make_app_shareable! rewrites Concurrent::Map ivars into FROZEN Hashes (see make_shareable.rb), so a worker Ractor's @cache_keys[args.join("|")] ||= ... write raises FrozenError. The cache is pure memoization keyed by (generator, args), so route it through IsolatedExecutionState (one mutable cache per Ractor). The inner OpenSSL::Digest lambda patch.



405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 405

def _install_caching_key_generator_patch
  return if @caching_key_generator_patched
  @caching_key_generator_patched = true
  _register_patch :caching_key_generator, "8.1"
  return unless defined?(::ActiveSupport::CachingKeyGenerator)
  ::ActiveSupport::CachingKeyGenerator.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def generate_key(*args)
      store = (RactorRailsShim.storage[:ractor_rails_shim_caching_key_generator] ||= {})
      key = "\#{object_id}|\#{args.join("|")}"
      store.fetch(key) { store[key] = @key_generator.generate_key(*args) }
    end
  RUBY
end

._install_callbacks_nil_safe_patchObject

Patch ActiveSupport::Inflector::Inflections to not read @en_instance / @instance class ivars from a worker Ractor. The inflections instance holds rules (Arrays/Hashes of Strings) populated at boot; for a frozen shared app it's read-only. Workers share the main-ractor's inflections instance via a shareable fallback (made shareable in place). instance / instance_or_fallback are called per-request during routing (camelize). Patch ActiveSupport::Callbacks#run_callbacks to tolerate a nil __callbacks (the case in worker Ractors whose class_attribute fallback couldn't be made shareable because callback chains hold frozen, self-capturing Procs). For a frozen, read-only shared app the boot-time callbacks (ExecutionContext push/pop, CurrentAttributes clear) already ran in the main Ractor at boot; worker Ractors don't need to re-run them per request (CurrentAttributes/ExecutionContext are thread-local, hence per-Ractor, and start empty in a fresh worker). When __callbacks is nil, run_callbacks just yields the block — matching the empty-chain fast path in the original. Moved here from execution_wrapper.rb (it patches ActiveSupport::Callbacks, not ExecutionWrapper).



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 49

def _install_callbacks_nil_safe_patch
  return if @callbacks_nil_safe_patched
  @callbacks_nil_safe_patched = true
  _register_patch :callbacks_nil_safe, "8.1"
  return unless defined?(::ActiveSupport::Callbacks)
  ::ActiveSupport::Callbacks.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def run_callbacks_with_nil_safe(kind, type = nil)
      kind = kind.to_sym
      strategy = RactorRailsShim.storage_strategy
      # The generalized callback transport (see RactorRailsShim::Callbacks).
      # If any registered transport applies to this `kind` and the gate is
      # open (Ractor mode: __callbacks chain is empty — the worker case;
      # Thread mode: gate is closed so the real chain runs), delegate replay
      # to the Registry, which owns the single `yield` and asks each
      # applicable transport to run its before/after work. This generalizes
      # the old hardcoded `kind == :process_action || kind == :destroy` to
      # ANY callback kind that a transport has been registered for.
      registry = ::RactorRailsShim::Callbacks.registry
      if registry.applicable(kind).any?
        callbacks = __callbacks[kind] if __callbacks
        if strategy.replay_callbacks?(callbacks)
          return registry.replay(self, kind) { (yield if block_given?) }
        end
      end
      callbacks = __callbacks[kind] if __callbacks
      if callbacks.nil? || callbacks.empty?
        yield if block_given?
      else
        run_callbacks_without_nil_safe(kind, type) { yield if block_given? }
      end
    end
    alias_method :run_callbacks_without_nil_safe, :run_callbacks
    alias_method :run_callbacks, :run_callbacks_with_nil_safe
  RUBY
end

._install_controller_logger_patchObject

logger is delegated to config (a class_attribute) which loses its value when make_app_shareable! deep-freezes the shared graph, so a worker reads a nil/empty config and logger raises DelegationError ("logger delegated to config, but config is nil"). default_render (ImplicitRender) calls logger for its debug log, so a no-template request (e.g. GET /) raises in workers. Fall back to the CLASS-level ActionController::Base.config.logger — which IS correct in workers (it reads the frozen shareable class config) — and finally to Rails.logger. The real delegated logger still wins whenever it is readable (main, or workers whose config propagated).



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 363

def _install_controller_logger_patch
  return if @controller_logger_patched
  @controller_logger_patched = true
  _register_patch :controller_logger, "8.1"
  return unless defined?(::ActionController::Base)
  ::ActionController::Base.prepend(Module.new do
    def logger
      super
    rescue ActiveSupport::DelegationError
      ::ActionController::Base.config.logger || ::Rails.logger
    end

    # `config` is a class_attribute whose value cannot be deep-frozen, so
    # workers read nil (or a frozen empty default) instead of the real
    # action_controller config. Several code paths then call
    # `config.inheritable_copy` / `config.logger` and blow up
    # (NoMethodError / DelegationError) — including
    # ActionView::Helpers::ControllerHelper#assign_controller during
    # template rendering. Fall back to the CLASS-level
    # `ActionController::Base.config`, which IS correct in workers (it
    # reads the frozen shareable class config).
    def config
      cfg = super
      cfg || ::ActionController::Base.config
    rescue NoMethodError, ActiveSupport::DelegationError
      ::ActionController::Base.config
    end
  end)
  ::ActionController::Base.singleton_class.prepend(Module.new do
    def logger
      super
    rescue ActiveSupport::DelegationError
      ::ActionController::Base.config.logger || ::Rails.logger
    end
  end)
end

._install_controller_params_wrapper_patchObject

ParamsWrapper#_wrapper_options is a class_attribute whose default value (ActionController::ParamsWrapper::Options.from_hash(format: [])) holds a Mutex.new and back-references the controller/model klass, so it cannot be deep-frozen into the shared graph — its value is nil in a worker Ractor (see the #__class_attr__wrapper_options boot warning). ParamsWrapper#_wrapper_formats then calls _wrapper_options.format and raises NoMethodError: private method `format' called for nil on every wrapped request (e.g. POST /posts/:id/comments). Fall back to the class_attribute's own declared default — wrapping disabled — whenever the real value is unreadable in the worker. The default is built lazily inside the method (never captured in a closure) so the prepended module stays Ractor-shareable — the Options object (which owns a Mutex) only ever lives in the worker that calls it.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 413

def _install_controller_params_wrapper_patch
  return if @controller_params_wrapper_patched
  @controller_params_wrapper_patched = true
  _register_patch :controller_params_wrapper, "8.1"
  return unless defined?(::ActionController::Base) &&
                defined?(::ActionController::ParamsWrapper)
  ::ActionController::Base.prepend(Module.new do
    def _wrapper_options
      super ||
        ::ActionController::ParamsWrapper::Options.from_hash(format: [])
    rescue NoMethodError, ActiveSupport::DelegationError
      ::ActionController::ParamsWrapper::Options.from_hash(format: [])
    end
  end)
end

._install_csrf_reset_patchObject

In the shared :ractor graph, Devise's engine controllers (e.g. Devise::SessionsController) end up with a nil csrf_token_storage_strategy at request time — the value is dropped when make_app_shareable! deep-freezes the app (RequestForgeryProtection sets it only on ActionController::Base.config, and the per-controller frozen config copy loses it). A worker then raises NoMethodError on reset_csrf_token during reset_session (logout / sign_out). Guard the reset so a missing strategy is a no-op — reset_session regenerates the session id anyway, discarding the CSRF token.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 287

def _install_csrf_reset_patch
  return if @csrf_reset_patched
  @csrf_reset_patched = true
  _register_patch :csrf_reset, "8.1"
  return unless defined?(::ActionController::RequestForgeryProtection)
  rfp = ::ActionController::RequestForgeryProtection
  rfp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def reset_csrf_token(request) # :doc:
      request.env.delete(CSRF_TOKEN)
      strat = csrf_token_storage_strategy
      strat.reset(request) if strat
    end
  RUBY

  # `csrf_token_storage_strategy` and `forgery_protection_strategy` are
  # both delegated to the controller `config` (a class_attribute). In the
  # shared :ractor graph the per-controller frozen `config` copy loses
  # them, so a worker reads nil and CSRF token handling raises
  # ("undefined method 'fetch' for nil" / "undefined method 'new' for
  # nil"). Default to the standard SessionStore / Exception strategies so
  # token ISSUANCE and VALIDATION work in workers.
  #
  # NOTE: prepending to RequestForgeryProtection (a module) would NOT
  # reach the controllers, because ActionController::Base already
  # *included* it before this patch runs. Prepend to the BASE CLASS so
  # every controller (incl. Devise subclasses) picks up the default.
  # The defaults are referenced via their constant paths (not captured
  # locals) because `def` bodies do not close over enclosing locals.
  ::ActionController::Base.prepend(Module.new do
    def csrf_token_storage_strategy
      super || ::ActionController::RequestForgeryProtection::SessionStore.new
    end

    def forgery_protection_strategy
      super || ::ActionController::RequestForgeryProtection::ProtectionMethods::Exception
    end

    # Delegated to `config` (a class_attribute) which loses its value when
    # make_app_shareable! deep-freezes the shared graph, so a worker reads
    # nil. With a nil param key, form_authenticity_param reads params[nil]
    # and CSRF VALIDATION rejects every POST (even with a valid token),
    # because the token is carried under the real key (:authenticity_token).
    # Default to the standard param name so validation can find the token.
    def request_forgery_protection_token
      super || :authenticity_token
    end

    # `allow_forgery_protection` is delegated to `config` (a class_attribute
    # carrying the full, unshareable action_controller config graph). That
    # graph cannot be deep-frozen, so the shareable fallback built at
    # prepare_for_ractors! falls back to the EMPTY default — and a worker's
    # view `config` therefore reports `allow_forgery_protection = nil`,
    # making `protect_against_forgery?` false and suppressing CSRF token
    # issuance (no `<meta name="csrf-token">`, no hidden form field). The
    # CLASS-level `ActionController::Base.config.allow_forgery_protection`
    # IS correct in workers (it reads the frozen shareable class config),
    # so fall back to it when the per-instance/config value is unavailable.
    # The real value still wins whenever it is readable (main, or workers
    # whose config propagated), so apps that disable forgery protection
    # are unaffected.
    def allow_forgery_protection
      super || ::ActionController::Base.config.allow_forgery_protection
    end
  end)
end

._install_devise_authenticatable_patchObject

Devise::Models::Authenticatable::ClassMethods#devise_parameter_filter memoizes @devise_parameter_filter ||= Devise::ParameterFilter.new(...) on the MODEL CLASS. A worker Ractor cannot set an instance variable on a shared class/module, so calling it raises "can not set instance variables of classes/modules by non-main Ractors". Route the memoized filter through IsolatedExecutionState (per-Ractor), keyed by the (shared, stable) class object_id. case_insensitive_keys / strip_whitespace_keys are class_attribute values the shim already routes through IES, so they read fine in a worker.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ractor_rails_shim/patches/devise.rb', line 85

def _install_devise_authenticatable_patch
  return if @devise_authenticatable_patched
  @devise_authenticatable_patched = true
  _register_patch :devise_authenticatable, "5.0"
  return unless defined?(::Devise::Models::Authenticatable::ClassMethods)
  ::Devise::Models::Authenticatable::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def devise_parameter_filter
      key = :"ractor_rails_shim_devise_param_filter_\#{object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      f = Devise::ParameterFilter.new(case_insensitive_keys, strip_whitespace_keys)
      RactorRailsShim.storage[key] = f
      f
    end
  RUBY
end

._install_devise_failure_app_patchObject

Devise::FailureApp.call memoizes its Rack endpoint in a class-level ivar (@respond ||= action(:respond)). A worker Ractor cannot set instance variables on a shared class/module, so calling it raises "can not set instance variables of classes/modules by non-main Ractors". Route the memoized endpoint through IsolatedExecutionState (per-Ractor), so each worker builds and caches its own copy without mutating the shared class.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ractor_rails_shim/patches/devise.rb', line 48

def _install_devise_failure_app_patch
  return if @devise_failure_app_patched
  @devise_failure_app_patched = true
  _register_patch :devise_failure_app, "5.0"
  return unless defined?(::Devise::FailureApp)
  ::Devise::FailureApp.singleton_class.module_eval <<-'RUBY', __FILE__, __LINE__ + 1
    def call(env)
      respond = RactorRailsShim.storage[:ractor_rails_shim_devise_failure_respond] ||= action(:respond)
      respond.call(env)
    end
  RUBY
  # Devise::FailureApp#relative_url_root reads `config.action_controller
  # .try(:relative_url_root)`. `config.action_controller` is a
  # Rails::Railtie::Configuration whose `relative_url_root` is NOT a real
  # method, so `try` triggers `method_missing`, which reads the `@@options`
  # class variable — unreadable from a worker Ractor. `Rails.application
  # .config.relative_url_root` IS a real method on
  # Rails::Application::Configuration (no method_missing, no class var), so
  # use just that; for the common case (no relative root) it returns nil.
  if defined?(::Devise::FailureApp)
    ::Devise::FailureApp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def relative_url_root
        @relative_url_root ||= Rails.application.config.relative_url_root
      end
    RUBY
  end
end

._install_devise_mailer_patchObject

Devise.mailer reads @@mailer_ref (a class variable) which raises IsolationError from a worker Ractor. The @@mailer_ref holds a Devise::Getter instance (which is shareable — it only holds a String class name). Capture it at prepare time and have workers read the captured shareable copy instead of the cvar. Also patch Devise.parent_mailer and Devise.mailer_sender which read @@parent_mailer / @@mailer_sender cvars — these are read when building mailer messages.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ractor_rails_shim/patches/devise.rb', line 110

def _install_devise_mailer_patch
  return if @devise_mailer_patched
  @devise_mailer_patched = true
  _register_patch :devise_mailer, "5.0"
  return unless defined?(::Devise)

  # Capture the Getter instance (it holds a String — shareable).
  mailer_ref = nil
  begin
    mailer_ref = ::Devise.class_variable_get(:@@mailer_ref)
  rescue StandardError
    nil
  end
  if mailer_ref
    RactorRailsShim.const_set(:SHAREABLE_DEVISE_MAILER_REF,
      Ractor.make_shareable(mailer_ref))
  end

  ::Devise.singleton_class.module_eval <<-'RUBY', __FILE__, __LINE__ + 1
    def mailer
      if ::Ractor.main?
        @@mailer_ref.get
      else
        ref = ::RactorRailsShim::SHAREABLE_DEVISE_MAILER_REF
        ref ? ref.get : ::Devise::Mailer
      end
    end
  RUBY
end

._install_devise_url_helpers_patchObject



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/ractor_rails_shim/patches/devise.rb', line 140

def _install_devise_url_helpers_patch
  return if @devise_url_helpers_patched
  @devise_url_helpers_patched = true
  _register_patch :devise_url_helpers, "5.0"
  # Generate DeviseMappingSnapshot predicates from Devise::MODULES now that
  # Devise is loaded. Replaces the hardcoded fallback list with the actual
  # module set (incl. any third-party gems added at boot). No-op if
  # Devise::MODULES isn't defined.
  DeviseMappingSnapshot.generate_predicates_from_devise_modules!
  return unless defined?(::Devise::Controllers::UrlHelpers)

  mod = ::Devise::Controllers::UrlHelpers

  # Patch Devise.mappings FIRST (before the snapshot below) so every
  # subsequent read — including the snapshot — does NOT trigger a route
  # reload. Routes are fully drawn by Rails.application.initialize! before
  # prepare_for_ractors! runs, so @@mappings is already populated in main;
  # workers read the shareable snapshot instead. The ORIGINAL
  # Devise.mappings calls reload_routes_unless_loaded, which during
  # prepare collapses the RouteSet to a single railtie route (rails/info)
  # that then gets frozen into the shared app graph — breaking routing for
  # every worker Ractor.
  if defined?(::Devise) && ::Devise.respond_to?(:mappings)
    ::Devise.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def mappings
        return RactorRailsShim::DEVISE_MAPPINGS if !Ractor.main? && RactorRailsShim.const_defined?(:DEVISE_MAPPINGS)
        ::Devise.class_variable_get(:@@mappings)
      end
    RUBY
  end

  # Capture a shareable snapshot of Devise.mappings in MAIN (after routes
  # are drawn). The real Mapping objects hold an unshareable failure-app
  # lambda and a default-proc Hash, so we can't share them directly; build
  # a DeviseMappingSnapshot per scope (see make_shareable.rb). Workers read
  # this via the patched Devise.mappings reader.
  if Ractor.main? && defined?(::Devise)
    begin
      h = ::Devise.mappings
      snap = {}
      h.each { |scope, mapping| snap[scope] = _devise_mapping_snapshot(mapping) }
      snap = _swallow("make devise mappings shareable") { Ractor.make_shareable(snap) }
      const_set(:DEVISE_MAPPINGS, snap) if snap
    rescue StandardError
      nil
    end
  end

  # Redefine each generated helper via string eval (no captured binding).
  # Replicate the EXACT body of Devise::Controllers::UrlHelpers.generate_helpers!
  # (lib/devise/controllers/url_helpers.rb): the helper does NOT call the
  # alias method on the context — it reconstructs the REAL route helper name
  # from `action` + `scope` + `module_name` (e.g. alias `session_path` ->
  # real `user_session_path`) and sends THAT to the context. Bake in
  # `action`/`module_name`/`path_or_url` as literals; `scope` is resolved
  # per-call from the argument, so interpolate it at runtime (\\#{scope}).
  routes = ::Devise::URL_HELPERS.slice(*(::Devise.mappings.values.map(&:used_helpers).flatten.uniq))
  routes.each do |module_name, actions|
    [:path, :url].each do |path_or_url|
      actions.each do |action|
        action_prefix = action ? "#{action}_" : ""
        method = :"#{action_prefix}#{module_name}_#{path_or_url}"
        mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{method}(resource_or_scope, *args)
            scope = Devise::Mapping.find_scope!(resource_or_scope)
            router_name = Devise.mappings[scope].router_name
            context = router_name ? send(router_name) : _devise_route_context
            context.send("#{action_prefix}\#{scope}_#{module_name}_#{path_or_url}", *args)
          end
        RUBY
      end
    end
  end
end

._install_exception_wrapper_patchObject

Patch ActionDispatch::ExceptionWrapper instance methods that read @@rescue_responses / @@rescue_templates class variables directly (bypassing the mattr_accessor reader the shim already reroutes through IES). Workers can't read class vars; route through the class method.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 61

def _install_exception_wrapper_patch
  return if @exception_wrapper_patched
  @exception_wrapper_patched = true
  _register_patch :exception_wrapper, "8.1"
  return unless defined?(::ActionDispatch::ExceptionWrapper)
  ::ActionDispatch::ExceptionWrapper.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def rescue_template
      self.class.rescue_templates[exception_class_name]
    end
    def status_code
      ActionDispatch::Response.rack_status_code(self.class.rescue_responses[exception_class_name])
    end
    def rescue_response?
      self.class.rescue_responses.key?(exception.class.name)
    end
  RUBY
  # Also patch the class method (status_code_for_exception) that reads
  # @@rescue_responses directly — called by ActionController::Instrumentation
  # at request time. Route through the mattr reader (which the shim already
  # reroutes through IES).
  ::ActionDispatch::ExceptionWrapper.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def status_code_for_exception(class_name)
      ActionDispatch::Response.rack_status_code(rescue_responses[class_name])
    end
  RUBY
end

._install_execution_context_patchObject



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 239

def _install_execution_context_patch
  return if @exec_context_patched
  @exec_context_patched = true
  _register_patch :execution_context, "8.1"
  return unless defined?(::ActiveSupport::ExecutionContext)
  ec = ::ActiveSupport::ExecutionContext
  acb_key = :ractor_rails_shim_exec_context_after_change_callbacks
  nest_key = :ractor_rails_shim_exec_context_nestable
  acb_key_str = acb_key.inspect
  nest_key_str = nest_key.inspect
  ec.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def after_change_callbacks
      v = RactorRailsShim.storage[#{acb_key_str}]
      return v if RactorRailsShim.storage.key?(#{acb_key_str})
      if Ractor.main? && instance_variable_defined?(:@after_change_callbacks)
        v = @after_change_callbacks
        RactorRailsShim.storage[#{acb_key_str}] = v
        v
      else
        arr = []
        RactorRailsShim.storage[#{acb_key_str}] = arr
        arr
      end
    end
    def after_change(&block)
      after_change_callbacks << block
    end
    def nestable
      v = RactorRailsShim.storage[#{nest_key_str}]
      return v if RactorRailsShim.storage.key?(#{nest_key_str})
      if Ractor.main? && instance_variable_defined?(:@nestable)
        v = @nestable
        RactorRailsShim.storage[#{nest_key_str}] = v
        v
      else
        false
      end
    end
    def nestable=(val)
      RactorRailsShim.storage[#{nest_key_str}] = val
      @nestable = val if Ractor.main?
      val
    end
  RUBY
  # Rewrite the methods that read @after_change_callbacks directly.
  ec.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def set(**options)
      options.symbolize_keys!
      keys = options.keys
      store = record.store
      previous_context = if block_given?
        keys.zip(store.values_at(*keys)).to_h
      end
      store.merge!(options)
      after_change_callbacks.each(&:call)
      if block_given?
        begin
          yield
        ensure
          store.merge!(previous_context)
          after_change_callbacks.each(&:call)
        end
      end
    end
    def []=(key, value)
      record.store[key.to_sym] = value
      after_change_callbacks.each(&:call)
    end
  RUBY
end

._install_flash_helpers_patchObject

Patch the flash-type helper methods (notice, alert, ...) defined by ActionController::Metal::Flash#add_flash_types via define_method(type) { request.flash[type] }. That block is compiled in the MAIN Ractor, so calling it from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor". Redefine each flash type as a string-eval'd method (no captured binding) so it is callable from any Ractor. Called at prepare_for_ractors! time, after the controllers are loaded and the types are known.



437
438
439
440
441
442
443
444
445
446
447
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 437

def _install_flash_helpers_patch
  return if @flash_helpers_patched
  @flash_helpers_patched = true
  _register_patch :flash_helpers, "8.1"
  return unless defined?(::ActionController::Base)
  types = ::ActionController::Base._flash_types rescue []
  types.each do |type|
    ::ActionController::Base.class_eval "def #{type}; request.flash[#{type.inspect}]; end"
    ::ActionController::Base.send(:private, type) if ::ActionController::Base.private_method_defined?(type) rescue nil
  end
end

._install_hash_compute_if_absent_patchObject

The shim's make_app_shareable! replaces Concurrent::Map instance variables (which are not Ractor-shareable) with plain Hashes so workers can read them. But Rails code calls Concurrent::Map#compute_if_absent on these caches (e.g. ActiveModel::AttributeMethods' attribute_method_patterns_ cache). Plain Hash lacks that method, so we add a compatible definition. See RactorRailsShim::Patches::HashComputeIfAbsent for the full semantics contract (nil-storing, per-Hash IES isolation via object_id). Install the Hash#compute_if_absent patch. Delegates to RactorRailsShim::Patches::HashComputeIfAbsent.install (extracted Step 22.1, Issue #22); kept as a facade method so the existing framework_patch_dispatch auto-discovery (which enumerates _install_*_patch singleton methods), compute_if_absent_spec.rb, and storage_spec.rb keep passing. The idempotency flag now lives on the role object. See Patches::HashComputeIfAbsent for the contract (mutable vs frozen receiver, nil-storing, per-Hash IES isolation via object_id).



346
347
348
# File 'lib/ractor_rails_shim/patches/core.rb', line 346

def _install_hash_compute_if_absent_patch
  Patches::HashComputeIfAbsent.install
end

._install_i18n_backend_patchObject

Patch I18n::Backend::Simple::Implementation#translations and #store_translations. The original uses a MUTEX-backed Concurrent::Hash default block (MUTEX is a non-shareable constant on the module), so a worker Ractor that builds its own (per the patched I18n::Config#backend) backend and lazy-loads translations hits "can not access non-shareable objects in constant ...MUTEX". Worker-local backends are single-threaded (a Ractor serializes its requests), so drop the mutex and use a plain Hash. Applied at prepare_for_ractors! time (after the i18n backend class is loaded).



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/ractor_rails_shim/patches/i18n.rb', line 390

def _install_i18n_backend_patch
  return if @i18n_backend_patched
  @i18n_backend_patched = true
  _register_patch :i18n_backend, "8.1"
  return unless defined?(::I18n::Backend::Simple::Implementation)
  impl = ::I18n::Backend::Simple::Implementation
  impl.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def translations(do_init: false)
      init_translations if do_init && !initialized?
      @translations ||= {}
    end
    def store_translations(locale, data, options = {})
      if ::I18n.enforce_available_locales &&
         ::I18n.available_locales_initialized? &&
         !::I18n.locale_available?(locale)
        return data
      end
      locale = locale.to_sym
      translations[locale] ||= {}
      data = ::I18n::Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
      ::I18n::Utils.deep_merge!(translations[locale], data)
    end
  RUBY
end

._install_i18n_interpolation_patchObject

Patch I18n.interpolate_hash. It reads INTERPOLATION_PATTERNS_CACHE — a constant Hash with a default proc (unshareable) — to fetch the compiled interpolation Regexp. A worker Ractor cannot read that constant, raising "can not access non-shareable objects in constant I18n::INTERPOLATION_PATTERNS_CACHE by non-main ractor". Route the cache through IsolatedExecutionState so each Ractor compiles its own Regexp once.



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/ractor_rails_shim/patches/i18n.rb', line 421

def _install_i18n_interpolation_patch
  return if @i18n_interpolation_patched
  @i18n_interpolation_patched = true
  _register_patch :i18n_interpolation, "8.1"
  return unless defined?(::I18n)
  ::I18n.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def interpolate_hash(string, values)
      patterns = config.interpolation_patterns
      cache = (RactorRailsShim.storage[:ractor_rails_shim_i18n_interp_cache] ||= {})
      pattern = cache[patterns] ||= ::Regexp.union(patterns)
      interpolated = false

      interpolated_string = string.gsub(pattern) do |match|
        interpolated = true

        if match == '%%'
          '%'
        else
          key = ($1 || $2 || match.tr("%{}", "")).to_sym
          value = if values.key?(key)
                    values[key]
                  else
                    config.missing_interpolation_argument_handler.call(key, values, string)
                  end
          value = value.call(values) if value.respond_to?(:call)
          $3 ? sprintf("%#{$3}", value) : value
        end
      end

      interpolated ? interpolated_string : string
    end
  RUBY
end

._install_i18n_patchObject

Patch I18n::Config's class-variable-backed accessors (default_locale, locale, backend, etc.) to not read @@cvars from a worker Ractor. I18n defines these manually (@@default_locale ||= :en), not via cattr_accessor, so the shim's mattr rewrite doesn't catch them. The values are frozen Symbols / shareable config objects; route the frequently-read ones (default_locale, locale) through IES with the same default. Read per-request during view lookup (LookupContext details).



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/ractor_rails_shim/patches/i18n.rb', line 17

def _install_i18n_patch
  return if @i18n_patched
  @i18n_patched = true
  _register_patch :i18n, "8.1"
  return unless defined?(::I18n::Config)
  cfg = ::I18n::Config
  dl_key = :ractor_rails_shim_i18n_default_locale
  l_key = :ractor_rails_shim_i18n_locale
  av_key = :ractor_rails_shim_i18n_available_locales
  avs_key = :ractor_rails_shim_i18n_available_locales_set
  dl_key_str = dl_key.inspect
  l_key_str = l_key.inspect
  av_key_str = av_key.inspect
  avs_key_str = avs_key.inspect
  cfg.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def default_locale
      v = RactorRailsShim.storage[#{dl_key_str}]
      return v if RactorRailsShim.storage.key?(#{dl_key_str})
      if Ractor.main? && defined?(@@default_locale)
        cv = @@default_locale
        RactorRailsShim.storage[#{dl_key_str}] = cv
        return cv
      end
      RactorRailsShim.storage[#{dl_key_str}] = :en
      :en
    end
    def default_locale=(locale)
      v = locale && locale.to_sym
      RactorRailsShim.storage[#{dl_key_str}] = v
      @@default_locale = v if Ractor.main?
      v
    end
    def locale
      v = RactorRailsShim.storage[#{l_key_str}]
      return v if RactorRailsShim.storage.key?(#{l_key_str})
      default_locale
    end
    def locale=(locale)
      v = locale && locale.to_sym
      RactorRailsShim.storage[#{l_key_str}] = v
      v
    end
    # available_locales is read per-request during view template lookup
    # (ActionView::Resolver::PathParser#build_path_regex). The original
    # reads the @@available_locales class variable, which a worker Ractor
    # cannot access (Ractor::IsolationError). Route it through IES. In
    # main we mirror the class var; in a worker we default to [:en] WITHOUT
    # delegating to backend.available_locales (that path reads the @@backend
    # / @@load_path class vars, which are also unreadable from a worker).
    # The template-regex path only needs the list of locale symbols, and
    # [:en] is the documented I18n default — correct for apps that don't
    # set config.i18n.available_locales explicitly.
    def available_locales
      v = RactorRailsShim.storage[#{av_key_str}]
      return v if RactorRailsShim.storage.key?(#{av_key_str})
      if Ractor.main?
        if defined?(@@available_locales) && (cv = @@available_locales)
          RactorRailsShim.storage[#{av_key_str}] = cv
          return cv
        end
        al = backend.available_locales
        al = al.freeze if al.respond_to?(:freeze) && !al.frozen?
        RactorRailsShim.storage[#{av_key_str}] = al
        return al
      end
      al = [:en].freeze
      RactorRailsShim.storage[#{av_key_str}] = al
      al
    end
    def available_locales=(locales)
      v = Array(locales).map { |l| l.to_sym }
      v = nil if v.empty?
      RactorRailsShim.storage[#{av_key_str}] = v
      @@available_locales = v if Ractor.main?
      v
    end
    def available_locales_set
      v = RactorRailsShim.storage[#{avs_key_str}]
      return v if RactorRailsShim.storage.key?(#{avs_key_str})
      if Ractor.main? && defined?(@@available_locales_set) && (cv = @@available_locales_set)
        RactorRailsShim.storage[#{avs_key_str}] = cv
        return cv
      end
      s = available_locales.inject(Set.new) { |set, locale| set << locale.to_s << locale.to_sym }
      RactorRailsShim.storage[#{avs_key_str}] = s
      s
    end
    def available_locales_initialized?
      !!(RactorRailsShim.storage[#{av_key_str}])
    end
    # enforce_available_locales is read during every I18n.translate (the
    # Label/tag translation path in views). The original reads the
    # @@enforce_available_locales class variable, which a worker Ractor
    # cannot access (Ractor::IsolationError). Route it through IES; in main
    # we mirror the class var, in a worker we default to `true` (the
    # documented I18n default).
    def enforce_available_locales
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_enforce]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_enforce)
      if Ractor.main? && defined?(@@enforce_available_locales)
        cv = @@enforce_available_locales
        RactorRailsShim.storage[:ractor_rails_shim_i18n_enforce] = cv
        return cv
      end
      RactorRailsShim.storage[:ractor_rails_shim_i18n_enforce] = true
      true
    end
    def enforce_available_locales=(val)
      v = !!val
      RactorRailsShim.storage[:ractor_rails_shim_i18n_enforce] = v
      @@enforce_available_locales = v if Ractor.main?
      v
    end
    # backend reads the @@backend class variable (which a worker Ractor
    # cannot access). The backend holds the loaded translations. Because the
    # translation data can contain Procs (e.g. `number.nth.ordinals` in
    # ActiveSupport's en locale), the whole backend cannot be deep-frozen
    # and shared. Instead, each worker builds its OWN backend instance (of
    # the same class as the main backend, so fallbacks etc. are preserved)
    # and lazy-loads translations from the shareable +load_path+ (see the
    # patched `load_path`/`load_path=`). The worker-local backend is mutable
    # (its @interpolations Proc is created in the worker, so it's fine).
    def backend
      if Ractor.main?
        @@backend ||= ::I18n::Backend::Simple.new
      else
        key = :ractor_rails_shim_i18n_backend
        b = RactorRailsShim.storage[key]
        return b if b
        cls = (RactorRailsShim.const_defined?(:I18N_BACKEND_CLASS) && RactorRailsShim::I18N_BACKEND_CLASS) || ::I18n::Backend::Simple
        b = cls.new
        RactorRailsShim.storage[key] = b
        b
      end
    end
    def backend=(value)
      @@backend = value
    end
    # load_path reads the @@load_path class variable (unreadable from a
    # worker). Capture the (shareable) list of translation file paths in
    # main; workers reload translations from disk via these paths.
    def load_path
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_load_path]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_load_path)
      if Ractor.main?
        lp = (defined?(@@load_path) && @@load_path) || []
        lp = lp.dup.freeze if lp.respond_to?(:freeze) && !lp.frozen?
        RactorRailsShim.storage[:ractor_rails_shim_i18n_load_path] = lp
        lp
      else
        RactorRailsShim.const_defined?(:I18N_LOAD_PATH) ? RactorRailsShim::I18N_LOAD_PATH : []
      end
    end
    def load_path=(lp)
      lp = Array(lp)
      RactorRailsShim.storage[:ractor_rails_shim_i18n_load_path] = lp
      @@load_path = lp if Ractor.main?
      lp
    end
    # default_separator / exception_handler / missing_interpolation_argument_handler
    # / interpolation_patterns each read a @@ class variable unreadable from a
    # worker. Route them through IES; main mirrors the class var, workers use
    # the documented default (each default is worker-local and shareable-safe:
    # a String, a fresh ExceptionHandler, a fresh lambda, or the frozen
    # DEFAULT_INTERPOLATION_PATTERNS constant).
    def default_separator
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_sep]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_sep)
      if Ractor.main?
        cv = defined?(@@default_separator) ? @@default_separator : "."
        RactorRailsShim.storage[:ractor_rails_shim_i18n_sep] = cv
        cv
      else
        "."
      end
    end
    def default_separator=(separator)
      RactorRailsShim.storage[:ractor_rails_shim_i18n_sep] = separator
      @@default_separator = separator if Ractor.main?
      separator
    end
    def exception_handler
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_exc]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_exc)
      if Ractor.main?
        cv = defined?(@@exception_handler) ? @@exception_handler : ::I18n::ExceptionHandler.new
        RactorRailsShim.storage[:ractor_rails_shim_i18n_exc] = cv
        cv
      else
        ::I18n::ExceptionHandler.new
      end
    end
    def exception_handler=(handler)
      RactorRailsShim.storage[:ractor_rails_shim_i18n_exc] = handler
      @@exception_handler = handler if Ractor.main?
      handler
    end
    def missing_interpolation_argument_handler
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_miss]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_miss)
      if Ractor.main?
        cv = defined?(@@missing_interpolation_argument_handler) ? @@missing_interpolation_argument_handler : lambda do |missing_key, provided_hash, string|
            raise ::I18n::MissingInterpolationArgument.new(missing_key, provided_hash, string)
          end
        RactorRailsShim.storage[:ractor_rails_shim_i18n_miss] = cv
        cv
      else
        lambda do |missing_key, provided_hash, string|
            raise ::I18n::MissingInterpolationArgument.new(missing_key, provided_hash, string)
          end
      end
    end
    def missing_interpolation_argument_handler=(handler)
      RactorRailsShim.storage[:ractor_rails_shim_i18n_miss] = handler
      @@missing_interpolation_argument_handler = handler if Ractor.main?
      handler
    end
    def interpolation_patterns
      v = RactorRailsShim.storage[:ractor_rails_shim_i18n_ip]
      return v if RactorRailsShim.storage.key?(:ractor_rails_shim_i18n_ip)
      if Ractor.main?
        cv = defined?(@@interpolation_patterns) ? @@interpolation_patterns : ::I18n::DEFAULT_INTERPOLATION_PATTERNS.dup
        RactorRailsShim.storage[:ractor_rails_shim_i18n_ip] = cv
        cv
      else
        ::I18n::DEFAULT_INTERPOLATION_PATTERNS
      end
    end
    def interpolation_patterns=(patterns)
      RactorRailsShim.storage[:ractor_rails_shim_i18n_ip] = patterns
      @@interpolation_patterns = patterns if Ractor.main?
      patterns
    end
  RUBY

  # Capture the I18n backend class and shareable load paths in MAIN after
  # the app has initialized. Workers build their own backend instance of
  # the captured class and reload translations from the captured load paths
  # (see `I18n::Config#backend` / `#load_path`). Eager-load the backend
  # class so the constant is globally defined for worker Ractors.
  if Ractor.main? && defined?(::I18n)
    begin
      # Eager-load I18n classes/constants in main so they are globally
      # defined for worker Ractors (which cannot autoload).
      ::I18n::Backend::Simple rescue nil
      ::I18n::ExceptionHandler rescue nil
      ::I18n::MissingInterpolationArgument rescue nil
      ::I18n::DEFAULT_INTERPOLATION_PATTERNS rescue nil
      backend = ::I18n.backend
      backend.translate(:en, "") rescue nil
      backend.available_locales rescue nil
      const_set(:I18N_BACKEND_CLASS, backend.class) unless RactorRailsShim.const_defined?(:I18N_BACKEND_CLASS)
      raw_lp = (backend.respond_to?(:instance_variable_get) && backend.instance_variable_get(:@load_path)) ||
                (::I18n.respond_to?(:load_path) && ::I18n.load_path) || []
      shareable_lp = Ractor.make_shareable(Array(raw_lp).dup) rescue Array(raw_lp).map(&:to_s).freeze
      const_set(:I18N_LOAD_PATH, shareable_lp) unless RactorRailsShim.const_defined?(:I18N_LOAD_PATH)
    rescue StandardError
      nil
    end
  end

  # Patch I18n.fallbacks (a singleton method on the I18n module) to not
  # read the @@fallbacks class variable from a worker Ractor. It already
  # uses Fiber/Thread-local storage with @@fallbacks as the fallback;
  # route the @@fallbacks read through IES so workers build their own
  # I18n::Locale::Fallbacks. Called per-request via LookupContext details.
  if defined?(::I18n)
    i18n = ::I18n
    fb_key = :ractor_rails_shim_i18n_fallbacks
    fb_key_str = fb_key.inspect
    i18n.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def fallbacks
        v = RactorRailsShim.storage[#{fb_key_str}]
        return v if RactorRailsShim.storage.key?(#{fb_key_str})
        if Ractor.main? && defined?(@@fallbacks)
          cv = @@fallbacks
          if cv
            RactorRailsShim.storage[#{fb_key_str}] = cv
            return cv
          end
        end
        built = I18n::Locale::Fallbacks.new
        RactorRailsShim.storage[#{fb_key_str}] = built
        built
      end
    RUBY

    # I18n::Locale::Tag.implementation — manual @@implementation ||= Simple.
    # The value is a module (shareable). Route through IES.
    if defined?(::I18n::Locale::Tag)
      tag = ::I18n::Locale::Tag
      tag_key = :ractor_rails_shim_i18n_tag_implementation
      tag_key_str = tag_key.inspect
      tag.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
        def implementation
          v = RactorRailsShim.storage[#{tag_key_str}]
          return v if RactorRailsShim.storage.key?(#{tag_key_str})
          if Ractor.main? && defined?(@@implementation)
            cv = @@implementation
            RactorRailsShim.storage[#{tag_key_str}] = cv
            return cv
          end
          RactorRailsShim.storage[#{tag_key_str}] = I18n::Locale::Tag::Simple
          I18n::Locale::Tag::Simple
        end
      RUBY
    end

    # I18n::Base#normalize_key reads the @@normalized_key_cache class
    # variable (a double-nested Hash with default procs — unshareable, and
    # unreadable from a worker). It's a pure performance cache, so route it
    # through IsolatedExecutionState: each Ractor builds its own nested
    # cache via I18n.new_double_nested_cache and reads/writes it locally.
    if defined?(::I18n::Base)
      nk_key = :ractor_rails_shim_i18n_normalized_key_cache
      nk_key_str = nk_key.inspect
      ::I18n::Base.module_eval <<-RUBY, __FILE__, __LINE__ + 1
        def normalize_key(key, separator)
          cache = RactorRailsShim.storage[#{nk_key_str}]
          cache ||= (RactorRailsShim.storage[#{nk_key_str}] = ::I18n.new_double_nested_cache)
          cache[separator][key] ||=
            case key
            when Array
              key.flat_map { |k| normalize_key(k, separator) }
            else
              keys = key.to_s.split(separator)
              keys.delete('')
              keys.map! do |k|
                case k
                when /\A[-+]?([1-9]\d*|0)\z/ # integer
                  k.to_i
                when 'true'
                  true
                when 'false'
                  false
                else
                  k.to_sym
                end
              end
              keys
            end
        end
      RUBY
    end

    # I18n.reserved_keys_pattern memoizes its compiled regex in a lazy class
    # ivar (@reserved_keys_pattern) which a worker Ractor cannot write.
    # Route the cache through IsolatedExecutionState.
    if defined?(::I18n)
      rkp_key = :ractor_rails_shim_i18n_reserved_keys_pattern
      rkp_key_str = rkp_key.inspect
      ::I18n.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
        def reserved_keys_pattern
          v = RactorRailsShim.storage[#{rkp_key_str}]
          return v if v
          pat = /(?<!%)%\\{(#{::I18n::RESERVED_KEYS.join("|")})\\}/
          RactorRailsShim.storage[#{rkp_key_str}] = pat
          pat
        end
      RUBY
    end
  end
end

._install_inflector_patchObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 118

def _install_inflector_patch
  return if @inflector_patched
  @inflector_patched = true
  _register_patch :inflector, "8.1"
  return unless defined?(::ActiveSupport::Inflector::Inflections)
  inf = ::ActiveSupport::Inflector::Inflections
  en_key = :ractor_rails_shim_inflections_en
  inst_key = :ractor_rails_shim_inflections_instance
  en_key_str = en_key.inspect
  inst_key_str = inst_key.inspect
  inf.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def instance(locale = :en)
      if locale == :en
        v = RactorRailsShim.storage[#{en_key_str}]
        return v if RactorRailsShim.storage.key?(#{en_key_str})
        if Ractor.main?
          existing = instance_variable_get(:@__en_instance__) if instance_variable_defined?(:@__en_instance__)
          RactorRailsShim.storage[#{en_key_str}] = existing
          return existing || new.tap { |i| instance_variable_set(:@__en_instance__, i) }
        end
        fb = RactorRailsShim::SHAREABLE_FALLBACK[#{en_key_str}]
        return fb if fb
        built = new
        RactorRailsShim.storage[#{en_key_str}] = built
        built
      else
        h = RactorRailsShim.storage[#{inst_key_str}] ||= (Ractor.main? ? (instance_variable_defined?(:@__instance__) ? instance_variable_get(:@__instance__) : Concurrent::Map.new) : Concurrent::Map.new)
        h[locale] ||= new
      end
    end

    def instance_or_fallback(locale)
      return instance(locale) if locale == :en
      h = RactorRailsShim.storage[#{inst_key_str}]
      if h && h.key?(locale)
        return h[locale]
      end
      if Ractor.main? && instance_variable_defined?(:@__instance__)
        iv = instance_variable_get(:@__instance__)
        return iv[locale] if iv && iv.key?(locale)
      end
      instance(locale)
    end
  RUBY
  # Register so _build_shareable_fallback! captures the :en inflections
  # instance (made shareable) for workers.
  CLASS_ATTRIBUTES << ["ActiveSupport::Inflector::Inflections", :__en_instance__, en_key, nil]
  # Materialize the :en instance into IES in main so the fallback builder
  # can read + share it.
  inf.instance(:en) if Ractor.main?
end

._install_journey_routes_patchObject

Make Journey route recognition work under kino :ractor.

The shared app graph (frozen + made shareable by make_app_shareable!) already carries the routes' ast and the GTG simulator. The simulator, however, is normally UNshareable because TransitionTable seeds @memos with a default-Proc Hash (Hash.new { |h,k| h[k] = [] }), and because its @memos holds the per-route Route objects whose constraint Procs can't cross Ractor boundaries. The default Proc is the only thing we can fix; the Route constraint Procs are instead made shareable by make_app_shareable! when it freezes the whole graph (its proc-replacement pass rewrites them).

So the plan:

1. Patch TransitionTable to use a plain Hash + `add_memo` using `||= []`
 (behavior-identical, but shareable once Route memos are frozen).
2. Warm + cache `@ast` / `@simulator` on the live Routes object AFTER
 make_app_shareable!'s route precompute (which reloads/resets the
 routes) and BEFORE Ractor.make_shareable freezes the graph. Once
 frozen into the shared graph, worker Ractors read the cached ivars
 via the ORIGINAL Routes#ast/#simulator (no per-worker rebuild — a
 rebuild would have to read the frozen Route memos AND reuse several
 non-shareable class constants, which is fragile).

We deliberately do NOT override ast/simulator: the original methods read the cached ivars, which is exactly what workers need.



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 566

def _install_journey_routes_patch
  return if @journey_routes_patched
  @journey_routes_patched = true
  _register_patch :journey_routes, "8.1"
  return unless defined?(::ActionDispatch::Journey::Routes)

  # Patch TransitionTable to drop its default-Proc @memos. Must run before
  # the simulator is warmed (in _warm_journey_routes!, called from
  # make_app_shareable! after the route precompute).
  if defined?(::ActionDispatch::Journey::GTG::TransitionTable)
    tt = ::ActionDispatch::Journey::GTG::TransitionTable
    tt.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def initialize
        @stdparam_states = {}
        @regexp_states   = {}
        @string_states   = {}
        @accepting       = {}
        @memos           = {}
      end
      def add_memo(idx, memo)
        (@memos[idx] ||= []) << memo
      end
    RUBY
  end

  # Make Routes#ast / #simulator tolerant of a FROZEN receiver. The shim
  # warms + caches these ivars on the live (unfrozen) graph before freezing,
  # but if the cache is missing on the frozen shared object (e.g. routes
  # were re-drawn after warming, or warming was skipped), the original
  # `@simulator ||= build` raise FrozenError in a worker Ractor. When frozen,
  # build and RETURN the value without memoizing — the build is read-only
  # over the frozen route nodes, so it's safe and stays worker-local.
  routes = ::ActionDispatch::Journey::Routes
  routes.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def ast
      return @ast if defined?(@ast) && @ast
      built = ::ActionDispatch::Journey::Nodes::Or.new(anchored_routes.map(&:ast))
      frozen? ? built : (@ast ||= built)
    end

    def simulator
      return @simulator if defined?(@simulator) && @simulator
      gtg = ::ActionDispatch::Journey::GTG::Builder.new(ast).transition_table
      built = ::ActionDispatch::Journey::GTG::Simulator.new(gtg)
      frozen? ? built : (@simulator ||= built)
    end
  RUBY

  # Path::Pattern memoizes several ivars via `||=` (@re, @offsets,
  # @required_names, @optional_names, @requirements_for_missing_keys_check).
  # On a frozen shared graph the `||=` assignment raises FrozenError in a
  # worker Ractor. Make them frozen-tolerant: return the cached value if
  # present, otherwise build and RETURN it without memoizing (the build is
  # read-only over the frozen ast/requirements). This keeps workers correct
  # even if warming skipped a pattern.
  if defined?(::ActionDispatch::Journey::Path::Pattern)
    ::ActionDispatch::Journey::Path::Pattern.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def required_names
        return @required_names if defined?(@required_names) && @required_names
        built = names - optional_names
        frozen? ? built : (@required_names ||= built)
      end

      def optional_names
        return @optional_names if defined?(@optional_names) && @optional_names
        built = spec.find_all(&:group?).flat_map { |g| g.find_all(&:symbol?) }.map(&:name).uniq
        frozen? ? built : (@optional_names ||= built)
      end

      def to_regexp
        return @re if defined?(@re) && @re
        built = regexp_visitor.new(@separators, @requirements).accept(spec)
        frozen? ? built : (@re ||= built)
      end

      def requirements_for_missing_keys_check
        return @requirements_for_missing_keys_check if defined?(@requirements_for_missing_keys_check) && @requirements_for_missing_keys_check
        built = requirements.transform_values { |regex| /\A#\{regex\}\Z/ }
        frozen? ? built : (@requirements_for_missing_keys_check ||= built)
      end

      def offsets
        return @offsets if defined?(@offsets) && @offsets
        built = begin
          offs = [0]
          spec.find_all(&:symbol?).each do |node|
            node = node.to_sym
            if @requirements.key?(node)
              re = /#\{Regexp.union(@requirements[node])\}|/
              offs.push((re.match("").length - 1) + offs.last)
            else
              offs << offs.last
            end
          end
          offs
        end
        frozen? ? built : (@offsets ||= built)
      end
    RUBY
  end
end

._install_json_encoding_patchObject

Patch ActiveSupport::JSON::Encoding. The module memoizes two encoders in class ivars (@encoder_without_options / @encoder_without_escape) inside json_encoder=, and exposes json_encoder as a attr_reader (so it too reads the @json_encoder class ivar). A worker Ractor cannot read any of these module ivars, raising Ractor::IsolationError ("can not get unshareable values from instance variables of classes/modules from non-main Ractors"). Capture the encoder CLASS in main (on assignment) into a shareable constant, then build a per-Ractor encoder instance via IsolatedExecutionState instead of reading the module ivars.



496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 496

def _install_json_encoding_patch
  return if @json_encoding_patched
  @json_encoding_patched = true
  _register_patch :json_encoding, "8.1"
  return unless defined?(::ActiveSupport::JSON::Encoding)
  enc = ::ActiveSupport::JSON::Encoding
  ec_key = :ractor_rails_shim_json_encoder
  ec_key_str = ec_key.inspect
  ecn_key = :ractor_rails_shim_json_encoder_no_escape
  ecn_key_str = ecn_key.inspect
  enc.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def json_encoder=(encoder)
      RactorRailsShim.const_set(:JSON_ENCODER_CLASS, encoder) if Ractor.main? && defined?(RactorRailsShim)
      @json_encoder = encoder if Ractor.main?
      encoder
    end
    def json_encoder
      RactorRailsShim::JSON_ENCODER_CLASS
    end
    def encode_without_options(value)
      encoder = RactorRailsShim.storage[#{ec_key_str}]
      encoder ||= (RactorRailsShim.storage[#{ec_key_str}] = RactorRailsShim::JSON_ENCODER_CLASS.new)
      encoder.encode(value)
    end
    def encode_without_escape(value)
      encoder = RactorRailsShim.storage[#{ecn_key_str}]
      encoder ||= (RactorRailsShim.storage[#{ecn_key_str}] = RactorRailsShim::JSON_ENCODER_CLASS.new(escape: false))
      encoder.encode(value)
    end
  RUBY
  # The JSON encoding constants (HTML_ENTITIES_REGEX etc.) live on
  # ActiveSupport::JSON::Encoding but are NOT Ractor-shareable in Ruby 4.0
  # (Regexp.union / frozen-string Hash return false for Ractor.shareable?).
  # Deep-freeze + replace them here (in main, during prepare_for_ractors!)
  # so worker Ractors can read them when the encoder escapes HTML. Belt and
  # suspenders alongside the SHAREABLE_CONSTANTS registration.
  if Ractor.main?
    %w[ESCAPED_CHARS HTML_ENTITIES_REGEX FULL_ESCAPE_REGEX JS_SEPARATORS_REGEX].each do |name|
      next unless enc.const_defined?(name, false)
      v = enc.const_get(name, false)
      unless Ractor.shareable?(v)
        begin
          enc.send(:remove_const, name) if enc.const_defined?(name, false)
          enc.const_set(name, Ractor.make_shareable(v))
        rescue StandardError
          nil
        end
      end
    end
  end
  # Make sure the constant exists on RactorRailsShim so worker references
  # resolve. It is set on the first `json_encoder=` call during init; seed a
  # default here so even a direct call before init is safe.
  unless RactorRailsShim.const_defined?(:JSON_ENCODER_CLASS)
    RactorRailsShim.const_set(:JSON_ENCODER_CLASS, ::ActiveSupport::JSON::Encoding::JSONGemEncoder)
  end
end

._install_json_renderer_patchObject

render json:, render xml:, and render js: are dispatched to _render_with_renderer_json / _render_with_renderer_xml / _render_with_renderer_js — methods Rails defines via ActionController::Renderers.add(key, &block)define_method(_render_with_renderer_method_name(key), &block) at boot (action_controller/metal/renderers.rb). The block is compiled in the MAIN Ractor, so calling it from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor" (the same failure the StatsController works around with stdlib JSON.generate). Redefine each default renderer as a string-eval'd def (no captured binding) so it is callable from any Ractor. The bodies replicate Rails' defaults exactly (to_json/to_xml + content_type + response_body), referencing only the receiver (self) and shareable constants (Mime, Mime, Mime) — no captured locals.



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 463

def _install_json_renderer_patch
  return if @json_renderer_patched
  @json_renderer_patched = true
  _register_patch :json_renderer, "8.1"
  return unless defined?(::ActionController::Renderers)
  ::ActionController::Renderers.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def _render_with_renderer_json(json, options)
      json = json.to_json(options) unless json.kind_of?(String)
      json = "\#{options[:callback]}(\#{json})" if options[:callback]
      self.content_type = Mime[:json]
      self.response_body = json
    end

    def _render_with_renderer_xml(xml, options)
      xml = xml.to_xml(options) unless xml.kind_of?(String)
      self.content_type = Mime[:xml]
      self.response_body = xml
    end

    def _render_with_renderer_js(js, options)
      self.content_type = Mime[:js]
      self.response_body = js.respond_to?(:to_js) ? js.to_js : js
    end
  RUBY
end

._install_kaminari_config_patchObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ractor_rails_shim/patches/kaminari.rb', line 21

def _install_kaminari_config_patch
  return if @kaminari_patched
  @kaminari_patched = true
  _register_patch :kaminari_config, "8.1"
  return unless defined?(::Kaminari)

  # In the main ractor, capture the config object and make it shareable.
  shareable_config = nil
  if Ractor.main?
    begin
      cfg = ::Kaminari.instance_variable_get(:@_config) rescue nil
      if cfg
        begin
          Ractor.make_shareable(cfg)
          shareable_config = cfg
        rescue StandardError => e
          # If the config can't be made shareable (unlikely — it's all
          # integers/symbols/nil), build a fresh one with defaults.
          shareable_config = Ractor.make_shareable(::Kaminari::Config.new)
        end
      else
        shareable_config = Ractor.make_shareable(::Kaminari::Config.new)
      end
    rescue StandardError => e
      # Best-effort
    end
  end
  shareable_config ||= Ractor.make_shareable(::Kaminari::Config.new) rescue nil

  # Store the shareable config as a constant so workers can read it.
  if shareable_config
    _reassign_shareable_const(:KAMINARI_SHAREABLE_CONFIG, shareable_config)
  end

  k_key = :ractor_rails_shim_kaminari_config
  k_key_str = k_key.inspect
  ::Kaminari.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def config
      v = RactorRailsShim.storage[#{k_key_str}]
      return v if RactorRailsShim.storage.key?(#{k_key_str})
      if Ractor.main? && instance_variable_defined?(:@_config)
        @_config
      else
        RactorRailsShim::KAMINARI_SHAREABLE_CONFIG
      end
    end

    def config=(val)
      RactorRailsShim.storage[#{k_key_str}] = val
    end
  RUBY

  # Register so the shareable fallback builder knows about it.
  CLASS_ATTRIBUTES << ["Kaminari", :config, k_key, nil] if shareable_config

  # Patch Kaminari's `page` class method to avoid the `extending` block
  # (a Proc compiled in the main Ractor). Kaminari defines `page` via eval
  # with `.extending { include Kaminari::ActiveRecordRelationMethods;
  # include Kaminari::PageScopeMethods }` — a block that captures the main
  # Ractor's binding. Calling it from a worker raises "defined with an
  # un-shareable Proc in a different Ractor". Fix: redefine `page` to pass
  # the modules as arguments to `extending` (shareable constants, no block).
  # The delegate classes already include these modules from main's
  # _share_model_classes!, so the extending is redundant in workers but
  # harmless (it just re-adds already-included modules).
  _install_kaminari_page_method_patch
end

._install_kaminari_page_method_patchObject



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ractor_rails_shim/patches/kaminari.rb', line 89

def _install_kaminari_page_method_patch
  return if @kaminari_page_patched
  @kaminari_page_patched = true
  _register_patch :kaminari_page_method, "8.1"
  return unless defined?(::Kaminari)
  return unless defined?(::ActiveRecord::Base)

  page_method_name = (::Kaminari.config.page_method_name rescue :page).to_s
  return if page_method_name.empty?

  # Override on ActiveRecord::Base singleton class (Kaminari defines it
  # here via eval in the included block of ActiveRecordModelExtension).
  ::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{page_method_name}(num = nil)
      per_page = max_per_page && (default_per_page > max_per_page) ? max_per_page : default_per_page
      limit(per_page).offset(per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending(
        ::Kaminari::ActiveRecordRelationMethods,
        ::Kaminari::PageScopeMethods
      )
    end
  RUBY
end

._install_local_cache_patchObject

Patch ActiveSupport::Cache::Strategy::LocalCache#local_cache_key. The original memoizes the key in a @local_cache_key ivar on the store:

`@local_cache_key ||= "...".to_sym`

When the store is part of the frozen, shared Rails.application graph (deep-frozen by make_app_shareable! for kino :ractor mode), a worker Ractor writing that ivar raises FrozenError. The key is a pure function of the store's class + object_id (both stable for the shared object), so compute it deterministically each call — no ivar write. The key still addresses LocalCacheRegistry, which is already Ractor-safe (it uses IsolatedExecutionState), so each Ractor keeps its own local cache.



383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 383

def _install_local_cache_patch
  return if @local_cache_patched
  @local_cache_patched = true
  _register_patch :local_cache, "8.1"
  return unless defined?(::ActiveSupport::Cache::Strategy::LocalCache)
  lc = ::ActiveSupport::Cache::Strategy::LocalCache
  lc.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def local_cache_key
      str = "\#{self.class.name.underscore}_local_cache_\#{object_id}".gsub(/[\\/-]/, "_")
      str.to_sym
    end
  RUBY
end

._install_log_subscriber_patchObject

Patch ActiveSupport::LogSubscriber.logger — a raw class ivar with lazy init (@logger ||= Rails.logger) that's WRITTEN at request teardown via flush_all!. Workers can't write class ivars → IsolationError. Route through IES; workers get Rails.logger (which the shim already routes through IES) so it resolves to the worker's own per-Ractor logger.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 315

def _install_log_subscriber_patch
  return if @log_subscriber_patched
  @log_subscriber_patched = true
  _register_patch :log_subscriber, "8.1"
  return unless defined?(::ActiveSupport::LogSubscriber)
  ls = ::ActiveSupport::LogSubscriber
  key = :ractor_rails_shim_log_subscriber_logger
  key_str = key.inspect
  ls.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def logger
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      if Ractor.main? && instance_variable_defined?(:@logger)
        @logger
      elsif defined?(::Rails) && ::Rails.respond_to?(:logger)
        ::Rails.logger
      end
    end

    def logger=(val)
      RactorRailsShim.storage[#{key_str}] = val
    end
  RUBY
end

._install_loofah_patchObject

Loofah (pulled in by rails-html-sanitizer for sanitize / simple_format) memoizes @document_klass on its shared DocumentFragment classes. A worker Ractor cannot set an ivar on a class defined in the main Ractor, so fragment parsing raises "can not set instance variables of classes/modules by non-main Ractors". Route the memoization through Ractor.current (keyed by the fragment class) instead.



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 717

def _install_loofah_patch
  return if @loofah_patched
  @loofah_patched = true
  _register_patch :loofah_document_klass, "8.1"
  return unless defined?(::Loofah::HtmlFragmentBehavior::ClassMethods)

  mod = ::Loofah::HtmlFragmentBehavior::ClassMethods
  mod.module_eval do
    def document_klass
      store = (Ractor.current[:__rrs_loofah_doc_klass__] ||= {})
      store[self.object_id] ||=
        if Loofah.html5_support? && self == Loofah::HTML5::DocumentFragment
          Loofah::HTML5::Document
        elsif self == Loofah::HTML4::DocumentFragment
          Loofah::HTML4::Document
        else
          raise ArgumentError, "unexpected class: #{self}"
        end
    end
  end
end

._install_lookup_context_patchObject



67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 67

def _install_lookup_context_patch
  return if @lookup_context_patched
  @lookup_context_patched = true
  _register_patch :lookup_context, "8.1"
  return unless defined?(::ActionView::LookupContext)
  # Force autoload of the nested ActionView::Template constants that the
  # patched details_cache_key references (Template::Types,
  # TemplateDetails::Requested). Constants are global, so defining them
  # here (main ractor) makes them visible to worker ractors, which cannot
  # autoload. Without this, the first template render in a worker dies on
  # `NameError: uninitialized constant ActionView::Template::TemplateDetails`.
  if Ractor.main? && defined?(::ActionView::Template)
    ::ActionView::Template::Types rescue nil
    ::ActionView::TemplateDetails rescue nil
  end
  lc = ::ActionView::LookupContext
  key = :ractor_rails_shim_lookup_context_registered_details
  key_str = key.inspect
  lc.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def registered_details
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      if Ractor.main? && instance_variable_defined?(:@registered_details)
        @registered_details
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}] || []
      end
    end
    def registered_details=(val)
      RactorRailsShim.storage[#{key_str}] = val
      @registered_details = val if Ractor.main?
      val
    end
  RUBY
  CLASS_ATTRIBUTES << ["ActionView::LookupContext", :registered_details, key, []]

  # Redefine default_#{name} methods. register_detail (line 25 of
  # lookup_context.rb) defines these via Accessors.define_method(:"default_#{name}", &block)
  # — a Proc from the main ractor. Calling them from a worker raises
  # "defined with an un-shareable Proc in a different Ractor".
  # Trigger: when Accept: */* is sent, request.formats returns [Mime::ALL],
  # and LookupContext#formats= (the override at line 263) does
  # `values.concat(default_formats) if values.delete "*/*"` — Mime::ALL
  # compares == to "*/*", so delete removes it and default_formats is called.
  # Fix: call each block once in main, make the result shareable, and
  # redefine the method via string eval (no captured binding).
  accessors = ::ActionView::LookupContext::Accessors
  ::ActionView::LookupContext.registered_details.each do |name|
    block = accessors::DEFAULT_PROCS[name]
    next unless block
    begin
      value = block.call
    rescue StandardError
      next
    end
    begin
      value = Ractor.make_shareable(value)
    rescue StandardError
      value = value.dup.freeze rescue value
    end
    const_name = "SHIM_DEFAULT_#{name.upcase}_VALUE"
    verbose, $VERBOSE = $VERBOSE, nil
    accessors.const_set(const_name, value)
    $VERBOSE = verbose
    accessors.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def default_#{name}
        ::ActionView::LookupContext::Accessors::#{const_name}
      end
    RUBY
  end

  # Patch ActionView::Rendering::ClassMethods#view_context_class to read
  # from a shareable registry (populated in main) instead of building via
  # Class.new{...} blocks (un-shareable Proc from a worker). The built
  # class is made shareable; workers read it via the registry.
  if defined?(::ActionView::Rendering::ClassMethods)
    rcm = ::ActionView::Rendering::ClassMethods

    # `ActionView::Base.with_empty_template_cache` is patched separately and
    # EARLY (see _install_with_empty_template_cache_patch, installed via
    # ActiveSupport.on_load(:action_view) before eager load) because the
    # framework's original uses `define_method(:compiled_method_container)
    # { subclass }` — a block/Proc captured in the main Ractor that raises
    # "defined with an un-shareable Proc in a different Ractor" when a
    # worker calls it. In production `DetailsKey.view_context_class` calls
    # with_empty_template_cache during eager load, so the patch MUST be in
    # place before then.
    _install_with_empty_template_cache_patch if defined?(::ActionView::Base)

    # Build the per-controller view_context_class registry in main. We call
    # the ORIGINAL build_view_context_class directly (bypassing Rails'
    # inherit_view_context_class? short-circuit, which otherwise makes
    # subclasses reuse ActionController::Base's class built with a nil
    # `_routes` and thus NO route url_helpers). `routes` is forced to the
    # shareable Rails.application.routes when the controller's own `_routes`
    # is nil, so named helpers (new_post_path, etc.) are always present.
    if Ractor.main?
      registry = {}
      ::AbstractController::Base.descendants.each do |ctrl|
        begin
          routes = ctrl.respond_to?(:_routes) ? ctrl._routes : nil
          routes = ::Rails.application.routes if routes.nil? && ::Rails.respond_to?(:application) && ::Rails.application
          vcc = rcm.instance_method(:build_view_context_class).bind(ctrl).call(
            ::ActionView::LookupContext::DetailsKey.view_context_class,
            ctrl.respond_to?(:supports_path?) ? ctrl.supports_path? : true,
            routes,
            ctrl.respond_to?(:_helpers) ? ctrl._helpers : nil
          )
          registry[ctrl] = vcc if vcc
        rescue StandardError => e
          # skip controllers that can't build (e.g. abstract)
        end
      end
      registry.delete_if { |_, v| v.nil? }
      registry.freeze
      begin
        Ractor.make_shareable(registry)
        self._view_context_registry = registry
      rescue StandardError => e
        # If the registry can't be made shareable, leave it — workers fall
        # back to the empty-cache base.
      end
    end

    rcm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def view_context_class
        return @view_context_class if Ractor.main? && instance_variable_defined?(:@view_context_class) && @view_context_class

        if Ractor.main?
          @view_context_class ||= build_view_context_class(
            ActionView::LookupContext::DetailsKey.view_context_class,
            supports_path?,
            _routes,
            _helpers
          )
          return @view_context_class
        end

        # Worker Ractor. The frozen shared controller class carries a
        # memoized @view_context_class built in main (in the registry) with
        # its proper route url_helpers + controller helpers. Look it up by
        # controller class; fall back to the shareable fallback class (built
        # in main with Rails.application.routes) for any controller not
        # present in the registry at prepare time.
        vcc = RactorRailsShim._view_context_registry[self]
        return vcc if vcc
        RactorRailsShim._view_context_fallback
      end

      # `inherit_view_context_class?` (action_view/rendering.rb:52) compares
      # `superclass._helpers` — and `_helpers` is defined via a block
      # (redefine_singleton_method) that cannot run in a worker Ractor
      # ("defined with an un-shareable Proc in a different Ractor"). Return
      # false so each controller builds its own view_context_class (the
      # normal Rails behaviour whenever _routes/_helpers differ), avoiding
      # the `_helpers` comparison entirely. Behaviour is identical in the
      # main Ractor (the inherited class would be equivalent).
      def inherit_view_context_class?
        false
      end
    RUBY
  end
  vcc_key = :ractor_rails_shim_lookup_context_view_context_class
  vcc_key_str = vcc_key.inspect
  CLASS_ATTRIBUTES << ["ActionView::LookupContext::DetailsKey", :view_context_class, vcc_key, nil]
  # Build it now in main and stash in IES so the fallback builder picks it up.
  if Ractor.main? && defined?(::ActionView::Base)
    RactorRailsShim.storage[vcc_key] = ::ActionView::LookupContext::DetailsKey.view_context_class
    # Shareable fallback view_context_class for any controller not present in
    # the registry at prepare time. Subclasses ActionView::Base, so it
    # inherits the per-class compiled_method_container (self.class) and the
    # route url_helpers. build_view_context_class is a ClassMethods method on
    # controllers, so invoke it via the unbound method bound to
    # ActionController::Base.
    fallback = rcm.instance_method(:build_view_context_class).bind(::ActionController::Base).call(
      ::ActionView::LookupContext::DetailsKey.view_context_class,
      true,
      (defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application) ? ::Rails.application.routes : nil,
      nil
    )
    fallback = _swallow("make view context fallback shareable") { Ractor.make_shareable(fallback) }
    self._view_context_fallback = fallback
  end

  dk = ::ActionView::LookupContext::DetailsKey
  dk_key = :ractor_rails_shim_lookup_context_details_keys
  dc_key = :ractor_rails_shim_lookup_context_digest_cache
  dk_key_str = dk_key.inspect
  dc_key_str = dc_key.inspect
  vcc_key_str2 = vcc_key.inspect
  dk.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def view_context_class
      v = RactorRailsShim.storage[#{vcc_key_str2}]
      return v if RactorRailsShim.storage.key?(#{vcc_key_str2})
      if Ractor.main? && instance_variable_defined?(:@view_context_class)
        v = @view_context_class
        RactorRailsShim.storage[#{vcc_key_str2}] = v
        v
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{vcc_key_str2}]
      end
    end
    def details_keys
      v = RactorRailsShim.storage[#{dk_key_str}]
      return v if v
      if Ractor.main? && instance_variable_defined?(:@details_keys)
        v = @details_keys
      else
        v = Concurrent::Map.new
      end
      RactorRailsShim.storage[#{dk_key_str}] = v
      v
    end
    def digest_cache(details)
      dc = (RactorRailsShim.storage[#{dc_key_str}] ||= Concurrent::Map.new)
      dc[details_cache_key(details)] ||= Concurrent::Map.new
    end
    def details_cache_key(details)
      details_keys.fetch(details) do
        if formats = details[:formats]
          unless ::ActionView::Template::Types.valid_symbols?(formats)
            details = details.dup
            details[:formats] &= ::ActionView::Template::Types.symbols
          end
        end
        details_keys[details] ||= ::ActionView::TemplateDetails::Requested.new(**details)
      end
    end
  RUBY
end

._install_mail_patchObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/ractor_rails_shim/patches/mail.rb', line 21

def _install_mail_patch
  return if @mail_patched
  @mail_patched = true
  _register_patch :mail, "8.1"
  return unless defined?(::Mail)

  # --- Mail::Message.default_charset (read in Mail::Message#initialize) ---
  if defined?(::Mail::Message)
    ::Mail::Message.singleton_class.class_eval do
      def default_charset
        if ::Ractor.main?
          @@default_charset
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_default_charset] || "UTF-8"
        end
      end

      def default_charset=(charset)
        if ::Ractor.main?
          @@default_charset = charset
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_default_charset] = charset
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_default_charset] = charset
        end
      end
    end
  end

  # --- Mail::Header.maximum_amount (read while parsing/building headers) ---
  if defined?(::Mail::Header)
    ::Mail::Header.singleton_class.class_eval do
      def maximum_amount
        if ::Ractor.main?
          @@maximum_amount
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_max_amount] || 1000
        end
      end

      def maximum_amount=(value)
        if ::Ractor.main?
          @@maximum_amount = value
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_max_amount] = value
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_max_amount] = value
        end
      end
    end
  end

  # --- ActionMailer::Base.mailer_name (raw class ivar on the mailer subclass) ---
  # `mailer_name` (aliased to `controller_path`) computes the mailer's view
  # path prefix from its class name, but reads the lazy `@mailer_name` class
  # ivar — unreadable from a worker Ractor. The value defaults to
  # `name.underscore`, which is derivable from the (shareable) class name, so
  # just compute it in workers.
  if defined?(::ActionMailer::Base)
    _install_actionmailer_mailer_name_patch
  elsif defined?(::ActiveSupport.on_load)
    # ActionMailer not loaded yet (e.g. shim `install` runs before
    # `require "rails/all"`). Register the patch to fire the moment
    # ActionMailer::Base is loaded — BEFORE the app eager-loads its mailers,
    # so the `local_prefixes` -> `[controller_path]` -> `mailer_name` call
    # sites compile against the patched method (a late `prepare_for_ractors!`
    # patch binds too late: those call sites have already captured the
    # original `@mailer_name`-reading method).
    ::ActiveSupport.on_load(:action_mailer) do
      RactorRailsShim.send(:_install_actionmailer_mailer_name_patch)
    end
  end

  # --- Freeze all un-shareable `mail` gem constants so workers can read them ---
  # The `mail` gem defines many Regexp / mutable-Array / mutable-Hash module
  # constants (e.g. Mail::Utilities::TO_CRLF_REGEX, Mail::SMTP::DEFAULTS).
  # Reading an un-shareable constant from a non-main Ractor raises
  # IsolationError, so make every constant value under `Mail` shareable (in
  # place — freezing Regexps/Arrays/Hashes). Done once at install; constants
  # are fixed at boot so freezing after boot is safe.
  if defined?(::Mail)
    _make_mail_constants_shareable!
  end

  # --- Mail::Encodings registry (a module ivar, read during message build) ---
  # `Mail::Encodings.@transfer_encodings` is a module-level ivar holding the
  # registered transfer encodings (base64, quoted_printable, ...). Reading it
  # from a worker Ractor raises IsolationError. The registry is fixed at boot,
  # so capture a frozen shareable copy and have workers read it.
  if defined?(::Mail::Encodings)
    enc_reg = nil
    begin
      enc_reg = ::Mail::Encodings.instance_variable_get(:@transfer_encodings)
    rescue StandardError
      enc_reg = nil
    end
    if enc_reg
      RactorRailsShim.const_set(
        :SHAREABLE_MAIL_TRANSFER_ENCODINGS,
        Ractor.make_shareable(enc_reg.dup)
      )
    end
    ::Mail::Encodings.singleton_class.class_eval do
      def get_encoding(name)
        reg = ::Ractor.main? ? @transfer_encodings : ::RactorRailsShim::SHAREABLE_MAIL_TRANSFER_ENCODINGS
        reg[get_name(name)]
      end

      def get_all
        reg = ::Ractor.main? ? @transfer_encodings : ::RactorRailsShim::SHAREABLE_MAIL_TRANSFER_ENCODINGS
        reg.values
      end

      def defined?(name)
        reg = ::Ractor.main? ? @transfer_encodings : ::RactorRailsShim::SHAREABLE_MAIL_TRANSFER_ENCODINGS
        reg.include? get_name(name)
      end
    end
  end

  # --- Mail.delivery_method (resolves Mail::Configuration, a Singleton) ---
  # `Mail.delivery_method` calls `Mail::Configuration.instance` — a Singleton
  # whose class ivar (`@singleton__mutex__`) is un-shareable, so reading it
  # from a worker Ractor raises IsolationError. Capture the RESOLVED
  # delivery-method CLASS at install (main) into a shareable constant; workers
  # build a fresh, worker-local instance of it instead of touching the
  # Singleton. The `:test` delivery instance only appends to
  # `TestMailer.deliveries` (class-level, patched above), so a fresh instance
  # per call is correct.
  dm_class = nil
  begin
    resolved = ::Mail::Configuration.instance.delivery_method
    dm_class = resolved.class if resolved
  rescue StandardError
    dm_class = nil
  end
  if dm_class
    RactorRailsShim.const_set(
      :SHAREABLE_MAIL_DELIVERY_METHOD_CLASS,
      Ractor.make_shareable(dm_class)
    )
  end
  ::Mail.singleton_class.class_eval do
    alias_method :_rrs_orig_delivery_method, :delivery_method
    def delivery_method(method = nil, settings = {})
      if ::Ractor.main?
        _rrs_orig_delivery_method(method, settings)
      else
        klass = ::RactorRailsShim::SHAREABLE_MAIL_DELIVERY_METHOD_CLASS
        return klass.new(settings) if klass
        _rrs_orig_delivery_method(method, settings)
      end
    end
  end

  # --- Mail::Configuration.instance (a Singleton) ---
  # `Mail::Message#delivery_method` calls `Mail::Configuration.instance`
  # directly. The Singleton's `instance` reads the un-shareable class ivar
  # `@singleton__instance__`, which raises IsolationError in a worker Ractor.
  # Build a fresh, worker-local Configuration instance instead (the Singleton
  # makes `new` private, so use `allocate` + `initialize`). A fresh instance
  # is correct: delivery settings are derived per-call from the method name.
  if defined?(::Mail::Configuration)
    unless ::Mail::Configuration.singleton_class.private_instance_methods
            .include?(:_rrs_orig_configuration_instance)
      ::Mail::Configuration.singleton_class.class_eval do
        alias_method :_rrs_orig_configuration_instance, :instance
      end
    end
    ::Mail::Configuration.singleton_class.class_eval do
      def instance
        if ::Ractor.main?
          _rrs_orig_configuration_instance
        else
          cfg = ::Mail::Configuration.send(:allocate)
          cfg.send(:initialize)
          cfg
        end
      end
    end
  end

  # --- Mail delivery observers / interceptors ---
  # `inform_interceptors` / `inform_observers` read `@@delivery_interceptors`
  # / `@@delivery_notification_observers` INLINE (not via the reader), so the
  # readers alone don't help. Patch the call sites to read the per-Ractor IES
  # slot. Workers have no registered observers/interceptors, so an empty array
  # is correct.
  ::Mail.singleton_class.class_eval do
    def inform_interceptors(mail_obj)
      interceptors = if ::Ractor.main?
        @@delivery_interceptors
      else
        ::RactorRailsShim.storage[:ractor_rails_shim_mail_interceptors] ||= []
      end
      interceptors.each { |i| i.delivering_email(mail_obj) }
    end

    def inform_observers(mail_obj)
      observers = if ::Ractor.main?
        @@delivery_notification_observers
      else
        ::RactorRailsShim.storage[:ractor_rails_shim_mail_observers] ||= []
      end
      observers.each { |o| o.delivered_email(mail_obj) }
    end

    # `uniq` does `@@uniq += 1` (a cvar WRITE) — also illegal in workers.
    # Use a per-Ractor IES counter instead.
    def uniq
      if ::Ractor.main?
        @@uniq += 1
      else
        n = (::RactorRailsShim.storage[:ractor_rails_shim_mail_uniq] || 0) + 1
        ::RactorRailsShim.storage[:ractor_rails_shim_mail_uniq] = n
        n
      end
    end
  end

  # --- Mail::TestMailer.deliveries (ActionMailer's :test delivery appends here) ---
  if defined?(::Mail::TestMailer)
    ::Mail::TestMailer.singleton_class.class_eval do
      def deliveries
        if ::Ractor.main?
          @@deliveries ||= []
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_test_deliveries] ||= []
        end
      end

      def deliveries=(val)
        if ::Ractor.main?
          @@deliveries = val
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_test_deliveries] = val
        else
          ::RactorRailsShim.storage[:ractor_rails_shim_mail_test_deliveries] = val
        end
      end
    end
  end

  # --- Mail::Parsers::*Parser class ivars (Citrus/Ragel parse tables) ---
  # The `mail` gem's generated parsers (MimeVersionParser, AddressParser, …)
  # are MODULES whose `class << self` block stores parse tables as singleton
  # ivars (`@_index_offsets`, `@_trans_keys`, `@_key_spans`, …). These are
  # populated at file-load time and are deterministic/shareable once frozen,
  # but reading a class/module ivar from a non-main Ractor raises
  # IsolationError. Capture each value once (in the main Ractor) and redefine
  # the attr reader to return the captured, shareable copy — so worker calls
  # to `_index_offsets` / `_trans_keys` / … return the same table.
  # --- Module-level ivars on `mail` gem modules (Citrus/Ragel parse tables,
  # `Mail::Utilities#charset_encoder`, …) ---
  # These modules store state as class/module ivars (`@_index_offsets`,
  # `@_trans_keys`, `@charset_encoder`, …) populated at file-load time.
  # Reading a module ivar from a non-main Ractor raises IsolationError, so
  # capture each value once (in the main Ractor) into a shareable constant
  # and redefine the attr reader to return the captured copy (via
  # string-eval `def` — NOT `define_method`, whose block would be an
  # un-shareable Proc). This covers `Mail::Parsers::*Parser` and
  # `Mail::Utilities` (which uses `attr_accessor :charset_encoder`).
  target_modules = []
  if defined?(::Mail::Parsers)
    ::Mail::Parsers.constants(false).each do |pname|
      pmod = ::Mail::Parsers.const_get(pname) rescue nil
      target_modules << pmod if pmod.is_a?(Module)
    end
  end
  target_modules << ::Mail::Utilities if defined?(::Mail::Utilities)
  target_modules.each do |pmod|
    ivs = pmod.instance_variables
    next if ivs.empty?
    captured = {}
    ivs.each do |iv|
      val = pmod.instance_variable_get(iv) rescue nil
      captured[iv] = ::Ractor.make_shareable(val) rescue val
    end
    captured.freeze
    const_name = :RRS_MODULE_IVARS
    unless pmod.singleton_class.const_defined?(const_name)
      pmod.singleton_class.const_set(const_name, captured)
    end
    pmod.singleton_class.class_eval do
      captured.each do |iv, val|
        reader = iv.to_s.sub(/\A@/, "").to_sym
        if method_defined?(reader, true) && !val.nil?
          class_eval(<<~RUBY)
            def #{reader}
              ::#{pmod}.singleton_class::#{const_name}[#{iv.inspect}]
            end
          RUBY
        end
      end
    end
  end
  # --- Mail::PartsList / Mail::AttachmentsList (DelegateClass(Array)) ---
  # `DelegateClass(Array)` generates each delegating method (`[]`, `<<`, `each`,
  # …) via a block compiled in the main Ractor, so the methods are un-shareable
  # Procs that raise "defined with an un-shareable Proc in a different Ractor"
  # when called from a worker Ractor (hit during message building:
  # `Mail::Body#<<` -> `Mail::PartsList.new[val]`). Redefine the delegating
  # methods as string-eval `def`s (shareable) that forward to `__getobj__` —
  # identical behavior to DelegateClass, but callable from any Ractor. We skip
  # methods the class defines itself (e.g. `PartsList#collect`) so we don't
  # clobber their (already shareable) custom implementations.
  if defined?(::Mail::PartsList) || defined?(::Mail::AttachmentsList)
    [::Mail::PartsList, ::Mail::AttachmentsList].each do |klass|
      next unless klass.is_a?(Class)
      # `DelegateClass` also compiles `__getobj__` / `__setobj__` (and the
      # inherited `initialize`, which calls `__setobj__`) as Procs in the main
      # Ractor, so redefine those shareably too. The backing ivar is
      # `@delegate_dc_obj` (verified against Ruby's DelegateClass).
      klass.class_eval(<<~RUBY)
        def __getobj__
          @delegate_dc_obj
        end

        def __setobj__(obj)
          @delegate_dc_obj = obj
        end

        # Shadow the DelegateClass-generated `initialize` (a Proc compiled in
        # the main Ractor). Reproduce PartsList's own init: build the backing
        # Array and point the delegation ivar at it directly, so `super` never
        # reaches the un-shareable DelegateClass constructor.
        def initialize(*args)
          @parts = Array.new(*args)
          @delegate_dc_obj = @parts
        end
      RUBY
      ::Array.instance_methods(false).each do |m|
        next if %i[object_id __send__ __id__ equal?].include?(m)
        im = klass.instance_method(m) rescue nil
        next if im && im.owner == klass
        klass.class_eval(<<~RUBY)
          def #{m}(*args, &block)
            __getobj__.send(:#{m}, *args, &block)
          end
        RUBY
      end
      klass.class_eval(<<~RUBY)
        def method_missing(name, *args, &block)
          __getobj__.send(name, *args, &block)
        end

        def respond_to_missing?(name, include_private = false)
          __getobj__.respond_to?(name, include_private) || super
        end
      RUBY
    end
  end

end

._install_marcel_patchObject



190
# File 'lib/ractor_rails_shim/patches/marcel.rb', line 190

def self._install_marcel_patch = MarcelPatch.apply!

._install_messages_serializer_patchObject

Patch ActiveSupport::Messages::SerializerWithFallback. Its SERIALIZERS constant is a Hash of serializer modules — but the Hash itself is not Ractor-shareable, so a worker Ractor reading it raises "can not access non-shareable objects in constant ...SERIALIZERS". The individual serializer modules ARE shareable, so route the lookup through IsolatedExecutionState (a per-Ractor cache of the same module references, which workers can read). .load resolves the fallback serializer the same way.



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 427

def _install_messages_serializer_patch
  return if @messages_serializer_patched
  @messages_serializer_patched = true
  _register_patch :messages_serializer, "8.1"
  return unless defined?(::ActiveSupport::Messages::SerializerWithFallback)
  swf = ::ActiveSupport::Messages::SerializerWithFallback
  swf.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def serializer_for(format)
      if Ractor.main?
        SERIALIZERS.fetch(format)
      else
        (RactorRailsShim.storage[:ractor_rails_shim_serializers] ||= {
          marshal: ::ActiveSupport::Messages::SerializerWithFallback::MarshalWithFallback,
          json: ::ActiveSupport::Messages::SerializerWithFallback::JsonWithFallback,
          json_allow_marshal: ::ActiveSupport::Messages::SerializerWithFallback::JsonWithFallbackAllowMarshal,
          message_pack: ::ActiveSupport::Messages::SerializerWithFallback::MessagePackWithFallback,
          message_pack_allow_marshal: ::ActiveSupport::Messages::SerializerWithFallback::MessagePackWithFallbackAllowMarshal,
        })[format]
      end
    end

    def [](format)
      if format.to_s.include?("message_pack") && !defined?(::ActiveSupport::MessagePack)
        require "active_support/message_pack"
      end
      serializer_for(format)
    end
  RUBY
  swf.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def load(dumped)
      format = detect_format(dumped)
      if format == self.format
        _load(dumped)
      elsif format && fallback?(format)
        payload = { serializer: self.format, fallback: format, serialized: dumped }
        ActiveSupport::Notifications.instrument("message_serializer_fallback.active_support", payload) do
          payload[:deserialized] = serializer_for(format)._load(dumped)
        end
      else
        raise "Unsupported serialization format"
      end
    end
  RUBY

  # MessagePackWithFallback#available? lazily memoizes `@available` directly
  # on the module. When a worker Ractor first deserializes a cookie,
  # SerializerWithFallback#load -> detect_format -> MessagePackWithFallback
  # .dumped? -> available? tries to SET that ivar, raising
  #   Ractor::IsolationError: can not set instance variables of
  #   classes/modules by non-main Ractors
  # Replace the ivar memoization with a pure constant check. The module is
  # shareable and ActiveSupport::MessagePack resolves to a shareable class,
  # so this is safe from any Ractor.
  ::ActiveSupport::Messages::SerializerWithFallback::MessagePackWithFallback.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def available?
      defined?(::ActiveSupport::MessagePack)
    end
  RUBY
end

._install_mime_negotiation_worker_patchObject

Patch ActionDispatch::Http::MimeNegotiation#formats and #negotiate_mime to be worker-Ractor safe. Mime[:html] reads from Mime::EXTENSION_LOOKUP, a mutable Hash that is NOT shareable (Ractor.make_shareable fails because Mime::Type objects hold unshareable state). A worker Ractor reading EXTENSION_LOOKUP raises Ractor::IsolationError, which propagates up as an UnknownFormat from Devise's respond_with. Fix: cache the commonly-used Mime types in a shareable IES slot at prepare time; the patched formats method uses the shareable lookup in workers instead of the raw constant.



800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 800

def _install_mime_negotiation_worker_patch
  return if @mime_neg_worker_patched
  @mime_neg_worker_patched = true
  _register_patch :mime_negotiation_worker, "8.1"
  return unless defined?(::ActionDispatch::Http::MimeNegotiation)

  # Capture shareable Mime type references at prepare time (main Ractor).
  if Ractor.main?
    begin
      shareable = {}
      [:html, :json, :xml, :js, :text, :csv, :pdf, :zip, :all].each do |sym|
        mt = ::Mime[sym] rescue nil
        next unless mt
        ::Ractor.make_shareable(mt) rescue nil
        shareable[sym] = mt
      end
      ::Ractor.make_shareable(shareable) rescue nil
      _reassign_shareable_const(:SHAREABLE_MIME_TYPES, shareable)
    rescue StandardError => e
      # best-effort
    end
  end

  mod = ::ActionDispatch::Http::MimeNegotiation
  mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def formats
      fetch_header("action_dispatch.request.formats") do |k|
        v = if params_readable?
          fmt = begin; ::Mime[parameters[:format]]; rescue StandardError; nil; end
          if fmt
            [fmt]
          elsif Ractor.main?
            [::Mime[:html]].compact
          else
            sm = RactorRailsShim::SHAREABLE_MIME_TYPES rescue nil
            sm ? [sm[:html]].compact : []
          end
        elsif use_accept_header && valid_accept_header
          accepts.dup
        elsif extension_format = format_from_path_extension
          [extension_format]
        elsif xhr?
          if Ractor.main?
            [::Mime[:js]].compact
          else
            sm = RactorRailsShim::SHAREABLE_MIME_TYPES rescue nil
            sm ? [sm[:js]].compact : []
          end
        else
          if Ractor.main?
            [::Mime[:html]].compact
          else
            sm = RactorRailsShim::SHAREABLE_MIME_TYPES rescue nil
            sm ? [sm[:html]].compact : []
          end
        end

        v.select! do |format|
          format.symbol || format.ref == "*/*"
        end

        set_header k, v
      end
    end
  RUBY

  # Patch the Collector#negotiate_format to use the shareable Mime types
  # when comparing against request.formats. The Collector is created inside
  # respond_with and holds @responses keyed by Mime::Type objects. If those
  # objects were created in a worker (via Mime[:html] on a non-shareable
  # EXTENSION_LOOKUP), they may be different instances than the ones in
  # request.formats. Use equal? comparison via the shareable snapshot to
  # ensure identity.
  if defined?(::ActionController::MimeResponds::Collector)
    ::ActionController::MimeResponds::Collector.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def negotiate_format(request)
        @format = request.negotiate_mime(@responses.keys)
        # Worker fallback: if negotiation failed but the request accepts
        # HTML and we have an HTML response, return it. This handles the
        # case where Mime type identity comparison fails across Ractors
        # due to different object instances.
        if @format.nil? && @responses.keys.any?
          sm = (RactorRailsShim::SHAREABLE_MIME_TYPES rescue nil)
          if sm
            req_fmts = request.formats rescue []
            req_fmts.each do |rf|
              @responses.keys.each do |rk|
                if rf.respond_to?(:symbol) && rk.respond_to?(:symbol) && rf.symbol == rk.symbol
                  @format = rk
                  return @format
                end
              end
            end
          end
        end
        @format
      end
    RUBY
  end
end

._install_module_introspection_patchObject

Patch Module#module_parent_name so a worker Ractor does not write the @parent_name class ivar on a shared (non-frozen) module. The default memoizes @parent_name ||= ... on first use; when that first use happens in a worker it writes a class ivar on a shared module, which raises Ractor::IsolationError ("can not set instance variables of classes/modules by non-main Ractors"). Route the per-worker cache through IsolatedExecutionState (keyed by module object_id); main keeps the original class-ivar behavior.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 178

def _install_module_introspection_patch
  return if @module_introspection_patched
  @module_introspection_patched = true
  _register_patch :module_introspection, "8.1"
  return unless defined?(::Module)
  ::Module.module_eval do
    def module_parent_name
      if defined?(@parent_name)
        @parent_name
      else
        name = self.name
        return if name.nil?

        parent_name = name =~ /::[^:]+\z/ ? -$` : nil
        if Ractor.main?
          @parent_name = parent_name unless frozen?
        else
          store = (RactorRailsShim.storage[:rrs_module_parent_names] ||= {})
          store[object_id] ||= parent_name
        end
        parent_name
      end
    end
  end
end

._install_notifications_notifier_patchObject

Patch ActiveSupport::Notifications.notifier to not read the @notifier class ivar from a worker Ractor. The original is attr_accessor :notifier with @notifier = Fanout.new set at module load — a raw class ivar holding a Fanout (which has a Mutex + subscriber Procs, both unshareable). Workers get their own per-Ractor Fanout (no subscribers — instrumentation is a no-op in workers, which is correct for a read-only shared app where log subscribers already ran in main). notifier is read by instrumenter (per-request via Rails::Rack::Logger).

Moved here from execution_wrapper.rb (it patches ActiveSupport

Notifications, not ExecutionWrapper).



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 95

def _install_notifications_notifier_patch
  return if @notifications_notifier_patched
  @notifications_notifier_patched = true
  _register_patch :notifications_notifier, "8.1"
  return unless defined?(::ActiveSupport::Notifications)
  notif = ::ActiveSupport::Notifications
  nkey = :ractor_rails_shim_notifications_notifier
  nkey_str = nkey.inspect
  notif.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def notifier
      v = RactorRailsShim.storage[#{nkey_str}]
      return v if RactorRailsShim.storage.key?(#{nkey_str})
      if Ractor.main? && instance_variable_defined?(:@notifier)
        @notifier
      else
        built = ActiveSupport::Notifications::Fanout.new
        RactorRailsShim.storage[#{nkey_str}] = built
        built
      end
    end
  RUBY
end

._install_openssl_digest_patchObject



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ractor_rails_shim/patches/openssl.rb', line 29

def _install_openssl_digest_patch
  return if @openssl_digest_patched
  @openssl_digest_patched = true
  _register_patch :openssl_digest, "8.1"
  return unless defined?(::OpenSSL::Digest)

  algos = %w(MD4 MD5 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512)
  algos.each do |name|
    klass_name = name.tr("-", "_")
    next unless ::OpenSSL::Digest.const_defined?(klass_name)
    klass = ::OpenSSL::Digest.const_get(klass_name)
    next unless klass.is_a?(::Class)

    # Replace the lambda-based `initialize` (super(name, data)) with a
    # string-eval'd method that holds no captured Proc.
    klass.class_eval "def initialize(data = nil); super(#{name.inspect}, data); end"

    # Replace the singleton `digest`/`hexdigest` blocks the same way.
    klass.singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def digest(data)
        new.digest(data)
      end
      def hexdigest(data)
        new.hexdigest(data)
      end
    RUBY
  end
end

._install_orm_adapter_patchObject



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/ractor_rails_shim/patches/orm_adapter.rb', line 14

def _install_orm_adapter_patch
  return if @orm_adapter_patched
  @orm_adapter_patched = true
  _register_patch :orm_adapter, "8.1"
  return unless defined?(::OrmAdapter::ToAdapter)

  ::OrmAdapter::ToAdapter.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def to_adapter
      key = :"ractor_rails_shim_orm_adapter_\#{object_id}"
      v = RactorRailsShim.storage[key]
      return v if RactorRailsShim.storage.key?(key)
      adapter = self::OrmAdapter.new(self)
      RactorRailsShim.storage[key] = adapter
      adapter
    end
  RUBY
end

._install_parameter_encoding_patchObject

Patch ActionController::ParameterEncoding::ClassMethods#action_encoding_template to not read @_parameter_encodings (a raw class ivar) from a worker Ractor. The default is an empty-ish Hash; for a frozen shared app workers get an empty frozen Hash (no per-action param encodings — correct for apps that don't declare parameter_encoding, e.g. the health controller).



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ractor_rails_shim/patches/action_controller.rb', line 34

def _install_parameter_encoding_patch
  return if @param_encoding_patched
  @param_encoding_patched = true
  _register_patch :parameter_encoding, "8.1"
  return unless defined?(::ActionController::ParameterEncoding)
  pe = ::ActionController::ParameterEncoding::ClassMethods
  pe.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def action_encoding_template(action)
      enc = if Ractor.main?
        instance_variable_defined?(:@_parameter_encodings) ? @_parameter_encodings : nil
      else
        RactorRailsShim.storage[:ractor_rails_shim_param_encodings]
      end
      if enc && enc.has_key?(action.to_s)
        enc[action.to_s]
      end
    end
  RUBY
end

._install_path_registry_patchObject

Patch ActionView::PathRegistry to not read its raw class ivars (@view_paths_by_class, @file_system_resolvers) from a worker Ractor. These are populated at boot (view paths registered by the app). For a frozen shared app they're read-only; workers read them via the shareable fallback (built from main's values, made shareable). get_view_paths is called per-request during view lookup; all_file_system_resolvers is called by the exception backtrace builder.



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 398

def _install_path_registry_patch
  return if @path_registry_patched
  @path_registry_patched = true
  _register_patch :path_registry, "8.1"
  return unless defined?(::ActionView::PathRegistry)
  pr = ::ActionView::PathRegistry
  vpc_key = :ractor_rails_shim_path_registry_view_paths_by_class
  fsr_key = :ractor_rails_shim_path_registry_file_system_resolvers
  vpc_key_str = vpc_key.inspect
  fsr_key_str = fsr_key.inspect
  pr.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    # View paths are keyed by CLASS NAME (not the class object). In
    # ractor mode the worker Ractors rebind the app's constants into their
    # own namespace, so a worker's controller class is a DIFFERENT object
    # than the one used as the key in the main Ractor — keying by the
    # (stable) class name makes the shareable fallback resolve correctly
    # across that rebinding. A nil class (end of the ancestor walk)
    # terminates the recursion instead of calling #superclass on nil.
    def get_view_paths(klass)
      return [] if klass.nil?
      name = klass.respond_to?(:name) ? klass.name : nil
      h = RactorRailsShim.storage[#{vpc_key_str}]
      h = (Ractor.main? ? (instance_variable_defined?(:@view_paths_by_class) ? instance_variable_get(:@view_paths_by_class) : {}) : RactorRailsShim::SHAREABLE_FALLBACK[#{vpc_key_str}]) if h.nil?
      return h[name] if name && h.key?(name)
      get_view_paths(klass.superclass)
    end

    def set_view_paths(klass, paths)
      name = klass.respond_to?(:name) ? klass.name : nil
      return unless name
      h = RactorRailsShim.storage[#{vpc_key_str}]
      if h.nil?
        # Adopt any class-ivar entries Rails populated with Class-object keys
        # before this patch installed, re-keying them by (stable) class NAME
        # so the shareable fallback resolves across worker rebinding (a
        # worker's controller class is a different object than main's).
        h = {}
        if Ractor.main? && instance_variable_defined?(:@view_paths_by_class)
          old = instance_variable_get(:@view_paths_by_class)
          old.each do |k, v|
            nk = k.respond_to?(:name) ? k.name : k
            h[nk] = v if nk
          end
        end
        RactorRailsShim.storage[#{vpc_key_str}] = h
      end
      h[name] = paths
      instance_variable_set(:@view_paths_by_class, h) if Ractor.main?
    end

    def all_file_system_resolvers
      h = RactorRailsShim.storage[#{fsr_key_str}]
      h = (Ractor.main? ? (instance_variable_defined?(:@file_system_resolvers) ? instance_variable_get(:@file_system_resolvers) : {}) : RactorRailsShim::SHAREABLE_FALLBACK[#{fsr_key_str}]) if h.nil?
      h.values
    end

    # all_resolvers reads @view_paths_by_class directly (an unshareable
    # class ivar), which raises Ractor::IsolationError in a worker Ractor
    # and is hit while building an exception backtrace (masking the real
    # error). Route it through IES + the shareable fallback like the
    # other PathRegistry accessors.
    def all_resolvers
      h = RactorRailsShim.storage[#{vpc_key_str}]
      h = (Ractor.main? ? (instance_variable_defined?(:@view_paths_by_class) ? instance_variable_get(:@view_paths_by_class) : {}) : RactorRailsShim::SHAREABLE_FALLBACK[#{vpc_key_str}]) if h.nil?
      resolvers = [all_file_system_resolvers]
      resolvers.concat h.values.map(&:to_a)
      resolvers.flatten.uniq
    end
  RUBY
  if Ractor.main?
    # Rails populates @view_paths_by_class with Class-object keys (via the
    # original set_view_paths, which runs before this patch installs). Re-key
    # by (stable) class NAME so the shareable fallback resolves across worker
    # constant rebinding (a worker's controller class is a different object
    # than main's). @file_system_resolvers is already keyed by path string.
    if pr.instance_variable_defined?(:@view_paths_by_class)
      old = pr.instance_variable_get(:@view_paths_by_class)
      rekeyed = {}
      old.each do |k, v|
        nk = k.respond_to?(:name) ? k.name : k
        rekeyed[nk] = v if nk
      end
      pr.instance_variable_set(:@view_paths_by_class, rekeyed)
    end
  end
  # Register so the fallback builder captures + shares these.
  CLASS_ATTRIBUTES << ["ActionView::PathRegistry", :view_paths_by_class, vpc_key, {}]
  CLASS_ATTRIBUTES << ["ActionView::PathRegistry", :file_system_resolvers, fsr_key, {}]
end

._install_polymorphic_routes_patchObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
# File 'lib/ractor_rails_shim/patches/polymorphic_routes.rb', line 21

def _install_polymorphic_routes_patch
  return if @polymorphic_routes_patched
  @polymorphic_routes_patched = true
  _register_patch :polymorphic_routes, "8.1"
  return unless defined?(::ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder)
  hmb = ::ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder
  hmb.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def get(action, type)
      type = type.to_sym
      build action, type
    end

    def url
      build nil, "url"
    end

    def path
      build nil, "path"
    end
  RUBY
  hmb.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def handle_model_call(target, record)
      if mapping = polymorphic_mapping(target, record) rescue nil
        mapping.call(target, [record], suffix == "path")
      else
        method, args = handle_model(record)
        target.public_send(method, *args)
      end
    end
  RUBY

  # The module-level `polymorphic_path` / `polymorphic_url` (called by
  # `form_with` when it infers the URL from a model) invoke
  # `mapping.call` directly. A custom `resolve` mapping Proc is built in
  # the main Ractor and is un-shareable, so calling it from a worker
  # Ractor raises "defined with an un-shareable Proc in a different
  # Ractor". Rescue that and fall through to the (worker-safe)
  # HelperMethodBuilder path, which derives the route from the record's
  # model name — the same fallback the original code uses when no mapping
  # is registered at all.
  pm = ::ActionDispatch::Routing::PolymorphicRoutes
  pm.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def polymorphic_path(record_or_hash_or_array, options = {})
      if ::Hash === record_or_hash_or_array
        options = record_or_hash_or_array.merge(options)
        record  = options.delete :id
        return polymorphic_path record, options
      end

      if mapping = (polymorphic_mapping(record_or_hash_or_array) rescue nil)
        begin
          return mapping.call(self, [record_or_hash_or_array, options], true)
        rescue ::Ractor::Error
        end
      end

      opts   = options.dup
      action = opts.delete :action
      type   = :path

      HelperMethodBuilder.polymorphic_method self,
                                             record_or_hash_or_array,
                                             action,
                                             type,
                                             opts
    end

    def polymorphic_url(record_or_hash_or_array, options = {})
      if ::Hash === record_or_hash_or_array
        options = record_or_hash_or_array.merge(options)
        record  = options.delete :id
        return polymorphic_url record, options
      end

      if mapping = (polymorphic_mapping(record_or_hash_or_array) rescue nil)
        begin
          return mapping.call(self, [record_or_hash_or_array, options], false)
        rescue ::Ractor::Error
        end
      end

      opts   = options.dup
      action = opts.delete :action
      type   = opts.delete(:routing_type) || :url

      HelperMethodBuilder.polymorphic_method self,
                                             record_or_hash_or_array,
                                             action,
                                             type,
                                             opts
    end
  RUBY
end

._install_propshaft_patchObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ractor_rails_shim/patches/propshaft.rb', line 27

def _install_propshaft_patch
  return if @propshaft_patched
  @propshaft_patched = true
  _register_patch :propshaft, "1.3"
  return unless defined?(::Propshaft::LoadPath)

  lp = ::Propshaft::LoadPath
  lp.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def asset_paths_by_type(content_type)
      cache = (RactorRailsShim.storage[:"ractor_rails_shim_propshaft_type_\#{object_id}"] ||= {})
      if cache.key?(content_type)
        cache[content_type]
      else
        cache[content_type] = extract_logical_paths_from(assets.select { |a| a.content_type == ::Mime::EXTENSION_LOOKUP[content_type] })
      end
    end

    def asset_paths_by_glob(glob)
      cache = (RactorRailsShim.storage[:"ractor_rails_shim_propshaft_glob_\#{object_id}"] ||= {})
      if cache.key?(glob)
        cache[glob]
      else
        cache[glob] = extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(glob) })
      end
    end
  RUBY

  if lp.method_defined?(:asset_paths_by_path)
    lp.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def asset_paths_by_path(path)
        cache = (RactorRailsShim.storage[:"ractor_rails_shim_propshaft_path_\#{object_id}"] ||= {})
        if cache.key?(path)
          cache[path]
        else
          cache[path] = extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(path) })
        end
      end
    RUBY
  end
end

._install_query_parser_patchObject

ActionDispatch::QueryParser.each_pair returns enum_for(:each_pair, s, separator) when called without a block. The Enumerator wraps a Proc that was compiled in the main Ractor, so when a worker Ractor iterates it (pairs.each inside ParamBuilder#from_pairs) Ruby raises "defined with an un-shareable Proc in a different Ractor". Redefine it to materialize into a plain (shareable) frozen Array using a block-free loop, so worker Ractors can parse form/query pairs without crossing Ractor boundaries.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 124

def _install_query_parser_patch
  return if @query_parser_patched
  @query_parser_patched = true
  return unless defined?(::ActionDispatch::QueryParser)
  ::ActionDispatch::QueryParser.singleton_class.module_eval <<-'RUBY', __FILE__, __LINE__ + 1
    def each_pair(s, separator = nil)
      return _materialized_pairs(s, separator) unless block_given?
      s ||= ""
      splitter =
        if separator
          ::ActionDispatch::QueryParser::COMMON_SEP[separator] || /[#{separator}] */n
        else
          ::ActionDispatch::QueryParser::DEFAULT_SEP
        end
      s.split(splitter).each do |part|
        next if part.empty?
        k, v = part.split("=", 2)
        k = URI.decode_www_form_component(k)
        v &&= URI.decode_www_form_component(v)
        yield k, v
      end
      nil
    end

    def _materialized_pairs(s, separator)
      s ||= ""
      splitter =
        if separator
          ::ActionDispatch::QueryParser::COMMON_SEP[separator] || /[#{separator}] */n
        else
          ::ActionDispatch::QueryParser::DEFAULT_SEP
        end
      parts = s.split(splitter)
      result = []
      i = 0
      while i < parts.length
        part = parts[i]
        i += 1
        if part.empty?
          next
        end
        kv = part.split("=", 2)
        k = URI.decode_www_form_component(kv[0])
        v = kv[1] && URI.decode_www_form_component(kv[1])
        result << [k, v]
      end
      result.freeze
    end
  RUBY
end

._install_rack_request_patchObject

Patch Rack::Request's class-level attr_accessors (forwarded_priority, x_forwarded_proto_priority) to not read @ivars from a worker Ractor. The values are frozen-Symbol Arrays (shareable); route the cache through IES with the same default in workers. Read per-request via ActionDispatch::RemoteIp. Applied at prepare_for_ractors! time (after Rails boots, so Rack is loaded).



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ractor_rails_shim/patches/rack.rb', line 46

def _install_rack_request_patch
  return if @rack_request_patched
  @rack_request_patched = true
  _register_patch :rack_request, "8.1"
  return unless defined?(::Rack::Request)
  req = ::Rack::Request
  fp_key = :ractor_rails_shim_rack_forwarded_priority
  xp_key = :ractor_rails_shim_rack_x_forwarded_proto_priority
  fp_key_str = fp_key.inspect
  xp_key_str = xp_key.inspect
  req.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def forwarded_priority
      v = RactorRailsShim.storage[#{fp_key_str}]
      return v if RactorRailsShim.storage.key?(#{fp_key_str})
      if Ractor.main? && instance_variable_defined?(:@forwarded_priority)
        @forwarded_priority
      else
        [:forwarded, :x_forwarded]
      end
    end
    def forwarded_priority=(val)
      RactorRailsShim.storage[#{fp_key_str}] = val
      @forwarded_priority = val if Ractor.main?
      val
    end
    def x_forwarded_proto_priority
      v = RactorRailsShim.storage[#{xp_key_str}]
      return v if RactorRailsShim.storage.key?(#{xp_key_str})
      if Ractor.main? && instance_variable_defined?(:@x_forwarded_proto_priority)
        @x_forwarded_proto_priority
      else
        [:proto, :scheme]
      end
    end
    def x_forwarded_proto_priority=(val)
      RactorRailsShim.storage[#{xp_key_str}] = val
      @x_forwarded_proto_priority = val if Ractor.main?
      val
    end
  RUBY
end

._install_rack_utils_patchObject

Patch Rack::Utils singleton attr_accessors (default_query_parser, multipart_total_part_limit, multipart_file_limit) to not read @ivars from a worker Ractor. The values are shareable once frozen (QueryParser, Integers). Route through IES; workers read the shareable fallback. default_query_parser is read per-request during POST parsing.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ractor_rails_shim/patches/rack.rb', line 93

def _install_rack_utils_patch
  return if @rack_utils_patched
  @rack_utils_patched = true
  _register_patch :rack_utils, "8.1"
  return unless defined?(::Rack::Utils)
  u = ::Rack::Utils
  dqp_key = :ractor_rails_shim_rack_utils_default_query_parser
  mtp_key = :ractor_rails_shim_rack_utils_multipart_total_part_limit
  mfl_key = :ractor_rails_shim_rack_utils_multipart_file_limit
  dqp_key_str = dqp_key.inspect
  mtp_key_str = mtp_key.inspect
  mfl_key_str = mfl_key.inspect
  u.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def default_query_parser
      v = RactorRailsShim.storage[#{dqp_key_str}]
      return v if RactorRailsShim.storage.key?(#{dqp_key_str})
      if Ractor.main? && instance_variable_defined?(:@default_query_parser)
        v = @default_query_parser
        RactorRailsShim.storage[#{dqp_key_str}] = v
        v
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{dqp_key_str}] || ::Rack::QueryParser::QueryParser.make_default(32)
      end
    end
    def multipart_total_part_limit
      v = RactorRailsShim.storage[#{mtp_key_str}]
      return v if RactorRailsShim.storage.key?(#{mtp_key_str})
      if Ractor.main? && instance_variable_defined?(:@multipart_total_part_limit)
        v = @multipart_total_part_limit
        RactorRailsShim.storage[#{mtp_key_str}] = v
        v
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{mtp_key_str}] || 128
      end
    end
    def multipart_file_limit
      v = RactorRailsShim.storage[#{mfl_key_str}]
      return v if RactorRailsShim.storage.key?(#{mfl_key_str})
      if Ractor.main? && instance_variable_defined?(:@multipart_file_limit)
        v = @multipart_file_limit
        RactorRailsShim.storage[#{mfl_key_str}] = v
        v
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{mfl_key_str}] || 64
      end
    end
  RUBY
  CLASS_ATTRIBUTES << ["Rack::Utils", :default_query_parser, dqp_key, nil]
  CLASS_ATTRIBUTES << ["Rack::Utils", :multipart_total_part_limit, mtp_key, nil]
  CLASS_ATTRIBUTES << ["Rack::Utils", :multipart_file_limit, mfl_key, nil]
end

._install_reloader_patchObject

Patch ActiveSupport::Reloader#check! / #reloaded!. These are CLASS

methods that memoize @should_reload in a class ivar. ActionDispatch

Executor#call runs Reloader.run! -> check! on EVERY request, so a worker Ractor writing that class ivar raises Ractor::IsolationError ("can not set instance variables of classes/modules by non-main Ractors"). Route the flag through IsolatedExecutionState so each Ractor has its own. With reloading disabled (config.enable_reloading = false, the right setting for a frozen, shared kino :ractor graph) check.call is lambda { false }, so workers compute false (no reload) — but the write must still be Ractor-safe.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/ractor_rails_shim/patches/active_support.rb', line 350

def _install_reloader_patch
  return if @reloader_patched
  @reloader_patched = true
  _register_patch :reloader, "8.1"
  return unless defined?(::ActiveSupport::Reloader)
  rl = ::ActiveSupport::Reloader
  key = :ractor_rails_shim_reloader_should_reload
  key_str = key.inspect
  rl.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def check!
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      result = check.call
      RactorRailsShim.storage[#{key_str}] = result
      result
    end

    def reloaded!
      RactorRailsShim.storage[#{key_str}] = false
    end
  RUBY
end

._install_request_parameter_parsers_patchObject

Patch ActionDispatch::Request.parameter_parsers (singleton attr_reader backed by @parameter_parsers) to not read the class ivar from a worker Ractor. The value is a Hash of MIME-type → parser (lambdas). Route through IES; workers read the shareable fallback (the boot-time parsers, made shareable). Read per-request during parameter parsing.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 93

def _install_request_parameter_parsers_patch
  return if @request_param_parsers_patched
  @request_param_parsers_patched = true
  _register_patch :request_parameter_parsers, "8.1"
  return unless defined?(::ActionDispatch::Request)
  req = ::ActionDispatch::Request
  pp_key = :ractor_rails_shim_request_parameter_parsers
  pp_key_str = pp_key.inspect
  req.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def parameter_parsers
      v = RactorRailsShim.storage[#{pp_key_str}]
      return v if RactorRailsShim.storage.key?(#{pp_key_str})
      if Ractor.main? && instance_variable_defined?(:@parameter_parsers)
        v = @parameter_parsers
        RactorRailsShim.storage[#{pp_key_str}] = v
        v
      else
        RactorRailsShim::SHAREABLE_FALLBACK[#{pp_key_str}] || ActionDispatch::Request::DEFAULT_PARSERS
      end
    end
  RUBY
  CLASS_ATTRIBUTES << ["ActionDispatch::Request", :parameter_parsers, pp_key, nil]
end

._install_secure_random_alphabets!Object

ActiveSupport core-ext defines SecureRandom::BASE36_ALPHABET / BASE58_ALPHABET as Arrays of non-frozen Strings. These constants are read by SecureRandom.base36 / base58, which ActiveStorage::Blob calls to generate upload keys. A worker Ractor raises IsolationError accessing a non-shareable constant, so deep-freeze them (making them shareable) in the main Ractor. They are immutable by contract, so freezing is safe.

The core-ext is loaded lazily during boot. The constants are defined via module-body assignment (BASE36_ALPHABET = (...)), which does NOT fire a TracePoint(:constant) event, so we can't rely on the trace alone. Instead we attempt the freeze at install time AND at prepare_for_ractors! time (after initialize!, when the core-ext is guaranteed loaded). The _freeze_secure_random_alphabets! helper is idempotent and skips already shareable constants.



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 295

def _install_secure_random_alphabets!
  _freeze_secure_random_alphabets!
  return if @_secure_random_tp
  return unless defined?(::SecureRandom)

  @_secure_random_tp = TracePoint.new(:constant) do |tp|
    name = tp.const_name
    next unless name == "SecureRandom::BASE36_ALPHABET" ||
                name == "SecureRandom::BASE58_ALPHABET"
    _freeze_secure_random_alphabets!
    if %i[BASE36_ALPHABET BASE58_ALPHABET].all? do |c|
         ::SecureRandom.const_defined?(c) && ::Ractor.shareable?(::SecureRandom.const_get(c))
       end
      @_secure_random_tp.disable
      @_secure_random_tp = nil
    end
  end
  @_secure_random_tp.enable
rescue StandardError
  nil
end

._install_template_handlers_patchObject



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 298

def _install_template_handlers_patch
  return if @template_handlers_patched
  @template_handlers_patched = true
  _register_patch :template_handlers, "8.1"
  return unless defined?(::ActionView::Template::Handlers)
  # Eager-load the handler classes in main so workers don't need to
  # autoload them (workers can't autoload).
  if Ractor.main?
    ::ActionView::Template::Handlers::Raw rescue nil
    ::ActionView::Template::Handlers::ERB rescue nil
    ::ActionView::Template::Handlers::Html rescue nil
    ::ActionView::Template::Handlers::Builder rescue nil
  end
  h = ::ActionView::Template::Handlers
  th_key = :ractor_rails_shim_av_template_handlers
  dth_key = :ractor_rails_shim_av_default_template_handlers
  th_key_str = th_key.inspect
  dth_key_str = dth_key.inspect
  # The handler registry lives in class variables (@@template_handlers,
  # @@default_template_handlers, @@template_extensions) whose values are
  # mutable Hashes holding handler instances — and an unshareable `:ruby`
  # lambda. A worker Ractor cannot read these class vars. Route the
  # registry through IsolatedExecutionState: each Ractor builds its own
  # handler map (the defaults are deterministic), and in main we seed from
  # the live class var (capturing any custom handlers gems registered at
  # boot). The `:ruby` lambda makes the map unshareable, so the old
  # SHAREABLE_FALLBACK approach (which skips unshareable values) left
  # workers with an empty map. These are instance methods (Handlers is
  # extended into ActionView::Template, and the render path calls them on
  # the Template instance), so they must be defined on the module itself,
  # not just the singleton class.
  h.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def self._ractor_rails_shim_handlers
      map = RactorRailsShim.storage[#{th_key_str}]
      return map unless map.nil?
      if Ractor.main?
        cv = class_variable_get(:@@template_handlers) rescue nil
        cv = nil if cv && cv.empty?
        if cv
          RactorRailsShim.storage[#{th_key_str}] = cv
          return cv
        end
      end
      built = {
        raw: ::ActionView::Template::Handlers::Raw.new,
        erb: ::ActionView::Template::Handlers::ERB.new,
        html: ::ActionView::Template::Handlers::Html.new,
        builder: ::ActionView::Template::Handlers::Builder.new,
        ruby: ->(_, source) { source },
      }
      RactorRailsShim.storage[#{th_key_str}] = built
      built
    end

    def self._ractor_rails_shim_persist(map)
      RactorRailsShim.storage[#{th_key_str}] = map
      class_variable_set(:@@template_handlers, map) if Ractor.main?
    end

    def self.extensions
      self._ractor_rails_shim_handlers.keys
    end

    def registered_template_handler(extension)
      extension && ::ActionView::Template::Handlers._ractor_rails_shim_handlers[extension.to_sym]
    end

    def handler_for_extension(extension)
      registered_template_handler(extension) || ::ActionView::Template::Handlers::ERB.new
    end

    def template_handler_extensions
      ::ActionView::Template::Handlers._ractor_rails_shim_handlers.keys.map(&:to_s).sort
    end

    def register_template_handler(*extensions, handler)
      map = ::ActionView::Template::Handlers._ractor_rails_shim_handlers.dup
      extensions.each { |ext| map[ext.to_sym] = handler }
      ::ActionView::Template::Handlers._ractor_rails_shim_persist(map)
    end

    def unregister_template_handler(*extensions)
      map = ::ActionView::Template::Handlers._ractor_rails_shim_handlers.dup
      extensions.each { |ext| map.delete(ext.to_sym) }
      ::ActionView::Template::Handlers._ractor_rails_shim_persist(map)
    end

    def register_default_template_handler(extension, klass)
      register_template_handler(extension, klass)
    end
  RUBY
end

._install_warden_hooks_patchObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/ractor_rails_shim/patches/warden.rb', line 27

def _install_warden_hooks_patch
  return if @warden_patched
  @warden_patched = true
  _register_patch :warden_hooks, "8.1"
  return unless defined?(::Warden::Hooks)
  ::Warden::Hooks.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def _after_set_user
      if Ractor.main? && instance_variable_defined?(:@_after_set_user)
        @_after_set_user
      else
        []
      end
    end
    def _before_failure
      if Ractor.main? && instance_variable_defined?(:@_before_failure)
        @_before_failure
      else
        []
      end
    end
    def _after_failed_fetch
      if Ractor.main? && instance_variable_defined?(:@_after_failed_fetch)
        @_after_failed_fetch
      else
        []
      end
    end
    def _before_logout
      if Ractor.main? && instance_variable_defined?(:@_before_logout)
        @_before_logout
      else
        []
      end
    end
    def _on_request
      if Ractor.main? && instance_variable_defined?(:@_on_request)
        @_on_request
      else
        []
      end
    end
  RUBY
end

._install_warden_serializer_patchObject

Warden registers per-scope session serializers with Warden::SessionSerializer.send(:define_method, method_name, &block) (warden-1.2.9/lib/warden/manager.rb:71). The block is created while the app boots in the main Ractor, so the resulting method is Ractor-bound: invoking it (e.g. user_serialize) from a worker Ractor raises "defined with an un-shareable Proc in a different Ractor".

Devise's block body is simply mapping.to.serialize_into_session(record). We re-register the serializers as plain def methods (which are NOT Ractor-bound) that delegate to the model class's own serialize_into_session / serialize_from_session class methods — both worker-safe — so the chain is callable from any worker Ractor.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ractor_rails_shim/patches/warden.rb', line 115

def _install_warden_serializer_patch
  return if @warden_serializer_patched
  @warden_serializer_patched = true
  _register_patch :warden_serializer, "8.1"
  return unless defined?(::Warden::SessionSerializer)

  if defined?(::Devise) && ::Devise.respond_to?(:mappings)
    ::Devise.mappings.each do |scope, mapping|
      model = mapping.to
      ::Warden::SessionSerializer.class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{scope}_serialize(record)
          #{model}.serialize_into_session(record)
        end

        def #{scope}_deserialize(*keys)
          # Devise's serialize_into_session returns [[id], salt]. The key
          # passed by Warden::SessionSerializer#fetch is that value, but in
          # the kino :ractor worker the per-request session that Warden's
          # serializer sees can hold an extra wrapping layer
          # ([[[id], salt]]). Flatten so serialize_from_session(key, salt)
          # always receives exactly two arguments.
          #{model}.serialize_from_session(*keys.flatten)
        end
      RUBY
    end
  end

  ::Warden::SessionSerializer.class_eval do
    unless method_defined?(:serialize)
      def serialize(user)
        user
      end
    end

    unless method_defined?(:deserialize)
      def deserialize(key)
        key
      end
    end
  end
end

._install_warden_strategies_patchObject

Patch Warden::Strategies#_strategies. The strategy registry is a lazy class ivar (@strategies ||= {}) on the Warden::Strategies module; a worker Ractor reading it raises "can not get unshareable values from instance variables of classes/modules from non-main Ractors" (Devise's current_user / user_signed_in? in a layout hits Warden::Strategies -> _strategies). Capture the (shareable) registry in main and expose it via a constant that workers read.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ractor_rails_shim/patches/warden.rb', line 78

def _install_warden_strategies_patch
  return if @warden_strategies_patched
  @warden_strategies_patched = true
  _register_patch :warden_strategies, "8.1"
  return unless defined?(::Warden::Strategies)
  ::Warden::Strategies.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def _strategies
      if Ractor.main?
        @strategies ||= {}
      else
        RactorRailsShim::SHAREABLE_WARDEN_STRATEGIES || {}
      end
    end
  RUBY
  if Ractor.main?
    begin
      strat = ::Warden::Strategies.instance_variable_get(:@strategies)
      strat = Ractor.make_shareable(strat) if strat && !Ractor.shareable?(strat)
      RactorRailsShim.const_set(:SHAREABLE_WARDEN_STRATEGIES, strat) unless RactorRailsShim.const_defined?(:SHAREABLE_WARDEN_STRATEGIES)
    rescue StandardError
      nil
    end
  end
end

._install_with_empty_template_cache_patchObject

Patch ActionView::Base.with_empty_template_cache (action_view/base.rb:204) to a block-free def. The original defines compiled_method_container (instance + singleton) via define_method(&block) — an un-shareable Proc compiled in the main Ractor that raises "defined with an un-shareable Proc in a different Ractor" when a worker calls it. We also route compiled template methods through ONE shared SHAREABLE_COMPILED_MODULE so the application layout (and Devise shared partials) compile once and are visible to every controller / worker Ractor.

Installed EARLY via ActiveSupport.on_load(:action_view) (see core.rb install) so it is in place before production eager load calls DetailsKey.view_context_class -> with_empty_template_cache. Idempotent.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ractor_rails_shim/patches/action_view.rb', line 43

def _install_with_empty_template_cache_patch
  return if @with_empty_template_cache_patched
  return unless defined?(::ActionView::Base) && Ractor.main?
  @with_empty_template_cache_patched = true
  _register_patch :with_empty_template_cache, "8.1"
  ::ActionView::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def with_empty_template_cache
      subclass = Class.new(self) do
        include RactorRailsShim::SHAREABLE_COMPILED_MODULE
        def compiled_method_container
          RactorRailsShim::SHAREABLE_COMPILED_MODULE
        end
        def self.compiled_method_container
          RactorRailsShim::SHAREABLE_COMPILED_MODULE
        end
        def inspect
          "#<ActionView::Base:\#{'%#016x' % (object_id << 1)}>"
        end
      end
      subclass
    end
  RUBY
end

._make_mail_constants_shareable!Object

Walk every module/class under Mail and make each un-shareable constant into; leaf values (Regexp, Array, Hash, String, …) are frozen via Ractor.make_shareable. Truly un-shareable values (e.g. Procs bound to the main Ractor) are skipped — the worker would fall back to its own behavior. This clears the mail gem's many Regexp module constants (e.g. Mail::Utilities::TO_CRLF_REGEX), which would otherwise raise IsolationError when read from a worker Ractor.



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/ractor_rails_shim/patches/mail.rb', line 380

def _make_mail_constants_shareable!
  root = ::Mail
  seen = {}
  walk = lambda do |mod|
    return if seen[mod.object_id]
    seen[mod.object_id] = true
    mod.constants(false).each do |cname|
      val = mod.const_get(cname)
      if val.is_a?(Module)
        walk.call(val)
      elsif !::Ractor.shareable?(val)
        begin
          ::Ractor.make_shareable(val)
        rescue StandardError
          nil
        end
      end
    rescue StandardError
      nil
    end
  end
  walk.call(root)
rescue StandardError
  nil
end

._maybe_apply_active_storage_patchObject



332
333
334
335
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 332

def _maybe_apply_active_storage_patch
  _apply_active_storage_macro_patch unless @_as_patched_macros
  _apply_active_storage_blob_patch unless @_as_patched_blob
end

._patch_active_model_type_default_value!Object



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 717

def _patch_active_model_type_default_value!
  return if @active_model_type_default_patched
  @active_model_type_default_patched = true
  _register_patch :active_model_type_default_value, "8.1"
  return unless defined?(::ActiveModel::Type::Value)
  return unless ::ActiveModel::Type.respond_to?(:default_value)

  # Warm the default in the main Ractor and build a shareable copy that
  # workers can read. Workers MUST NOT call _rrs_orig_default_value because
  # the original reads @default_value (a class ivar on ActiveModel::Type)
  # which is not accessible from non-main Ractors. Capture + shareable-copy
  # the template HERE (in main, at patch time) so the worker's patched
  # method never touches the raw ivar.
  main_default = ::ActiveModel::Type.default_value rescue nil
  return unless main_default

  # Build a shareable copy of the default_value now (in main) so workers
  # can read it without touching the class ivar.
  shareable_default = begin
    copy = main_default.dup
    Ractor.make_shareable(copy) rescue copy
  end

  ::ActiveModel::Type.singleton_class.class_eval do
    alias_method :_rrs_orig_default_value, :default_value
    def default_value
      if Ractor.main?
        _rrs_orig_default_value
      else
        RactorRailsShim.storage[:rrs_am_type_default_value] ||= (
          RactorRailsShim::SHAREABLE_AM_TYPE_DEFAULT
        )
      end
    end
  end

  # Store the shareable default as a constant so workers can read it
  # without touching @default_value on ActiveModel::Type.
  _reassign_shareable_const(:SHAREABLE_AM_TYPE_DEFAULT, shareable_default)
end

._patch_rails_module_body(mod) ⇒ Object



68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/ractor_rails_shim/patches/rails_module.rb', line 68

def _patch_rails_module_body(mod)
  k = KEYS

  # Register the Rails module accessors in CLASS_ATTRIBUTES so the
  # shareable-fallback builder captures their main-ractor values at
  # prepare_for_ractors! / make_app_shareable! time and exposes them to
  # worker Ractors (e.g. Rails.logger is read per-request by
  # Rails::Rack::SilenceRequest). `application` is NOT registered — workers
  # get the shared app via Ractor.new(app), not via Rails.application.
  CLASS_ATTRIBUTES << ["Rails", :logger,        k[:logger],        nil]
  CLASS_ATTRIBUTES << ["Rails", :cache,         k[:cache],         nil]
  CLASS_ATTRIBUTES << ["Rails", :backtrace_cleaner, k[:backtrace_cleaner], nil]
  CLASS_ATTRIBUTES << ["Rails", :app_class,     k[:app_class],     nil]

  # We PREPEND a module onto Rails.singleton_class rather than redefine
  # the methods directly, because Rails defines its own `application`,
  # `env`, etc. LATER in rails.rb (`class << self; def application; ...`).
  # A direct module_eval redefinition would be clobbered when Rails'
  # own `def` runs afterward. A prepended module sits in front of the
  # singleton class in the method lookup chain and survives a later
  # `def` on the same class — so our IES-routed reader stays in front.
  # We call `super` to fall back to Rails' original method for the
  # main-ractor lazy-init path (which reads the @application ivar —
  # safe in the main ractor).
  patch = Module.new
  patch.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    # application: IES first; main ractor falls back to Rails' own
    # lazy init via super. Worker ractors return nil (their own IES
    # slot is empty until they boot their own app).
    def application
      v = RactorRailsShim.storage[#{k[:application].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:application].inspect})
      if Ractor.main?
        super
      else
        RactorRailsShim::AppShareabilizer.const_defined?(:SHAREABLE_APP) ? RactorRailsShim::AppShareabilizer::SHAREABLE_APP : nil
      end
    end

    def application=(val)
      RactorRailsShim.storage[#{k[:application].inspect}] = val
      super if Ractor.main?
      val
    end

    # Simple accessors: app_class, cache, logger, backtrace_cleaner.
    # Workers fall back to the shareable fallback (built from main's
    # value at make_app_shareable! time) when their own IES slot is empty.
    def app_class
      v = RactorRailsShim.storage[#{k[:app_class].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:app_class].inspect})
      return super if Ractor.main?
      RactorRailsShim::SHAREABLE_FALLBACK[#{k[:app_class].inspect}]
    end
    def app_class=(val)
      RactorRailsShim.storage[#{k[:app_class].inspect}] = val
      super if Ractor.main?
      val
    end

    def cache
      v = RactorRailsShim.storage[#{k[:cache].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:cache].inspect})
      return super if Ractor.main?
      RactorRailsShim::SHAREABLE_FALLBACK[#{k[:cache].inspect}]
    end
    def cache=(val)
      RactorRailsShim.storage[#{k[:cache].inspect}] = val
      super if Ractor.main?
      val
    end

    def logger
      v = RactorRailsShim.storage[#{k[:logger].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:logger].inspect})
      return super if Ractor.main?
      # Loggers are intrinsically mutable (formatters hold tag stacks,
      # logdevs hold IO + Mutex) and can't be shared read-only. Build a
      # per-worker ActiveSupport::BroadcastLogger (which mixes in
      # LoggerSilence#silence, used by Rails::Rack::SilenceRequest)
      # writing to $stderr (each Ractor has its own $stderr stream) and
      # cache it in IES so subsequent reads return the same instance.
      built = ActiveSupport::BroadcastLogger.new(Logger.new($stderr))
      RactorRailsShim.storage[#{k[:logger].inspect}] = built
      built
    end
    def logger=(val)
      RactorRailsShim.storage[#{k[:logger].inspect}] = val
      super if Ractor.main?
      val
    end

    def backtrace_cleaner
      v = RactorRailsShim.storage[#{k[:backtrace_cleaner].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:backtrace_cleaner].inspect})
      return super if Ractor.main?
      RactorRailsShim::SHAREABLE_FALLBACK[#{k[:backtrace_cleaner].inspect}]
    end
    def backtrace_cleaner=(val)
      RactorRailsShim.storage[#{k[:backtrace_cleaner].inspect}] = val
      super if Ractor.main?
      val
    end

    # env: worker ractors build their own EnvironmentInquirer from ENV
    # (no @ _env ivar to read). Main ractor falls back to super, which
    # lazily builds and caches in @_env.
    def env
      v = RactorRailsShim.storage[#{k[:env].inspect}]
      return v if RactorRailsShim.storage.key?(#{k[:env].inspect})
      if Ractor.main?
        super
      else
        built = ActiveSupport::EnvironmentInquirer.new(
          ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
        )
        RactorRailsShim.storage[#{k[:env].inspect}] = built
        built
      end
    end

    def env=(val)
      v = ActiveSupport::EnvironmentInquirer.new(val)
      RactorRailsShim.storage[#{k[:env].inspect}] = v
      super if Ractor.main?
      v
    end
  RUBY
  mod.singleton_class.prepend(patch)
end

._precompute_propshaft!(app) ⇒ Object

Warm every Propshaft Assembly/LoadPath lazy ivar in MAIN (before the app is frozen). Warmed ivars are truthy, so the ||= memoization in workers short-circuits instead of attempting to assign onto a frozen object. Assets::by_path is itself memoized; warming assets populates it so workers only read the frozen cache.



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
105
106
107
108
# File 'lib/ractor_rails_shim/patches/propshaft.rb', line 73

def _precompute_propshaft!(app)
  return unless Ractor.main?
  assets = app.assets rescue nil
  return unless assets
  assets.load_path rescue nil
  assets.compilers rescue nil
  assets.resolver rescue nil
  assets.prefix rescue nil
  assets.processor rescue nil
  # Build the asset map (LoadPath#assets -> assets_by_path) so the inner
  # cache is populated and frozen before workers read it. Then warm each
  # Asset's lazy ivars (@content_type, @digest) in MAIN — they are memoized
  # via `||=` and the Asset objects live inside the frozen, shared app
  # graph, so a worker reading them would raise FrozenError. Warming here
  # (while still mutable) lets the `||=` short-circuit in workers.
  assets.load_path.assets.each do |asset|
    asset.content_type rescue nil
    asset.digest rescue nil
  end
  # Warm the resolver manifest cache. Propshaft::Resolver::Static#manifest
  # memoizes `@manifest ||= Propshaft::Manifest.from_path(...)`; the
  # resolver is frozen in the shared graph, so without warming, a worker
  # rendering a `stylesheet_link_tag` (or reading asset integrity) for a
  # PRECOMPILED manifest raises FrozenError ("can't modify frozen
  # Propshaft::Resolver::Static"). `#manifest` is private — invoke via
  # send to trigger the `||=` in MAIN (while still mutable) so workers only
  # read the frozen, cached value.
  resolver = assets.resolver rescue nil
  if resolver.respond_to?(:manifest, true)
    begin
      resolver.send(:manifest)
    rescue StandardError
      nil
    end
  end
end

._reassign_shareable_const(name, value) ⇒ Object

Reassign a constant on RactorRailsShim with a new shareable value, silencing the "already initialized constant" warning that const_set emits when the constant was previously defined. Centralizes the $VERBOSE-suppressed const_set dance that was repeated at every shareable-constant rebuild site (SHAREABLE_FALLBACK, SHAREABLE_MATTR_DEFAULTS, etc.). The value MUST already be frozen + shareable — this method does not make it so.

Issue #35 (Round 4): for the four frozen registries that Registry owns (SHAREABLE_FALLBACK, SHAREABLE_MATTR_DEFAULTS, ABSTRACT_REGISTRY, VIEW_CONTEXT_REGISTRY), also update the Registry instance variable so role objects reading through Registry.* see the new value. The facade const_set is kept for the string-eval'd code that reads RactorRailsShim::SHAREABLE_*.



125
126
127
128
129
130
131
132
133
134
# File 'lib/ractor_rails_shim/patches/core.rb', line 125

def _reassign_shareable_const(name, value)
  ConstReassign.call(self, name, value)
  case name
  when :SHAREABLE_FALLBACK then Registry.reassign_shareable_fallback(value)
  when :SHAREABLE_MATTR_DEFAULTS then Registry.reassign_shareable_mattr_defaults(value)
  when :ABSTRACT_REGISTRY then Registry.reassign_abstract_registry(value)
  when :VIEW_CONTEXT_REGISTRY then Registry.reassign_view_context_registry(value)
  end
  value
end

._rebuild_activerecord_model_snapshots!Object

(Re)build the per-model shareable snapshots that workers read instead of un-shareable class ivars: AR_PRIMARY_KEYS_SHAREABLE (model name -> primary key) and SHAREABLE_PENDING_ATTR_MODS (model object_id -> attribute macro modifications). Separated from _share_model_classes! so it can be called again AFTER eager-load (at make_app_shareable! time), when ActiveRecord::Base.descendants actually contains the app's models. Called too early (e.g. during the initial install, before eager-load) it only captures ActiveRecord::Base — the post-boot call overwrites the constant with the complete map, which is what workers spawned after make_app_shareable! read.



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 570

def _rebuild_activerecord_model_snapshots!
  return unless defined?(::ActiveRecord::Base)

  classes = [::ActiveRecord::Base]
  classes.concat(::ActiveRecord::Base.descendants) rescue nil

  # pending attribute modifications (custom attribute macros)
  capture = {}
  classes.each do |klass|
    n = klass.name
    next unless n
    begin
      mods = klass.instance_variable_get(:@pending_attribute_modifications)
      if mods && mods.is_a?(Array) && !mods.empty?
        shareable = mods.dup
        Ractor.make_shareable(shareable)
        capture[klass.object_id] = shareable
      end
    rescue StandardError
      nil
    end
  end
  capture.freeze
  Ractor.make_shareable(capture)
  RactorRailsShim._reassign_shareable_const(:SHAREABLE_PENDING_ATTR_MODS, capture)

  # primary keys
  pk_map = {}
  classes.each do |klass|
    n = klass.name
    next unless n
    pk = klass.primary_key rescue next
    pk_map[n] = pk if pk
  end
  shareable = Ractor.make_shareable(pk_map)
  _reassign_shareable_const(:AR_PRIMARY_KEYS_SHAREABLE, shareable)
end

._register_for_fallback(mod_name, sym, key, default) ⇒ Object

Register a mattr accessor so _build_shareable_fallback! can capture + make shareable the main-ractor value at prepare_for_ractors! time. The default is stored too so the fallback builder can use it when the live value can't be shared (e.g. __callbacks holds self-capturing Procs).



245
246
247
# File 'lib/ractor_rails_shim/patches/mattr_accessor.rb', line 245

def _register_for_fallback(mod_name, sym, key, default)
  CLASS_ATTRIBUTES << [mod_name, sym, key, default]
end

._register_patch(name, *tested_segments) ⇒ Object

Record that a patch was developed/tested against the given Rails version segments. Delegates to RactorRailsShim::VersionPolicy.register.



277
278
279
# File 'lib/ractor_rails_shim/patches/core.rb', line 277

def _register_patch(name, *tested_segments)
  RactorRailsShim::VersionPolicy.register(name, *tested_segments)
end

._seed_active_storage_prefix!Object

Seed the shareable ActiveStorage table-name prefix/suffix constants from the (main-ractor) ActiveStorage.table_name_prefix value. Must run in the main Ractor at prepare_for_ractors! time, after ActiveStorage is fully loaded. Workers read these constants (never the un-shareable mattr reader).



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/ractor_rails_shim/patches/active_storage.rb', line 418

def _seed_active_storage_prefix!
  return unless defined?(::ActiveStorage) && ::ActiveStorage.respond_to?(:table_name_prefix)
  # The reader was redefined to return the shareable constant, so read the
  # ORIGINAL value from `ActiveStorage`'s `@@table_name_prefix` class
  # variable (the mattr_accessor store) in main — callable from the main
  # Ractor, unlike the un-shareable mattr reader from a worker.
  prefix = if ::ActiveStorage.class_variable_defined?(:@@table_name_prefix)
    ::ActiveStorage.class_variable_get(:@@table_name_prefix).to_s
  else
    "active_storage_"
  end
  suffix = if ::ActiveStorage.class_variable_defined?(:@@table_name_suffix)
    ::ActiveStorage.class_variable_get(:@@table_name_suffix).to_s
  else
    ""
  end
  RactorRailsShim.const_set(:ACTIVE_STORAGE_PREFIX, prefix.freeze)
  RactorRailsShim.const_set(:ACTIVE_STORAGE_SUFFIX, suffix.freeze)
  # Reset the cached `table_name` on ActiveStorage models so they recompute
  # with the correct prefix/suffix. Without this, `ActiveStorage::Blob`'s
  # `table_name` stays `"blobs"` (computed at eager-load with the empty
  # prefix) instead of `"active_storage_blobs"`. Set explicitly to avoid
  # going through `reset_table_name` (which may fail in edge cases).
  if defined?(::ActiveStorage::Blob)
    ::ActiveStorage::Blob.table_name = "#{prefix}blobs#{suffix}"
  end
  if defined?(::ActiveStorage::Attachment)
    ::ActiveStorage::Attachment.table_name = "#{prefix}attachments#{suffix}"
  end
end

._seed_mattr_default(key, default) ⇒ Object

Store the definition-time default for a mattr accessor in the runtime registry. If the default is shareable, also rebuild the SHAREABLE_MATTR_DEFAULTS constant (frozen + made shareable) so worker Ractors can read it before prepare_for_ractors! runs. The constant reassignment goes through _reassign_shareable_const (centralized $VERBOSE suppression).

INVARIANT: this method MUST only be called pre-spawn (i.e. during boot in the main Ractor, before worker Ractors are forked). The rebuild- from-scratch pattern (dup, freeze, make_shareable, const_set) reassigns the SHAREABLE_MATTR_DEFAULTS constant; any holder of a stale reference (e.g. a worker that already read the constant) won't see the update. The codebase is careful to call this only at mattr-definition time (boot), but a future maintainer adding a post-spawn call would silently break workers that cached the old constant. Don't.



230
231
232
233
234
235
236
237
238
239
# File 'lib/ractor_rails_shim/patches/mattr_accessor.rb', line 230

def _seed_mattr_default(key, default)
  MATTR_DEFAULTS[key] = default
  if default && Ractor.shareable?(default)
    h = SHAREABLE_MATTR_DEFAULTS.dup
    h[key] = default
    h.freeze
    Ractor.make_shareable(h)
    _reassign_shareable_const(:SHAREABLE_MATTR_DEFAULTS, h)
  end
end

._share_active_record_internals!Object

ActiveRecord's internal helper classes (the *Clause classes used while building a relation's Arel) cache a frozen "empty" singleton in a class instance variable via @empty ||= new(...).freeze (e.g. ActiveRecord::Relation::WhereClause#empty). Reading that class ivar from a worker Ractor raises Ractor::IsolationError if the value isn't shareable. Fix: warm .empty in the main Ractor (populating @empty with its frozen singleton), then make every class ivar on these helper classes shareable. Idempotent; must run in the main Ractor. ActiveRecord's internal helper classes/modules hold class instance variables that a worker Ractor reads during query building / connection establishment (e.g. ActiveRecord::ConnectionAdapters.@adapters — a Hash of adapter_name => [class_name, path]; the *Clause classes' @empty frozen singleton). A class ivar whose VALUE is shareable IS readable from a worker Ractor (unlike class variables), so we deep-freeze each value in the main Ractor and write it back (Monitor/Mutex->NoOpLock, Concurrent::Map->frozen Hash, etc. via _shareable_ivar_replacement).

TARGETED: only specific leaf registries (not a broad ActiveRecord::* sweep). A broad sweep also freezes AR-railtie initializer Collections reachable from the app graph, which breaks make_app_shareable's proc-replacement (frozen containers can't be mutated to swap Procs).



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 659

def 
  return unless defined?(::ActiveRecord::Base)

  # 1. Warm + freeze the *Clause `.empty` singletons.
  ObjectSpace.each_object(Class) do |c|
    n = c.name
    next unless n && n.start_with?("ActiveRecord::Relation::") &&
                n.end_with?("Clause")
    begin
      c.empty if c.respond_to?(:empty)
    rescue StandardError => e
    end
    _freeze_class_ivars!(c)
  end

  # 2. ConnectionAdapters.@adapters (String => [class_name, path]) — read
  #    by `ConnectionAdapters.resolve` during establish_connection.
  if defined?(::ActiveRecord::ConnectionAdapters)
    _freeze_class_ivars!(::ActiveRecord::ConnectionAdapters)
  end

  # 3. ActiveRecord::Type — holds @default_value (a lazy singleton Value
  #    used as a fallback type). Warm it and freeze the class ivar.
  if defined?(::ActiveRecord::Type)
    ::ActiveRecord::Type.default_value rescue nil
    _freeze_class_ivars!(::ActiveRecord::Type)
  end

  # 4. ActiveModel::Type — holds @default_value and @registry (contains
  #    Procs). Can't freeze the class ivars (Registry has Procs). Route
  #    default_value through IsolatedExecutionState so worker Ractors get
  #    their own lazily-initialized copy.
  if defined?(::ActiveModel::Type)
    _patch_active_model_type_default_value!
  end

  # 5. ActiveRecord::Associations::AssociationScope::INSTANCE is an instance
  #    singleton wrapping an unshareable identity lambda. Worker Ractors
  #    read it on EVERY association scope build (AssociationScope.scope),
  #    so it must be Ractor-shareable. Replace the constant with a
  #    deep-frozen, shareable copy built in the main Ractor — the same
  #    pattern as _freeze_journey_visitors! for Journey's *::INSTANCE.
  if defined?(::ActiveRecord::Associations::AssociationScope) &&
     ::ActiveRecord::Associations::AssociationScope.const_defined?(:INSTANCE)
    inst = ::ActiveRecord::Associations::AssociationScope::INSTANCE
      unless Ractor.shareable?(inst)
        _swallow("make AssociationScope::INSTANCE shareable") do
          # Reassigning a constant that already exists warns ("already
          # initialized constant"); drop the old binding first.
          ::ActiveRecord::Associations::AssociationScope.send(:remove_const, :INSTANCE) if ::ActiveRecord::Associations::AssociationScope.const_defined?(:INSTANCE, false)
          ::ActiveRecord::Associations::AssociationScope.const_set(
            :INSTANCE, Ractor.make_shareable(inst)
          )
        end
    end
  end
end

._share_model_classes!Object

ActiveRecord model classes lazily initialize many class instance variables on first use (e.g. @table_name, @arel_table, @predicate_builder, @columns_hash, @attribute_methods_module) via @ivar ||= compute. The computation is deterministic, but it WRITES the class ivar — which a worker Ractor cannot do (Ractor::IsolationError: "can not set instance variables of classes/modules by non-main Ractors").

Fix: in the main Ractor, warm every model class by running representative queries (count / first / page), which populates all the lazy class ivars with their shareable-or-not values. Then make every class ivar's VALUE shareable (deep-freeze via Ractor.make_shareable) and write it back while the class is still mutable in main. A class ivar holding a shareable value is readable from a worker Ractor, and the worker's ||= short-circuits (no write). Idempotent; must run in the main Ractor after eager_load.



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 404

def 
  return unless defined?(::ActiveRecord::Base)

  # Suppress reload_schema_from_cache for the rest of the main Ractor's
  # lifecycle. During warming, abstract classes (ActiveRecord::Base,
  # ApplicationRecord) trigger reload_schema_from_cache which recursively
  # resets @schema_loaded = false and @columns_hash = nil on all
  # descendants, destroying schema data that workers need. After
  # make_app_shareable! freezes the graph, no more reloads should happen
  # in main anyway.
  RactorRailsShim.instance_variable_set(:@_rrs_schema_warming, true)

  # Force load_schema on all concrete models BEFORE freezing any
  # reflections. load_schema! writes to reflection objects (e.g.
  # CounterCache writes @counter_cache_column), so it must run before
  # _share_model_classes! freezes them via Ractor.make_shareable.
  classes = [::ActiveRecord::Base]
  classes.concat(::ActiveRecord::Base.descendants) rescue nil
  classes.each do |klass|
    next if klass.respond_to?(:abstract_class?) && klass.abstract_class?
    begin
      klass.send(:load_schema) if klass.respond_to?(:load_schema, true)
    rescue StandardError
      nil
    end
  end

  classes = [::ActiveRecord::Base]
  classes.concat(::ActiveRecord::Base.descendants) rescue nil
  classes.each do |klass|
    # Warm the class's lazy ivars by actually exercising the query paths
    # the workers will hit. Main has a working connection handler, so this
    # populates exactly the ivars a real query touches.
    # Warm the class's lazy ivars by actually exercising the query paths
    # the workers will hit. Main has a working connection handler, so this
    # populates exactly the ivars a real query touches. Each call is
    # isolated: a failure in one must not skip the rest (e.g. the private
    # `relation` method or a cold connection must not prevent `table_name`
    # from being set).
    warm_calls = [
      -> { klass.connection_pool if klass.respond_to?(:connection_pool) },
      -> { klass.send(:reset_primary_key) if klass.respond_to?(:reset_primary_key, true) },
      -> { klass.table_name },
      -> { klass.arel_table },
      -> { klass.columns_hash },
      -> { klass.attribute_names },
      -> { klass.attribute_types },
      -> { klass.predicate_builder },
      -> { klass.defined_enums if klass.respond_to?(:defined_enums) },
      -> { klass.send(:relation) if klass.respond_to?(:relation, true) },
      -> { klass.count rescue nil },
      -> { klass.first rescue nil },
      -> { klass.send(:query_constraints_list) if klass.respond_to?(:query_constraints_list, true) },
      -> { if defined?(::Kaminari) && klass.respond_to?(:page)
             klass.page(1).to_a rescue nil
           end },
    ]
    warm_calls.each { |c| begin; c.call; rescue StandardError => e; end }

    # Warm reflection objects. Reflections are part of the model class
    # graph and get deep-frozen by make_app_shareable!. Their lazy caches
    # (@class_name, @klass, @inverse_name, @scope, etc.) memoize via ||=
    # which writes to a frozen object from a worker -> FrozenError. Warm
    # them here in main so the frozen copies hold the values.
    begin
      if klass.respond_to?(:reflect_on_all_associations, true)
        klass.reflect_on_all_associations.each do |refl|
          begin
            refl.class_name rescue nil
            refl.klass rescue nil
            refl.inverse_name rescue nil
            refl.inverse_of rescue nil
            refl.active_record rescue nil
            refl.plural_name rescue nil
            refl.options rescue nil
            refl.macro rescue nil
            refl.scope rescue nil if refl.respond_to?(:scope)
            refl.check_validity! rescue nil
            refl.automatic_inverse_of rescue nil if refl.respond_to?(:automatic_inverse_of)
          rescue StandardError
            nil
          end
        end
      end
    rescue StandardError
      nil
    end

    # Make every class ivar shareable and write it back. The class is
    # still mutable here (in main), so the write is allowed.
    begin
      klass.instance_variables.each do |iv|
        v = klass.instance_variable_get(iv) rescue nil
        next unless v
        next if Ractor.shareable?(v)
        replacement = _shareable_ivar_replacement(v)
        next unless replacement
        begin
          klass.instance_variable_set(iv, replacement)
        rescue StandardError => e
          # frozen owner — leave as-is
        end
      end
    rescue StandardError => e
      # BasicObject / frozen owners
    end

  end

  # Build the per-model shareable snapshots (primary keys + pending
  # attribute modifications) workers read instead of un-shareable class
  # ivars. Delegated to _rebuild_activerecord_model_snapshots! so it can
  # be called again post-eager-load (at make_app_shareable! time) once
  # ActiveRecord::Base.descendants holds the app's models.
  _capture_dependent_associations!
  _rebuild_activerecord_model_snapshots!
end

._share_relation_delegate_caches!Object

Make every loaded AR model class's @relation_delegate_cache shareable. Idempotent; must run in the main Ractor after eager_load so that all model classes (and their caches) exist.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 371

def 
  return unless defined?(::ActiveRecord::Base)
  classes = [::ActiveRecord::Base]
  classes.concat(::ActiveRecord::Base.descendants) rescue nil
  classes.each do |klass|
    cache = klass.instance_variable_get(:@relation_delegate_cache) rescue nil
    next unless cache
    next if Ractor.shareable?(cache)
    begin
      klass.instance_variable_set(:@relation_delegate_cache,
        Ractor.make_shareable(cache))
    rescue StandardError => e
      # Best-effort: if a cache holds an unshareable delegate class we
      # can't freeze, skip it. The worker will then hit a clear error on
      # the first relation method and we can patch that class specifically.
    end
  end
end

._shareable_ivar_replacement(v) ⇒ Object

  • Monitor/Mutex -> NoOpLock (never contended post-boot)
  • Concurrent::Map -> frozen Hash
  • else -> Ractor.make_shareable; if that fails (statement caches etc.), a frozen empty container of the same kind so the worker reads a shareable value (cold cache in workers; slower, correct). Returns nil if no replacement can be made.


614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 614

def _shareable_ivar_replacement(v)
  if v.is_a?(::Monitor) || v.is_a?(::Mutex)
    Ractor.make_shareable(NoOpLock.new)
  elsif defined?(::Concurrent::Map) && v.is_a?(::Concurrent::Map)
    h = {}
    begin
      v.each_pair { |k, val| h[k] = val }
    rescue StandardError => e
    end
    Ractor.make_shareable(h)
  else
    begin
      Ractor.make_shareable(v)
    rescue StandardError => e
      case v
      when ::Hash then Ractor.make_shareable({})
      when ::Array then Ractor.make_shareable([])
      when ::Set then Ractor.make_shareable(::Set.new)
      else nil
      end
    end
  end
end

._swallow(label) ⇒ Object

Swallow an exception raised by the block, optionally logging it when debug? is on. Used by freeze/shareability paths where individual failures are expected (some ivars hold intrinsically unshareable values like Procs) but a worker crash on the same value later has no visible cause. label identifies the call site (e.g. "freeze AR ivar Post@column_defaults"). Keep the label short — it's only for grepping. Delegates to RactorRailsShim::Funnel.swallow (extracted Issue #31, step 31.1d).



107
108
109
# File 'lib/ractor_rails_shim/patches/core.rb', line 107

def _swallow(label)
  Funnel.swallow(label) { yield }
end

._version_mismatch(message) ⇒ Object

Apply the configured policy to a mismatch message. Delegates to RactorRailsShim::VersionPolicy.mismatch.



263
264
265
# File 'lib/ractor_rails_shim/patches/core.rb', line 263

def _version_mismatch(message)
  RactorRailsShim::VersionPolicy.mismatch(message)
end

._warm_journey_routes!Object

Warm + cache @ast / @simulator on the live Routes graph. Called from make_app_shareable! AFTER the route precompute (which resets the routes) and BEFORE Ractor.make_shareable freezes the graph. Must run in the main Ractor. Idempotent (caches on the mutable object, then frozen in place).



740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 740

def _warm_journey_routes!
  return unless Ractor.main?
  _freeze_journey_visitors!
  _freeze_mime_negotiation!
  begin
    rs = ::Rails.application.routes rescue nil
    # Navigate to the ActionDispatch::Journey::Routes object — the one that
    # is frozen into the shared graph and read (via Router#simulator) on
    # every request. Rails wraps it in a RouteSet (and sometimes a
    # LazyRouteSet), neither of which defines #simulator, so calling
    # `rs.routes.simulator` would silently NoMethodError and leave @simulator
    # uncached — forcing worker Ractors to rebuild the simulator (and hit
    # Ractor::IsolationError on GTG constants). Descend through #routes until
    # we reach the Journey::Routes instance.
    routes = rs
    while routes.respond_to?(:routes) && !routes.is_a?(::ActionDispatch::Journey::Routes)
      nxt = routes.routes
      break if nxt.equal?(routes)
      routes = nxt
    end
    if routes.is_a?(::ActionDispatch::Journey::Routes)
      routes.ast
      routes.simulator
      _warm_path_patterns!(routes)
    end
  rescue StandardError => e
    # best-effort
  end
end

._warm_path_patterns!(routes) ⇒ Object

Pre-compute every Journey::Path::Pattern's lazy memoized ivars (@required_names, @optional_names, @offsets, @re, @requirements_for_missing_keys_check) on the LIVE (unfrozen) pattern, before Ractor.make_shareable freezes the graph. Path::Pattern#match (called on every request during route recognition) memoizes @offsets via @offsets ||= ...; on a frozen pattern that write raises FrozenError. By computing it now (and caching into the frozen object), the worker reads the cached value and never writes. We deliberately do NOT call the built-in eager_load!, which sets @ast = nil (the @ast/@spec are still read by requirements_anchored? and must survive).



718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'lib/ractor_rails_shim/patches/action_dispatch.rb', line 718

def _warm_path_patterns!(routes)
  return unless routes.respond_to?(:routes)
  routes.routes.each do |r|
    p = r.respond_to?(:path) ? r.path : nil
    next unless p
    begin
      p.required_names
      p.optional_names
      p.send(:offsets)
      p.to_regexp
      p.requirements_for_missing_keys_check if p.respond_to?(:requirements_for_missing_keys_check)
    rescue StandardError
      # best-effort — a pattern we can't warm will fall back to its own
      # (non-frozen) copy if one exists; ignore unusual shapes.
    end
  end
end

.applicable_patchesObject

Report which registered patches apply to the runtime Rails version (and which were skipped because they're untested on it). Useful for diagnostics and CI. Returns a Hash: { applied: [...], skipped: [...] }. Delegates to RactorRailsShim::VersionPolicy.applicable.



271
272
273
# File 'lib/ractor_rails_shim/patches/core.rb', line 271

def applicable_patches
  RactorRailsShim::VersionPolicy.applicable
end

.autoload_install!Object

Require this gem and the patches auto-install IF Rails is loaded. If Rails isn't loaded yet, install is deferred to the first call of install (call it from config/boot.rb before Rails.application).



24
25
26
# File 'lib/ractor_rails_shim.rb', line 24

def autoload_install!
  install if defined?(::Rails) && !installed?
end

.capture_app_constants!Object

Capture a frozen name -> object map for every constant the application's Zeitwerk loaders manage. Runs in the MAIN Ractor, after eager load, where all app constants are defined. The map travels to worker Ractors, which use it to (re)bind the constant names into their own namespaces.

Why this is needed: a Ractor boundary does NOT share top-level constant names — only the class/module objects reachable from the frozen shared app graph cross the boundary. A worker Ractor therefore sees RactorRailsShim, ActiveRecord, ApplicationRecord, the controllers, etc. (objects reachable from the app), but NOT the application's own model constants (e.g. Post): the object is in the graph, but its name is not bound in the worker, so PostsController#index's Post reference raises NameError. Rebinding the captured names fixes it without re-running autoloading (which is itself impossible in a worker, since Zeitwerk::Loader.new raises IsolationError off the main Ractor). Capture a frozen name -> object map for every constant the application's Zeitwerk loaders manage. Delegates to WorkerAppFactory.capture_constants! (extracted Issue #13, Step 13.4; WorkerApp moved to ractor_rails_shim/worker_app.rb). See WorkerAppFactory for the contract (Zeitwerk introspection, the non-Zeitwerk guard, and why the captured map is needed for worker constant rebinding).



303
304
305
# File 'lib/ractor_rails_shim/patches/core.rb', line 303

def capture_app_constants!
  WorkerAppFactory.capture_constants!
end

.debug=(value) ⇒ Object



95
96
97
# File 'lib/ractor_rails_shim/patches/core.rb', line 95

def debug=(value)
  Funnel.debug = value
end

.debug?Boolean

When true, swallowed exceptions in freeze/shareability paths are reported to $stderr so a worker Ractor that later crashes on an unshareable value has a traceable cause. Default false (silent, the historical behaviour). Enable for diagnosis:

RactorRailsShim.debug = true

Delegates to RactorRailsShim::Funnel (extracted Issue #31, step 31.1d).

Returns:

  • (Boolean)


91
92
93
# File 'lib/ractor_rails_shim/patches/core.rb', line 91

def debug?
  Funnel.debug?
end

.fix_url_helpers_singleton_routes!Object

The module-level singleton _routes (set via redefine_singleton_method in the gem's included block) is still a define_method block. It is not on the request hot-path, but make it shareable anyway. Called after the module has been included into ActionView/ActionController (i.e. from prepare_for_ractors!).



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/ractor_rails_shim/patches/route_helpers.rb', line 136

def fix_url_helpers_singleton_routes!
  return unless defined?(::Rails) && ::Rails.application
  return unless ::Rails.application.routes.respond_to?(:url_helpers)
  mod = ::Rails.application.routes.url_helpers
  return unless mod.respond_to?(:singleton_class)
  mod.singleton_class.class_eval do
    def _routes
      ::Rails.application.routes
    end
  end
rescue StandardError
end

.init_worker_ar_connections!Object

Worker-Ractor hook: create a fresh ConnectionHandler and establish connections from the captured configurations snapshot. Call this in each worker Ractor before serving requests:

Ractor.new(app) do |a|
RactorRailsShim.init_worker_ar_connections!
a.call(env)
end

Idempotent: safe to call multiple times (subsequent calls are no-ops once the handler is established). Uses Ractor.store_if_absent semantics via IES.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 236

def init_worker_ar_connections!
  return if Ractor.main?
  return unless defined?(::ActiveRecord::Base)

  key = :active_record_connection_handler
  # Store the handler in Ractor-local storage (Ractor.current), NOT in
  # the IES-backed RactorRailsShim.storage. IES is per-THREAD (it is a
  # Thread.attr_accessor), so a value set on the init thread is invisible
  # to the other worker threads in the same worker Ractor ->
  # ConnectionNotEstablished ("No connection handler for Ractor X").
  # Ractor.current storage is per-Ractor and shared by every thread of the
  # worker, so connection_handler resolves the same handler for all threads.
  existing = Ractor.current[key]
  return if existing

  # Establish a fresh, per-Ractor connection handler + pool from the
  # captured configurations snapshot. We call ConnectionHandler#establish_connection
  # DIRECTLY (not ActiveRecord::Base.establish_connection, which writes
  # the `@resolved_config` class ivar from the worker -> IsolationError).
  # ConnectionHandler#establish_connection reads ActiveRecord::Base.configurations
  # (now IES-routed by _install_activerecord_configurations_patch) inside
  # resolve_pool_config, so it works from a worker. Best-effort per config.
  snapshot = AR_CONFIGURATIONS_SNAPSHOT
  if snapshot && !snapshot.empty?
    env = ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
    env_configs = snapshot[env] || snapshot.values.first || {}

    handler = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new
    env_configs.each do |_name, config|
      begin
        handler.establish_connection(config,
          owner_name: ::ActiveRecord::Base,
          role: ::ActiveRecord::Base.current_role || :writing,
          shard: ::ActiveRecord::Base.current_shard || :default)
      rescue StandardError => e
        # Best-effort: if one connection fails, continue with others.
      end
    end

    Ractor.current[key] = handler
  end
end

.installObject

Install all the patches. Safe to call multiple times (idempotent). Delegates to Installer.install (extracted Issue #13, Step 13.6). See Installer for the orchestration contract (version check, run-mode resolve, branch by mode, early-boot install_* calls).



183
184
185
# File 'lib/ractor_rails_shim/patches/core.rb', line 183

def install
  Installer.install
end

.install_class_attributeObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ractor_rails_shim/patches/class_attribute.rb', line 33

def install_class_attribute
  _register_patch :class_attribute, "8.1"
  return if @class_attr_patched
  @class_attr_patched = true
  if defined?(::ActiveSupport::ClassAttribute)
    patch_class_attribute!
  else
    # Defer until ActiveSupport::ClassAttribute loads. A TracePoint(:class)
    # fires when `module ClassAttribute` opens. One-shot.
    @ca_tp = TracePoint.new(:class) do |trace|
      if defined?(::ActiveSupport::ClassAttribute) && !@ca_patched
        @ca_tp.disable
        patch_class_attribute!
      end
    end
    @ca_tp.enable
  end
end

.install_execution_wrapperObject



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/ractor_rails_shim/patches/execution_wrapper.rb', line 17

def install_execution_wrapper
  return if @exec_wrapper_patched
  @exec_wrapper_patched = true
  _register_patch :execution_wrapper, "8.1"
  if defined?(::ActiveSupport::ExecutionWrapper)
    patch_execution_wrapper!
  else
    @ew_tp = TracePoint.new(:class) do |trace|
      if defined?(::ActiveSupport::ExecutionWrapper) && !@exec_wrapper_registry_patched
        @ew_tp.disable
        patch_execution_wrapper!
      end
    end
    @ew_tp.enable
  end
end

.install_mattr_accessorObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/ractor_rails_shim/patches/mattr_accessor.rb', line 9

def install_mattr_accessor
  _register_patch :mattr_accessor, "8.1"
  return if @mattr_patched
  @mattr_patched = true

  ::Module.prepend(Module.new {
    # The prepended module's body is evaluated in the main ractor at
    # prepend time; the methods it defines are callable from any ractor
    # because they're defined via string eval (no captured binding).
    # But mattr_accessor itself runs at app boot in the main ractor, and
    # the per-accessor redefinition must also use string eval.
    #
    # IMPORTANT: Rails' mattr_accessor/cattr_accessor stores values in
    # CLASS VARIABLES (@@sym), not class instance variables (@sym). The
    # default value is written via class_variable_set("@@sym", default).
    # Class variables are also subject to Ractor::IsolationError from
    # non-main ractors (verified on Ruby 4.0.5), so we route through IES
    # the same way — but the main-ractor fallback must read @@sym, and
    # the seed must run in the main ractor at define-time (via super).
    def mattr_accessor(*syms, instance_reader: true, instance_writer: true,
                       instance_accessor: true, default: nil, **kwargs, &block)
      shareable = kwargs[:shareable]
      mod_name = name

      # Compute the default value the same way Rails does, so we can
      # seed worker-ractor IES slots with it (workers can't read @@sym).
      # The block form is evaluated once here (in main ractor) like Rails.
      sym_default = block_given? && default.nil? ? yield : default

      super # define the methods via the original path (sets @@sym)

      syms.each do |sym|
        key = :"ractor_rails_shim_mattr_#{mod_name}_#{sym}"
        key_str = key.inspect
        cv = "@@#{sym}"
        cv_str = cv.inspect

        # Register so _build_shareable_fallback! can capture the main-ractor
        # value (read from @@sym) at prepare_for_ractors! time. The default
        # is stored too so the fallback builder can use it when the live
        # value can't be shared.
        RactorRailsShim._register_for_fallback(mod_name, sym, key, sym_default)

        # Store the default in a runtime registry (NOT inlined into the
        # eval'd method body — arbitrary objects like Logger have invalid
        # `.inspect` output). The reader looks it up by key. Also rebuilds
        # the shareable subset constant if the default is shareable.
        RactorRailsShim._seed_mattr_default(key, sym_default)

        # Redefine the class reader via string eval (no captured binding).
        # Class variables are only touched from the main ractor; worker
        # ractors fall back to SHAREABLE_FALLBACK (built from main's @@sym
        # at prepare_for_ractors! time) when their own IES slot is empty.
        # NOTE: we deliberately do NOT inline the default value here —
        # arbitrary objects (e.g. Logger) have invalid `.inspect` output.
        # The fallback builder captures the live value (which may equal
        # the default) at prepare time.
        singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{sym}
            v = RactorRailsShim.storage[#{key_str}]
            return v if RactorRailsShim.storage.key?(#{key_str})

            if #{!!shareable}
              if class_variable_defined?(#{cv_str})
                class_variable_get(#{cv_str})
              end
            elsif Ractor.main?
              if class_variable_defined?(#{cv_str})
                class_variable_get(#{cv_str})
              else
                nil
              end
            else
              # Worker: try the shareable fallback (built from main's @@sym
              # at prepare_for_ractors! time). If empty, try the
              # definition-time default (only the shareable subset — the
              # full MATTR_DEFAULTS holds unshareable defaults like Logger
              # which workers can't read via the constant).
              fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
              return fb unless fb.nil?
              RactorRailsShim::SHAREABLE_MATTR_DEFAULTS[#{key_str}]
            end
          end

          def #{sym}=(val)
            RactorRailsShim.storage[#{key_str}] = val
            class_variable_set(#{cv_str}, val) if Ractor.main?
            val
          end
        RUBY

        # Instance readers/writers route through IES directly (NOT
        # self.class.#{sym}). Rails' original uses @@sym (a class variable
        # inherited by including classes); the shim routes through IES,
        # so the instance reader must also use IES. Using self.class.sym
        # would fail for mattr_accessor on Modules (e.g.
        # ActionView::Helpers::FormHelper#form_with_generates_ids):
        # self.class is the including class (ActionView::Base), which
        # doesn't have the module's singleton method.
        # Only redefine if instance_accessor is on (matches Rails).
        if instance_reader && instance_accessor
          module_eval <<-RUBY, __FILE__, __LINE__ + 1
            def #{sym}
              v = RactorRailsShim.storage[#{key_str}]
              return v if RactorRailsShim.storage.key?(#{key_str})
              if Ractor.main?
                self.class.class_variable_defined?(#{cv_str}) ? self.class.class_variable_get(#{cv_str}) : nil
              else
                RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
              end
            end
          RUBY
        end
        if instance_writer && instance_accessor
          module_eval <<-RUBY, __FILE__, __LINE__ + 1
            def #{sym}=(val)
              RactorRailsShim.storage[#{key_str}] = val
              self.class.class_variable_set(#{cv_str}, val) if Ractor.main? && self.class.class_variable_defined?(#{cv_str})
              val
            end
          RUBY
        end
      end
    end

    # mattr_reader declares read-only class/module attributes that Rails
    # generates as `def self.<sym>; @@<sym>; end`. The raw `@@<sym>` class
    # variable is unreadable from a non-main Ractor, raising
    # Ractor::IsolationError. Route the reader through IES (mirroring
    # mattr_accessor) so workers fall back to SHAREABLE_FALLBACK (built from
    # the main-ractor `@@<sym>` at prepare_for_ractors! time). There is no
    # writer, so only the reader is redefined. ActiveRecord::Encryption uses
    # `mattr_reader :config` for its encryption Config — without this patch
    # every worker hits the class-variable IsolationError when loading a
    # model's schema.
    def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
      mod_name = name
      sym_default = block_given? && default.nil? ? yield : default

      super # defines the original `def self.<sym>; @@<sym>; end` readers
            # and seeds @@<sym> with sym_default (via class_variable_set).

      syms.each do |sym|
        key = :"ractor_rails_shim_mattr_#{mod_name}_#{sym}"
        key_str = key.inspect
        cv = "@@#{sym}"
        cv_str = cv.inspect

        RactorRailsShim._register_for_fallback(mod_name, sym, key, sym_default)

        singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{sym}
            v = RactorRailsShim.storage[#{key_str}]
            return v if RactorRailsShim.storage.key?(#{key_str})
            if Ractor.main?
              if class_variable_defined?(#{cv_str})
                class_variable_get(#{cv_str})
              else
                nil
              end
            else
              fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
              return fb unless fb.nil?
              RactorRailsShim::SHAREABLE_MATTR_DEFAULTS[#{key_str}]
            end
          end
        RUBY

        if instance_reader && instance_accessor
          module_eval <<-RUBY, __FILE__, __LINE__ + 1
            def #{sym}
              v = RactorRailsShim.storage[#{key_str}]
              return v if RactorRailsShim.storage.key?(#{key_str})
              if Ractor.main?
                self.class.class_variable_defined?(#{cv_str}) ? self.class.class_variable_get(#{cv_str}) : nil
              else
                RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
              end
            end
          RUBY
        end
      end
    end

    # cattr_accessor is an alias for mattr_accessor in Rails; route it too.
    # Define the alias only when it isn't already present (Rails defines it
    # as `alias_method :cattr_accessor, :mattr_accessor`, which already
    # routes to the prepended mattr_accessor). Defining it ourselves when
    # missing keeps cattr_accessor working in environments (e.g. a bare
    # test suite) where Rails' core_ext hasn't defined it yet.
    unless method_defined?(:cattr_accessor, true)
      def cattr_accessor(*args, **kwargs, &block)
        mattr_accessor(*args, **kwargs, &block)
      end
    end

    # cattr_reader is an alias for mattr_reader in Rails; route it too.
    # Same logic as cattr_accessor above.
    unless method_defined?(:cattr_reader, true)
      def cattr_reader(*args, **kwargs, &block)
        mattr_reader(*args, **kwargs, &block)
      end
    end
  })
end

.install_rails_load_hookObject

Defer the Rails-module patch until Rails is defined. A TracePoint on :class fires when rails.rb opens module Rails (module bodies fire as :class); once the constant is assigned, we patch once and disable the hook.

Two flags are intentional and guard different things:

@rails_load_hook_installed — the TracePoint one-shot is armed (so we
don't stack multiple TracePoints on repeated `install` calls).
@rails_module_patched — the patch itself has been applied (checked
again inside the TracePoint block and in `patch_rails_module!`
because the immediate-path caller also goes through that method).


46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/ractor_rails_shim/patches/rails_module.rb', line 46

def install_rails_load_hook
  return if @rails_load_hook_installed
  @rails_load_hook_installed = true

  @rails_tp = TracePoint.new(:class) do |trace|
    if defined?(::Rails) && !@rails_module_patched
      @rails_tp.disable
      patch_rails_module!(::Rails)
    end
  end
  @rails_tp.enable
end

.install_rails_moduleObject

Patch the Rails module's class-level accessors (Rails.application, Rails.env, Rails.cache, etc.) to route through IsolatedExecutionState. Defers via a load hook if Rails isn't defined yet (the config/boot.rb case).



26
27
28
29
30
31
32
33
# File 'lib/ractor_rails_shim/patches/rails_module.rb', line 26

def install_rails_module
  _register_patch :rails_module, "8.1"
  if defined?(::Rails)
    patch_rails_module!(::Rails)
  else
    install_rails_load_hook
  end
end

.install_route_helpers_patchObject



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ractor_rails_shim/patches/route_helpers.rb', line 30

def install_route_helpers_patch
  return if @route_helpers_patched
  @route_helpers_patched = true
  _register_patch :route_helpers, "8.1"
  return unless defined?(::ActionDispatch::Routing::RouteSet)

  rs = ::ActionDispatch::Routing::RouteSet

  # 1. Replace the PATH / UNKNOWN lambdas with shareable Method objects.
  begin
    rs.send(:remove_const, :PATH) if rs.const_defined?(:PATH, false)
  rescue StandardError
  end
  rs.const_set(:PATH, ::ActionDispatch::Http::URL.method(:path_for)) rescue nil
  begin
    rs.send(:remove_const, :UNKNOWN) if rs.const_defined?(:UNKNOWN, false)
  rescue StandardError
  end
  rs.const_set(:UNKNOWN, ::ActionDispatch::Http::URL.method(:url_for)) rescue nil

  # 2. Named route helpers -> compiled `def` (regular `resources` routes).
  rs.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def define_url_helper(mod, name, helper, url_strategy)
      const_name = :"RRS_HELPER_\#{name}"
      # Reassigning a constant that already exists warns ("already
      # initialized constant"); drop the old binding first so re-draws of
      # the same route stay silent.
      mod.send(:remove_const, const_name) if mod.const_defined?(const_name, false)
      mod.const_set(const_name, helper)
      strategy = url_strategy.equal?(PATH) ? :PATH : :UNKNOWN
      body = "def " + name.to_s + "(*args)\\n" \
             "  last = args.last\\n" \
             "  options = case last\\n" \
             "    when Hash then args.pop\\n" \
             "    when ActionController::Parameters then args.pop.to_h\\n" \
             "  end\\n" \
             "  ::ActionDispatch::Routing::RouteSet.const_get(" + const_name.inspect + ").call(self, " + name.inspect + ", args, options, ::ActionDispatch::Routing::RouteSet.const_get(" + strategy.inspect + "))\\n" \
             "end"
      mod.module_eval(body, __FILE__, __LINE__ + 1)
    end
  RUBY

  # 3. `direct` / `resolve` helpers -> compiled `def`.
  if defined?(::ActionDispatch::Routing::RouteSet::NamedRouteCollection)
    ::ActionDispatch::Routing::RouteSet::NamedRouteCollection.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def add_url_helper(name, defaults, &block)
        helper = ::ActionDispatch::Routing::RouteSet::CustomUrlHelper.new(name, defaults, &block)
        path_name = :"\#{name}_path"
        url_name  = :"\#{name}_url"
        # Reassigning a constant that already exists warns ("already
        # initialized constant"); drop the old binding first so re-draws of
        # the same helper stay silent.
        @path_helpers_module.send(:remove_const, :"RRS_HELPER_\#{path_name}") if @path_helpers_module.const_defined?(:"RRS_HELPER_\#{path_name}", false)
        @path_helpers_module.const_set(:"RRS_HELPER_\#{path_name}", helper)
        @url_helpers_module.send(:remove_const, :"RRS_HELPER_\#{url_name}") if @url_helpers_module.const_defined?(:"RRS_HELPER_\#{url_name}", false)
        @url_helpers_module.const_set(:"RRS_HELPER_\#{url_name}", helper)
        pbody = "def " + path_name.to_s + "(*args)\\n  const_get(:\\\"RRS_HELPER_" + path_name.to_s + "\\\").call(self, args, true)\\nend"
        ubody = "def " + url_name.to_s + "(*args)\\n  const_get(:\\\"RRS_HELPER_" + url_name.to_s + "\\\").call(self, args, false)\\nend"
        @path_helpers_module.module_eval(pbody, __FILE__, __LINE__ + 1)
        @url_helpers_module.module_eval(ubody, __FILE__, __LINE__ + 1)
        @path_helpers << path_name
        @url_helpers  << url_name
        self
      end
    RUBY
  end

  # 4. `_routes` / `_generate_paths_by_default` -> compiled `def`.
  rs.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    alias_method :_rrs_orig_generate_url_helpers, :generate_url_helpers
    def generate_url_helpers(supports_path)
      routes = self
      mod = _rrs_orig_generate_url_helpers(supports_path)
      mod.module_eval("def _routes\n  @_routes || ::Rails.application.routes\nend", __FILE__, __LINE__ + 1)
      mod.module_eval("def _generate_paths_by_default\n  " + supports_path.inspect + "\nend", __FILE__, __LINE__ + 1)

      # Bound the stock `self.included` reinclude (actionpack
      # action_dispatch/routing/route_set.rb). That hook re-dups the module
      # and re-includes it while `!base._routes.equal?(@_proxy._routes)`.
      # Under the frozen, Ractor-shareable app graph a worker Ractor's
      # controller can report `_routes` as nil, so the equality never holds
      # and the reinclude loops forever (SystemStackError on the very first
      # request, e.g. GET /up -> Rails::HealthController). The intent is to
      # re-align `_routes` per base exactly once; bound it to one reinclude
      # per base so the loop can never happen while preserving that intent.
      seen = ::Set.new
      mod.instance_variable_set(:@_rrs_reinclude_seen, seen)
      mod.singleton_class.class_eval do
        alias_method :_rrs_orig_self_included, :included
        def included(base)
          seen = instance_variable_get(:@_rrs_reinclude_seen)
          return if seen.include?(base.object_id)
          seen << base.object_id
          _rrs_orig_self_included(base)
        end
      end
      mod
    end
  RUBY
end

.install_rubygemsObject

Installed at install (boot) time. Only redefines the reader; the snapshot is filled in later by snapshot_gem_paths!.



21
22
23
24
25
26
27
# File 'lib/ractor_rails_shim/patches/rubygems.rb', line 21

def install_rubygems
  return if @rubygems_patched
  @rubygems_patched = true
  _register_patch :rubygems, "all"
  return unless defined?(::Gem)
  patch_rubygems!
end

.install_url_helpers_patchObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ractor_rails_shim/patches/url_helpers.rb', line 27

def install_url_helpers_patch
  return if @url_helpers_patched
  @url_helpers_patched = true
  _register_patch :url_helpers, "8.1"
  return unless defined?(::ActiveRecord::Base)

  if defined?(::ActionView::RoutingUrlFor)
    ::ActionView::RoutingUrlFor.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      alias_method :_rrs_orig_view_url_for, :url_for
      def url_for(options = nil)
        _rrs_orig_view_url_for(options)
      rescue StandardError => e
        raise unless e.message.include?("un-shareable Proc")
        if options.is_a?(Hash) || options.is_a?(ActionController::Parameters)
          full_url_for(options)
        else
          meth = _generate_paths_by_default ? :path : :url
          builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.public_send(meth)
          builder.handle_model_call(self, options)
        end
      end
    RUBY
  end

  if defined?(::ActionDispatch::Routing::UrlFor)
    ::ActionDispatch::Routing::UrlFor.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      alias_method :_rrs_orig_full_url_for, :full_url_for
      def full_url_for(options = nil)
        _rrs_orig_full_url_for(options)
      rescue StandardError => e
        raise unless e.message.include?("un-shareable Proc")
        if options.is_a?(Hash) || options.is_a?(ActionController::Parameters)
          route_name = options.delete :use_route
          merged = options.to_h.symbolize_keys.reverse_merge!(url_options)
          # `self._routes` is an un-shareable `define_method` block in a
          # worker Ractor, so calling it re-raises the same error. Route the
          # fallback through the worker-safe shareable RouteSet instead.
          if RactorRailsShim.const_defined?(:SHAREABLE_ROUTES)
            RactorRailsShim::SHAREABLE_ROUTES.url_for(merged, route_name)
          else
            _routes.url_for(merged, route_name)
          end
        else
          builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url
          builder.handle_model_call(self, options)
        end
      end
    RUBY
  end
end

.install_zeitwerk_registryObject



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/ractor_rails_shim/patches/zeitwerk_registry.rb', line 17

def install_zeitwerk_registry
  return if @zeitwerk_patched
  @zeitwerk_patched = true
  _register_patch :zeitwerk_registry, "8.1"
  if defined?(::Zeitwerk::Registry)
    patch_zeitwerk_registry!
  else
    # Defer until Zeitwerk loads. A TracePoint(:class) fires when
    # `module Registry` opens. One-shot.
    #
    # Two flags guard different things (mirrors `rails_module.rb`):
    #   @zeitwerk_patched          — `install_zeitwerk_registry` ran
    #                                 (so we don't arm a second TracePoint).
    #   @zeitwerk_registry_patched — the actual patch was applied (checked
    #                                 again inside the TracePoint block and
    #                                 in `patch_zeitwerk_registry!`).
    @zw_tp = TracePoint.new(:class) do |trace|
      if defined?(::Zeitwerk::Registry) && !@zeitwerk_registry_patched
        @zw_tp.disable
        patch_zeitwerk_registry!
      end
    end
    @zw_tp.enable
  end
end

.installed?Boolean

Returns:

  • (Boolean)


187
188
189
# File 'lib/ractor_rails_shim/patches/core.rb', line 187

def installed?
  Installer.installed?
end

.make_app_shareable!(app) ⇒ Object

Public API: make Rails.application shareable across Ractors. Delegates to RactorRailsShim::AppShareabilizer.make_shareable!(app) (extracted Step 22.6, Issue #22). See AppShareabilizer for the full pipeline contract (precompute → freeze ivars → warm routes → neutralize logger → replace procs → replace locks → make_shareable → build fallback).

WARNING: this MUTATES the app object graph in place (replaces ivars). The app becomes read-only (frozen). Do NOT call if you intend to keep mutating the app (e.g. development reloading). Production-only.

Returns the shareable app. Raises on failure (e.g. if a Proc can't be replaced — add the missing constant to shareable_constants first).



52
53
54
# File 'lib/ractor_rails_shim/patches/make_shareable.rb', line 52

def make_app_shareable!(app)
  AppShareabilizer.make_shareable!(app)
end

.make_constant_shareable!(const_path) ⇒ Object

Resolve a constant path string to a value, and if it exists and is not already shareable, replace it with its shareable (deep-frozen) version. Returns true if the constant was made shareable (or already was); false if it doesn't exist yet (caller may retry). Delegates to ConstantShareabilizer.make_shareable! (extracted Issue #13, Step 13.1).



212
213
214
# File 'lib/ractor_rails_shim/patches/core.rb', line 212

def make_constant_shareable!(const_path)
  ConstantShareabilizer.make_shareable!(const_path)
end

.patch_class_attribute!Object

The actual patch. Idempotent. Must run in the main Ractor. redefine is a singleton method on ClassAttribute (defined in class << self), so we prepend onto the singleton class.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/ractor_rails_shim/patches/class_attribute.rb', line 55

def patch_class_attribute!
  return if @ca_patched
  @ca_patched = true
  ::ActiveSupport::ClassAttribute.singleton_class.prepend(Module.new {
    # redefine is called once per attribute at class_attribute-definition
    # time (in the main Ractor). The original defines methods with blocks;
    # we replace with string-eval'd methods that route through IES so
    # they're callable from any Ractor. The default value is seeded into
    # the main Ractor's IES slot immediately (matching original semantics
    # where the reader returns the default until a subclass overrides).
    def redefine(owner, name, namespaced_name, value)
      key = :"ractor_rails_shim_class_attr_#{owner.object_id}_#{namespaced_name}"

      # Seed the main Ractor's IES slot with the default. Only seed in
      # main — workers start nil and set their own value via the writer.
      RactorRailsShim.storage[key] = value if Ractor.main?

      # Also store in CLASS_ATTR_VALUES so the reader can fall back to it
      # in the MAIN ractor on non-boot threads. IES is thread-local: Puma's
      # request threads have empty IES slots, so the reader returns nil
      # without this fallback. This is the bug that breaks normal (non-
      # Ractor) multi-threaded servers — the minimal --minimal app didn't
      # hit it because /up doesn't trigger LogSubscriber.log_levels.
      # CLASS_ATTR_VALUES is NOT shareable (values may be mutable); only
      # safe to read from the main ractor.
      RactorRailsShim::Registry.class_attr_values[key] = value

      # Register so _build_shareable_fallback! can capture + make shareable
      # at prepare_for_ractors! time. owner.name may be nil for anonymous
      # classes (e.g. spec fixtures); use a stable label in that case.
      # The default value is stored too so the fallback builder can use it
      # when the live value can't be made shareable (e.g. __callbacks holds
      # self-capturing Procs — workers get the empty default, treating
      # boot-time callbacks as already-run, which is correct for a frozen
      # shared app).
      owner_label = owner.respond_to?(:name) ? owner.name : owner.class.name
      owner_label = owner_label || "anon_#{owner.class.name}_#{owner.object_id}"
      RactorRailsShim::Registry.class_attributes << [owner_label, namespaced_name, key, value]

      # Always define the namespaced reader/writer on owner's singleton
      # class via string eval (no captured binding). The class_attribute
      # macro itself also defines `def #{name}; #{namespaced_name}; end`
      # via class_eval (string-eval'd, safe) on the owner — that calls our
      # IES-routed namespaced reader/writer. We override BOTH the namespaced
      # and (when owner is a module's singleton) the public name.
      #
      # Worker-Ractor fallback: when the worker's own IES slot is empty
      # (which it is by default — the value lives in main's slot), fall
      # back to the frozen shareable table built at prepare_for_ractors!
      # time. This is read-only and shared across all workers; workers that
      # need their own mutable value call the writer, which writes their
      # IES slot and shadows the fallback.
      target = owner.singleton_class? ? owner : owner.singleton_class
      # Static missing-slot default: frozen shared Hash for __callbacks
      # (Rails indexes the result, so nil would NoMethodError), nil for
      # everything else. Decided once at method-definition time.
      missing_default = (name == :__callbacks) ? "RactorRailsShim::EMPTY_CALLBACKS_HASH" : "nil"
      # ONE heredoc for both modes — the selected strategy
      # (RactorRailsShim.storage_strategy, set once at install from
      # RunMode.thread?) decides the lookup/store backend. Collapses the
      # former two-mode `if thread_mode?` branch (Issue #15).
      target.module_eval RactorRailsShim._class_attr_methods(namespaced_name, namespaced_name, missing_default),
                          __FILE__, __LINE__ + 1

      # When owner is a module's singleton class, the original also
      # defines a public reader `def #{name}` on owner directly. Override
      # it with the strategy-routed version.
      if owner.singleton_class? && owner.attached_object.is_a?(Module)
        owner.module_eval RactorRailsShim._class_attr_methods(name, namespaced_name, missing_default),
                            __FILE__, __LINE__ + 1
      end

      # When owner IS a singleton class (e.g. called from class << self),
      # class_attribute's class_eval does `class << self` which opens a
      # nested singleton class. The public `def #{name}` ends up on
      # owner.singleton_class, which calls `#{namespaced_name}` — but
      # that method was only defined on `target` (= owner, the singleton
      # class), not on the nested level. Define it on owner.singleton_class
      # too so the nested `def #{name}` can resolve it.
      if owner.singleton_class?
        owner.singleton_class.module_eval RactorRailsShim._class_attr_methods(namespaced_name, namespaced_name, missing_default),
                                           __FILE__, __LINE__ + 1
      end
    end

    # redefine_method is used by `redefine` internally and by other call
    # sites (rare). The class_attribute path goes through our `redefine`
    # above; keep the original block-based behavior for any other callers
    # so we don't break unrelated code.
    def redefine_method(owner, name, private: false, &block)
      super
    end
  })
end

.patch_execution_wrapper!Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ractor_rails_shim/patches/execution_wrapper.rb', line 34

def patch_execution_wrapper!
  return if @exec_wrapper_registry_patched
  @exec_wrapper_registry_patched = true
  ew = ::ActiveSupport::ExecutionWrapper
  key = :ractor_rails_shim_exec_wrapper_active_key
  key_str = key.inspect
  # active_key returns :"active_execution_wrapper_<object_id>"; a frozen
  # Symbol is shareable. Compute it once per Ractor and cache in IES.
  ew.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def active_key
      v = RactorRailsShim.storage[#{key_str}]
      return v if RactorRailsShim.storage.key?(#{key_str})
      sym = :"active_execution_wrapper_\#{object_id}"
      RactorRailsShim.storage[#{key_str}] = sym
      sym
    end
  RUBY

  # Patch ActiveSupport::Callbacks#run_callbacks to tolerate a nil
  # __callbacks (the case in worker Ractors whose class_attribute fallback
  # couldn't be made shareable because callback chains hold frozen,
  # self-capturing Procs). For a frozen, read-only shared app the boot-time
  # callbacks (ExecutionContext push/pop, CurrentAttributes clear) already
  # ran in the main Ractor at boot; worker Ractors don't need to re-run
  # them per request (CurrentAttributes/ExecutionContext are thread-local,
  # hence per-Ractor, and start empty in a fresh worker). When __callbacks
  # is nil, run_callbacks just yields the block — matching the empty-chain
  # fast path in the original. (Method body lives in active_support.rb.)
  _install_callbacks_nil_safe_patch

  # Patch ActiveSupport::Notifications.notifier to not read the @notifier
  # class ivar from a worker Ractor. The original is `attr_accessor
  # :notifier` with `@notifier = Fanout.new` set at module load — a raw
  # class ivar holding a Fanout (which has a Mutex + subscriber Procs,
  # both unshareable). Workers get their own per-Ractor Fanout (no
  # subscribers — instrumentation is a no-op in workers, which is correct
  # for a read-only shared app where log subscribers already ran in main).
  # `notifier` is read by `instrumenter` (per-request via Rails::Rack::Logger).
  # (Method body lives in active_support.rb.)
  _install_notifications_notifier_patch
end

.patch_rails_module!(mod) ⇒ Object

The actual Rails-module patch. Idempotent. Must be called from the main Ractor (it prepends onto Rails.singleton_class).



61
62
63
64
65
66
# File 'lib/ractor_rails_shim/patches/rails_module.rb', line 61

def patch_rails_module!(mod)
  return if @rails_module_patched
  @rails_module_patched = true
  RactorRailsShim::ConstantShareabilizer.apply!
  _patch_rails_module_body(mod)
end

.patch_rubygems!Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/ractor_rails_shim/patches/rubygems.rb', line 29

def patch_rubygems!
  return if @rubygems_method_patched
  @rubygems_method_patched = true
  gem = ::Gem
  unless gem.singleton_class.method_defined?(:__shim_original_gem_paths)
    gem.singleton_class.alias_method :__shim_original_gem_paths, :paths
  end
  # `def` (not `define_method`) so the method has no captured binding and
  # is callable from any Ractor. `Ractor.main?` + a shareable constant are
  # both Ractor-safe.
  gem.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def paths
      if Ractor.main?
        __shim_original_gem_paths
      else
        ::RactorRailsShim::GEM_PATHS_SNAPSHOT
      end
    end
  RUBY
end

.patch_zeitwerk_registry!Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ractor_rails_shim/patches/zeitwerk_registry.rb', line 43

def patch_zeitwerk_registry!
  return if @zeitwerk_registry_patched
  @zeitwerk_registry_patched = true

  reg = ::Zeitwerk::Registry
  # The ivars Zeitwerk sets at the bottom of registry.rb. Map each to an
  # IES key and a default-builder string (eval'd in the reader when the
  # Ractor's slot is empty). Builders reference Zeitwerk constants by
  # full path so they're resolvable from any Ractor.
  ivars = {
    loaders:                  [:ractor_rails_shim_zw_loaders,    "Zeitwerk::Registry::Loaders.new"],
    gem_loaders_by_root_file: [:ractor_rails_shim_zw_gem,        "{}"],
    autoloads:                [:ractor_rails_shim_zw_autoloads,  "Zeitwerk::Registry::Autoloads.new"],
    explicit_namespaces:      [:ractor_rails_shim_zw_explicit,   "Zeitwerk::Registry::ExplicitNamespaces.new"],
    inceptions:               [:ractor_rails_shim_zw_inceptions, "Zeitwerk::Registry::Inceptions.new"],
    mutex:                    [:ractor_rails_shim_zw_mutex,      "Mutex.new"],
  }

  # Redefine each reader (and the mutex, which is read directly as @mutex)
  # to route through IES with lazy per-Ractor init. Use a PREPENDED module
  # (not direct module_eval on the singleton class) because Zeitwerk's
  # `attr_reader :loaders` etc. run LATER in the module body and would
  # clobber a direct redefinition — same load-order issue as the Rails
  # module accessors. A prepended module stays in front of the lookup chain.
  #
  # In the MAIN ractor, fall back to the existing ivar (set by Zeitwerk at
  # the bottom of registry.rb) so main-ractor state is preserved. Worker
  # ractors lazily build their own via the builder string.
  reader_patch = Module.new
  ivars.each do |ivar, (key, builder)|
    key_str = key.inspect
    ivar_sym = :"@#{ivar}"
    ivar_str = ivar_sym.inspect
    reader_patch.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{ivar}
        v = RactorRailsShim.storage[#{key_str}]
        return v if RactorRailsShim.storage.key?(#{key_str})
        if Ractor.main?
          existing = instance_variable_get(#{ivar_str}) if instance_variable_defined?(#{ivar_str})
          if existing
            RactorRailsShim.storage[#{key_str}] = existing
            return existing
          end
        end
        v = #{builder}
        RactorRailsShim.storage[#{key_str}] = v
        v
      end
    RUBY
  end
  reg.singleton_class.prepend(reader_patch)

  # `conflicting_root_dir?` and `loader_for_gem` read @mutex / @gem_loaders
  # directly via instance_variable_get-ish access (they use @mutex in the
  # method body). Since we redefined the readers, the direct @mutex refs
  # in those methods still hit the ivar. We need to rewrite those two
  # methods to call the reader instead. Easiest: prepend a module that
  # calls self.mutex / self.gem_loaders_by_root_file.
  reg.singleton_class.prepend(Module.new {
    def conflicting_root_dir?(loader, new_root_dir)
      mutex.synchronize do
        loaders.each do |existing_loader|
          next if existing_loader == loader
          existing_loader.__roots.each_key do |existing_root_dir|
            next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
            new_root_dir_slash = new_root_dir + '/'
            existing_root_dir_slash = existing_root_dir + '/'
            next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
            next if loader.__ignores?(existing_root_dir)
            break if existing_loader.__ignores?(new_root_dir)
            return existing_loader
          end
        end
        nil
      end
    end

    def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
      h = gem_loaders_by_root_file
      h[root_file] ||= Zeitwerk::GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
    end

    def unregister_loader(loader)
      gem_loaders_by_root_file.delete_if { |_, l| l == loader }
    end
  })
end

.prepare_for_ractors!Object

Public API: run after Rails.application.initialize! and BEFORE spawning worker Ractors. Makes every registered constant shareable (deep-freeze). Constants that didn't exist at install time (e.g. Rails::Railtie, loaded after module Rails opens) get fixed here. Idempotent; safe to call multiple times. Must run in the main Ractor.

NOTE: this does NOT build the framework-config shareable fallback. That step is folded into make_app_shareable! because some class_attribute / mattr_accessor values reference the app graph, and making them shareable must happen AFTER the app itself is already frozen (otherwise the app gets frozen prematurely and precompute/proc-replacement can't mutate it). If you call prepare_for_ractors! standalone (without make_app_shareable!), worker Ractors will see nil for framework config values that couldn't be shared without freezing the app — set them explicitly per worker, or use make_app_shareable!.

Delegates to Lifecycle.prepare_for_ractors! (extracted POODR §1 SRP).



240
241
242
# File 'lib/ractor_rails_shim/patches/core.rb', line 240

def prepare_for_ractors!
  Lifecycle.prepare_for_ractors!
end

.shareable_constantsObject

Public reader for the SHAREABLE_CONSTANTS registry. Users register their own constants via RactorRailsShim.shareable_constants << "MyGem::LIST". Delegates to ConstantShareabilizer.shareable_constants (which reads RactorRailsShim::SHAREABLE_CONSTANTS).



203
204
205
# File 'lib/ractor_rails_shim/patches/core.rb', line 203

def shareable_constants
  ConstantShareabilizer.shareable_constants
end

.snapshot_gem_paths!Object

Called from prepare_for_ractors! (post-boot, main Ractor) so the snapshot reflects Bundler-configured gem paths. Also called from install as a safety net so the constant is never undefined.



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ractor_rails_shim/patches/rubygems.rb', line 53

def snapshot_gem_paths!
  return unless defined?(::Gem)
  return if defined?(::RactorRailsShim::GEM_PATHS_SNAPSHOT)
  snap = ::Gem.paths
  begin
    Ractor.make_shareable(snap)
  rescue StandardError
    snap = Ractor.make_shareable(::Gem.path)
  end
  ::RactorRailsShim.const_set(:GEM_PATHS_SNAPSHOT, snap)
end

.snapshot_query_logs!Object

Capture the QueryLogs handlers/formatter as a shareable snapshot for workers (called post-boot, main Ractor, in prepare_for_ractors!).

We deliberately do NOT use Ractor.make_shareable on the raw @handlers objects: a handler may be a ZeroArityHandler wrapping a Proc, a raw lambda/Proc tag, or an IdentityHandler whose value is unshareable — all of which make_shareable raises on, which (the original rescue swallowed) left QUERY_LOGS_SNAPSHOT unset and every worker falling through to the original tag_content -> @handlers read -> Ractor::IsolationError. Instead we build a fresh, guaranteed-shareable structure:

{ format: :legacy|:sqlcommenter,
handlers: [[key, :get_key, nil] | [key, :identity, value], ...] }

Only GetKeyHandler (context key lookup) and IdentityHandler (constant value, when that value itself is shareable) are captured. Proc/lambda handlers can't be expressed cross-Ractor and are dropped in workers (tags that depend on per-request Procs simply don't appear in worker query comments — acceptable; the main Ractor still logs them).



2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2142

def snapshot_query_logs!
  return unless defined?(::ActiveRecord::QueryLogs)
  return if RactorRailsShim.const_defined?(:QUERY_LOGS_SNAPSHOT)
  return unless Ractor.main?
  begin
    ql = ::ActiveRecord::QueryLogs
    format = ql.tags_formatter
    format = :legacy if format == false || format.nil?
    raw_handlers = ql.instance_variable_get(:@handlers) || []
    entries = []
    raw_handlers.each do |key, handler|
      if handler.is_a?(::ActiveRecord::QueryLogs::GetKeyHandler)
        entries << [key, :get_key, nil]
      elsif handler.is_a?(::ActiveRecord::QueryLogs::IdentityHandler)
        value = handler.instance_variable_get(:@value)
        next unless Ractor.shareable?(value)
        entries << [key, :identity, value]
      else
        # ZeroArityHandler (wraps a Proc) or a raw Proc/lambda tag:
        # intrinsically unshareable / depends on a closure; skip in workers.
        next
      end
    end
    snap = { format: format, handlers: entries.freeze }.freeze
    Ractor.make_shareable(snap)
    RactorRailsShim.const_set(:QUERY_LOGS_SNAPSHOT, snap)
  rescue StandardError
    nil
  end
end

.storageObject

The active storage implementation. Patch sites route through this so the shim doesn't open the ActiveSupport namespace to alias the fallback.



95
96
97
# File 'lib/ractor_rails_shim/foundation/storage.rb', line 95

def storage
  Storage.storage
end

.thread_mode=(value) ⇒ Object



172
173
174
175
176
177
# File 'lib/ractor_rails_shim/patches/core.rb', line 172

def thread_mode=(value)
  RactorRailsShim::RunMode.thread = value
  # `storage_strategy` derives lazily from `RunMode.thread?` (see
  # `StorageStrategy`), so no explicit sync here — resetting `RunMode`
  # is enough to reset the strategy.
end

.thread_mode?Boolean

Install all the patches. Safe to call multiple times (idempotent).

May be called either before or after Rails is loaded:

- If Rails is already defined (e.g. `Bundler.require` ran first), the
Rails module accessors are patched immediately.
- If Rails is not yet defined (the normal `config/boot.rb` case, where
`install` is called before `require "rails"`), a one-shot load hook
defers the Rails-module patch until `rails.rb` is loaded. The
`mattr_accessor` macro patch (a `Module.prepend`) applies
immediately regardless, because it patches the macro itself, not
any Rails constant.

True when the shim should install its THREAD-server (Puma/Falcon) mode instead of the default Ractor (kino) mode. In thread mode Ractor.main? is true, so Rails' own globals (class variables / class ivars) are thread-safe and used as-is; only the class_attribute callback-chain isolation fix and the nil-safe callback replay are installed. The other patches route framework globals through per-Ractor IsolatedExecutionState, which is empty on Puma's request threads and would break the app, so they are skipped.

Configuration is owned by RactorRailsShim::RunMode: set explicitly via RactorRailsShim.thread_mode = true (or RunMode.thread = true), or let install resolve it from ENV["SERVER"] (puma|falcon|thin|webrick|thread*). The decision is a configuration responsibility extracted from install per POODR; install calls RunMode.resolve! (a no-op when already configured explicitly) and then reads RunMode.thread?. These facade methods delegate for backward compatibility with existing call sites (patches/class_attribute.rb, patches/active_support.rb, specs).

Returns:

  • (Boolean)


168
169
170
# File 'lib/ractor_rails_shim/patches/core.rb', line 168

def thread_mode?
  RactorRailsShim::RunMode.thread?
end

.version_policyObject

Policy for version mismatches. One of :warn (default), :strict, :off. Set before install:

RactorRailsShim.version_policy = :strict

Delegates to RactorRailsShim::VersionPolicy.policy.



76
77
78
# File 'lib/ractor_rails_shim/patches/core.rb', line 76

def version_policy
  RactorRailsShim::VersionPolicy.policy
end

.version_policy=(value) ⇒ Object



80
81
82
# File 'lib/ractor_rails_shim/patches/core.rb', line 80

def version_policy=(value)
  RactorRailsShim::VersionPolicy.policy = value
end

.worker_app!(frozen_app) ⇒ Object

Build the shareable Rack app handed to kino. Delegates to WorkerAppFactory.build (extracted Issue #13, Step 13.4). See WorkerAppFactory for the shareability contract (returns a frozen, Ractor.shareable? WorkerApp instance).



311
312
313
# File 'lib/ractor_rails_shim/patches/core.rb', line 311

def worker_app!(frozen_app)
  WorkerAppFactory.build(frozen_app)
end

.worker_ar_init(app) ⇒ Object

Wrap app so every worker Ractor initializes its ActiveRecord connections on first request. Returns a shareable wrapper.



2076
2077
2078
# File 'lib/ractor_rails_shim/patches/activerecord.rb', line 2076

def worker_ar_init(app)
  ArWorkerInitWrapper.new(app)
end