ractor-rails-shim 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +140 -2
- data/README.md +90 -27
- data/lib/ractor_rails_shim/patches/action_controller.rb +10 -1
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +1 -1
- data/lib/ractor_rails_shim/patches/action_view.rb +2 -2
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +17 -0
- data/lib/ractor_rails_shim/patches/active_support.rb +20 -20
- data/lib/ractor_rails_shim/patches/activerecord.rb +102 -20
- data/lib/ractor_rails_shim/patches/class_attribute.rb +17 -4
- data/lib/ractor_rails_shim/patches/core.rb +121 -51
- data/lib/ractor_rails_shim/patches/devise.rb +1 -1
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +1 -1
- data/lib/ractor_rails_shim/patches/kaminari.rb +1 -1
- data/lib/ractor_rails_shim/patches/make_shareable.rb +126 -37
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +2 -2
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +1 -1
- data/lib/ractor_rails_shim/patches/rack.rb +5 -5
- data/lib/ractor_rails_shim/patches/rails_module.rb +13 -6
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +8 -1
- data/lib/ractor_rails_shim/version.rb +1 -1
- data/lib/ractor_rails_shim/version_check.rb +10 -6
- metadata +3 -3
|
@@ -590,6 +590,18 @@ module RactorRailsShim
|
|
|
590
590
|
# which writes `@table_name`/`@arel_table`/etc. From a worker that write is
|
|
591
591
|
# Ractor::IsolationError. Route the value through IsolatedExecutionState
|
|
592
592
|
# (keyed by model object_id); main keeps the original class-ivar behavior.
|
|
593
|
+
#
|
|
594
|
+
# NOTE on naming: there are TWO ModelSchema-related install methods, with
|
|
595
|
+
# deliberately different scopes:
|
|
596
|
+
# * `_install_active_record_model_schema_patch` (this method) — patches
|
|
597
|
+
# `table_name` / `table_name=` / `reset_table_name` only.
|
|
598
|
+
# * `_install_activerecord_model_schema_patch` (below) — patches the
|
|
599
|
+
# *column-derived* lazy caches `symbol_column_to_string`,
|
|
600
|
+
# `content_columns` (and previously `column_defaults`, now provided
|
|
601
|
+
# by the prepended `ActiveRecordModelSchemaPatch`).
|
|
602
|
+
# Both target `ActiveRecord::ModelSchema::ClassMethods` but patch
|
|
603
|
+
# disjoint method sets; do not collapse them without auditing every
|
|
604
|
+
# caller.
|
|
593
605
|
def _install_active_record_model_schema_patch
|
|
594
606
|
return if @ar_model_schema_patched
|
|
595
607
|
@ar_model_schema_patched = true
|
|
@@ -776,15 +788,79 @@ module RactorRailsShim
|
|
|
776
788
|
cols
|
|
777
789
|
end
|
|
778
790
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
791
|
+
# NOTE: `column_defaults` is intentionally NOT redefined here. It is
|
|
792
|
+
# installed by the prepended `ActiveRecordModelSchemaPatch` (see
|
|
793
|
+
# `active_record_model_schema.rb`), which is prepended onto
|
|
794
|
+
# `ModelSchema::ClassMethods` by `_install_active_model_attribute_patch`
|
|
795
|
+
# and therefore sits in front of this module_eval heredoc in the
|
|
796
|
+
# method lookup chain. A second definition here would be dead code and
|
|
797
|
+
# a maintenance trap.
|
|
798
|
+
|
|
799
|
+
# `reload_schema_from_cache` (called by `load_schema`/`reload_schema!`)
|
|
800
|
+
# writes a batch of class ivars to `nil` to invalidate schema caches:
|
|
801
|
+
# @_returning_columns_for_insert, @arel_table, @column_names,
|
|
802
|
+
# @symbol_column_to_string_name_hash, @content_columns, @column_defaults
|
|
803
|
+
# (ModelSchema) plus @timestamp_attributes_for_create_in_model,
|
|
804
|
+
# @timestamp_attributes_for_update_in_model,
|
|
805
|
+
# @all_timestamp_attributes_in_model (Timestamp#reload_schema_from_cache
|
|
806
|
+
# via super). In a worker Ractor those writes raise
|
|
807
|
+
# Ractor::IsolationError ("can not set instance variables of
|
|
808
|
+
# classes/modules by non-main Ractors"). The shim already routes the
|
|
809
|
+
# *readers* for these caches through IES, so in a worker we only need
|
|
810
|
+
# to clear the IES slots — the next read rebuilds lazily. In main we
|
|
811
|
+
# keep the original class-ivar-clearing behavior.
|
|
812
|
+
def reload_schema_from_cache(recursive = true)
|
|
813
|
+
if Ractor.main?
|
|
814
|
+
super
|
|
815
|
+
else
|
|
816
|
+
# Clear this Ractor's IES slots for the IES-routed schema caches.
|
|
817
|
+
# Use `next` over an explicit list (not `IES.clear`) to avoid
|
|
818
|
+
# wiping unrelated slots. Keys mirror the readers above + the
|
|
819
|
+
# prepended ActiveRecordModelSchemaPatch (active_record_model_schema.rb)
|
|
820
|
+
# + ActiveModelAttributeRegistrationPatch (active_model_attribute.rb).
|
|
821
|
+
name_str = self.name
|
|
822
|
+
[
|
|
823
|
+
:"ractor_rails_shim_symbol_column_to_string_\#{name_str}",
|
|
824
|
+
:"ractor_rails_shim_content_columns_\#{name_str}",
|
|
825
|
+
:"rrs_column_defaults_\#{object_id}",
|
|
826
|
+
:"rrs_attributes_builder_\#{object_id}",
|
|
827
|
+
:"rrs_yaml_encoder_\#{object_id}",
|
|
828
|
+
:"rrs_returning_cols_\#{object_id}",
|
|
829
|
+
:"rrs_default_attributes_\#{object_id}",
|
|
830
|
+
:"rrs_attribute_types_\#{object_id}",
|
|
831
|
+
:"rrs_table_names",
|
|
832
|
+
:"rrs_arel_tables",
|
|
833
|
+
:"rrs_predicate_builders",
|
|
834
|
+
:"rrs_type_casters",
|
|
835
|
+
].each do |k| ActiveSupport::IsolatedExecutionState.delete(k) end
|
|
836
|
+
# The Timestamp subclass override calls `super` (this method); it
|
|
837
|
+
# has already run by the time we get here via the super chain, so
|
|
838
|
+
# its ivar-clears were intercepted by the worker branch of THIS
|
|
839
|
+
# override (no-op). No further action needed.
|
|
840
|
+
end
|
|
786
841
|
end
|
|
787
842
|
RUBY
|
|
843
|
+
|
|
844
|
+
# Patch ActiveRecord::Timestamp::ClassMethods#reload_schema_from_cache
|
|
845
|
+
# the same way: in main, keep the original ivar-clearing; in a worker,
|
|
846
|
+
# skip the class-ivar writes (the IES slots are cleared by the
|
|
847
|
+
# ModelSchema#reload_schema_from_cache override above via super).
|
|
848
|
+
if defined?(::ActiveRecord::Timestamp::ClassMethods)
|
|
849
|
+
::ActiveRecord::Timestamp::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
850
|
+
def reload_schema_from_cache(recursive = true)
|
|
851
|
+
if Ractor.main?
|
|
852
|
+
@timestamp_attributes_for_create_in_model = nil
|
|
853
|
+
@timestamp_attributes_for_update_in_model = nil
|
|
854
|
+
@all_timestamp_attributes_in_model = nil
|
|
855
|
+
super
|
|
856
|
+
else
|
|
857
|
+
# Worker: skip the class-ivar writes (would raise
|
|
858
|
+
# IsolationError). The ModelSchema super clears the IES slots.
|
|
859
|
+
super
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
RUBY
|
|
863
|
+
end
|
|
788
864
|
end
|
|
789
865
|
|
|
790
866
|
# Patch ActiveModel::Conversion::ClassMethods#_to_partial_path to route its
|
|
@@ -863,7 +939,7 @@ module RactorRailsShim
|
|
|
863
939
|
::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
864
940
|
def configurations
|
|
865
941
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
866
|
-
return v
|
|
942
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
867
943
|
if Ractor.main?
|
|
868
944
|
ActiveRecord::Core.class_variable_get(:@@configurations)
|
|
869
945
|
else
|
|
@@ -919,7 +995,7 @@ module RactorRailsShim
|
|
|
919
995
|
::ActiveRecord::DatabaseConfigurations.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
920
996
|
def db_config_handlers
|
|
921
997
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
922
|
-
return v
|
|
998
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
923
999
|
if Ractor.main?
|
|
924
1000
|
@db_config_handlers
|
|
925
1001
|
else
|
|
@@ -973,7 +1049,7 @@ module RactorRailsShim
|
|
|
973
1049
|
::ActiveRecord.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
974
1050
|
def query_transformers
|
|
975
1051
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
976
|
-
return v
|
|
1052
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
977
1053
|
if Ractor.main?
|
|
978
1054
|
@query_transformers
|
|
979
1055
|
else
|
|
@@ -1026,7 +1102,7 @@ module RactorRailsShim
|
|
|
1026
1102
|
::ActiveRecord.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
1027
1103
|
def #{method_name}
|
|
1028
1104
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
1029
|
-
return v
|
|
1105
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
1030
1106
|
if Ractor.main?
|
|
1031
1107
|
@#{method_name}
|
|
1032
1108
|
else
|
|
@@ -1058,7 +1134,7 @@ module RactorRailsShim
|
|
|
1058
1134
|
def registry
|
|
1059
1135
|
key = :"ractor_rails_shim_dedup_registry_\#{name || object_id}"
|
|
1060
1136
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1061
|
-
return v
|
|
1137
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1062
1138
|
h = {}
|
|
1063
1139
|
ActiveSupport::IsolatedExecutionState[key] = h
|
|
1064
1140
|
h
|
|
@@ -1081,7 +1157,7 @@ module RactorRailsShim
|
|
|
1081
1157
|
def query_constraints_list
|
|
1082
1158
|
key = :"ractor_rails_shim_qcl_\#{name || object_id}"
|
|
1083
1159
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1084
|
-
return v
|
|
1160
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1085
1161
|
result = if base_class? || primary_key != base_class.primary_key
|
|
1086
1162
|
primary_key if primary_key.is_a?(::Array)
|
|
1087
1163
|
else
|
|
@@ -1093,7 +1169,7 @@ module RactorRailsShim
|
|
|
1093
1169
|
def has_query_constraints?
|
|
1094
1170
|
key = :"ractor_rails_shim_qcl_\#{name || object_id}"
|
|
1095
1171
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1096
|
-
return !v.nil?
|
|
1172
|
+
return !v.nil? if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1097
1173
|
result = query_constraints_list
|
|
1098
1174
|
!result.nil?
|
|
1099
1175
|
end
|
|
@@ -1336,7 +1412,7 @@ module RactorRailsShim
|
|
|
1336
1412
|
::ActiveRecord::Base.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
1337
1413
|
def default_connection_handler
|
|
1338
1414
|
v = ActiveSupport::IsolatedExecutionState[#{dch_key_str}]
|
|
1339
|
-
return v
|
|
1415
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{dch_key_str})
|
|
1340
1416
|
if Ractor.main?
|
|
1341
1417
|
cv = RactorRailsShim::CLASS_ATTR_VALUES[:__ractor_rails_shim_ar_default_connection_handler__]
|
|
1342
1418
|
return cv if cv
|
|
@@ -1351,6 +1427,12 @@ module RactorRailsShim
|
|
|
1351
1427
|
# worker's other threads; Ractor.current is per-Ractor and shared by all
|
|
1352
1428
|
# threads of the worker. Falls back to IES (legacy) then
|
|
1353
1429
|
# default_connection_handler (main Ractor only).
|
|
1430
|
+
# NOTE: uses nil-sentinel (`unless v.nil?`) rather than key?-based
|
|
1431
|
+
# detection because Ractor#[] has no `key?` method (Ractor local
|
|
1432
|
+
# storage returns nil for both unset and set-to-nil). Safe here
|
|
1433
|
+
# because `connection_handler=` is only ever called with a real
|
|
1434
|
+
# ConnectionHandler instance, never nil — so nil unambiguously
|
|
1435
|
+
# means "unset, fall through to default_connection_handler".
|
|
1354
1436
|
def connection_handler
|
|
1355
1437
|
v = Ractor.current[:active_record_connection_handler]
|
|
1356
1438
|
return v unless v.nil?
|
|
@@ -1428,7 +1510,7 @@ module RactorRailsShim
|
|
|
1428
1510
|
def serialize_cast_value_compatible?
|
|
1429
1511
|
key = :"ractor_rails_shim_scv_\#{name || object_id}"
|
|
1430
1512
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1431
|
-
return v
|
|
1513
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1432
1514
|
result = ancestors.index(instance_method(:serialize_cast_value).owner) <= ancestors.index(instance_method(:serialize).owner)
|
|
1433
1515
|
ActiveSupport::IsolatedExecutionState[key] = result
|
|
1434
1516
|
result
|
|
@@ -1461,7 +1543,7 @@ module RactorRailsShim
|
|
|
1461
1543
|
def uncacheable_methods
|
|
1462
1544
|
key = :ractor_rails_shim_ar_uncacheable_methods
|
|
1463
1545
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1464
|
-
return v
|
|
1546
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1465
1547
|
result = (
|
|
1466
1548
|
delegated_classes.flat_map(&:public_instance_methods) - ActiveRecord::Relation.public_instance_methods
|
|
1467
1549
|
).to_set.freeze
|
|
@@ -1497,7 +1579,7 @@ module RactorRailsShim
|
|
|
1497
1579
|
def primary_key
|
|
1498
1580
|
key = :"ractor_rails_shim_pk_\#{name || object_id}"
|
|
1499
1581
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1500
|
-
return v
|
|
1582
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1501
1583
|
if Ractor.main?
|
|
1502
1584
|
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
|
|
1503
1585
|
v = @primary_key
|
|
@@ -1510,7 +1592,7 @@ module RactorRailsShim
|
|
|
1510
1592
|
def composite_primary_key?
|
|
1511
1593
|
key = :"ractor_rails_shim_pk_\#{name || object_id}"
|
|
1512
1594
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
1513
|
-
return v.is_a?(::Array)
|
|
1595
|
+
return v.is_a?(::Array) if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
1514
1596
|
if Ractor.main?
|
|
1515
1597
|
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
|
|
1516
1598
|
@primary_key.is_a?(::Array)
|
|
@@ -1700,7 +1782,7 @@ module RactorRailsShim
|
|
|
1700
1782
|
mig.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
1701
1783
|
def migrations_paths
|
|
1702
1784
|
v = ActiveSupport::IsolatedExecutionState[#{mp_key_str}]
|
|
1703
|
-
return v
|
|
1785
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{mp_key_str})
|
|
1704
1786
|
if Ractor.main? && instance_variable_defined?(:@migrations_paths)
|
|
1705
1787
|
v = @migrations_paths
|
|
1706
1788
|
ActiveSupport::IsolatedExecutionState[#{mp_key_str}] = v
|
|
@@ -102,11 +102,24 @@ module RactorRailsShim
|
|
|
102
102
|
# for copy-on-write fallback. This restores per-subclass isolation
|
|
103
103
|
# (lost by the IES-routed variant) without thread-local IES, which
|
|
104
104
|
# is empty on Puma's request threads.
|
|
105
|
+
#
|
|
106
|
+
# Key format MUST stay `:"ractor_rails_shim_class_attr_<oid>_<name>"`
|
|
107
|
+
# to match the writer below and the Ractor-mode key in
|
|
108
|
+
# `redefine`'s own `key` local. The original walked ancestors and
|
|
109
|
+
# interpolated the *whole* Symbol per ancestor on every read —
|
|
110
|
+
# allocating a fresh Array (ancestors) AND a fresh Symbol per
|
|
111
|
+
# ancestor, the dominant allocation source for GET requests. We
|
|
112
|
+
# interpolate only the per-ancestor `object_id` tail inside the
|
|
113
|
+
# lookup, which Ruby optimizes to a single Symbol allocation per
|
|
114
|
+
# ancestor (vs. the previous full-string interpolation). The
|
|
115
|
+
# Ractor-mode branch below avoids the walk entirely (literal key),
|
|
116
|
+
# but thread mode must walk because subclass COW fallback is
|
|
117
|
+
# load-bearing here.
|
|
105
118
|
target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
106
119
|
def #{namespaced_name}
|
|
107
120
|
self.ancestors.each do |anc|
|
|
108
121
|
v = RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"]
|
|
109
|
-
return v
|
|
122
|
+
return v if RactorRailsShim::CLASS_ATTR_VALUES.key?(:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}")
|
|
110
123
|
end
|
|
111
124
|
#{namespaced_name.inspect} == :__callbacks ? {} : nil
|
|
112
125
|
end
|
|
@@ -124,7 +137,7 @@ module RactorRailsShim
|
|
|
124
137
|
def #{name}
|
|
125
138
|
self.ancestors.each do |anc|
|
|
126
139
|
v = RactorRailsShim::CLASS_ATTR_VALUES[:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}"]
|
|
127
|
-
return v
|
|
140
|
+
return v if RactorRailsShim::CLASS_ATTR_VALUES.key?(:"ractor_rails_shim_class_attr_\#{anc.object_id}_#{namespaced_name}")
|
|
128
141
|
end
|
|
129
142
|
#{namespaced_name.inspect} == :__callbacks ? {} : nil
|
|
130
143
|
end
|
|
@@ -148,7 +161,7 @@ module RactorRailsShim
|
|
|
148
161
|
target.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
149
162
|
def #{namespaced_name}
|
|
150
163
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
151
|
-
return v
|
|
164
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
152
165
|
fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
|
|
153
166
|
return fb unless fb.nil?
|
|
154
167
|
RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
|
|
@@ -168,7 +181,7 @@ module RactorRailsShim
|
|
|
168
181
|
owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
169
182
|
def #{name}
|
|
170
183
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
171
|
-
return v
|
|
184
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
172
185
|
fb = RactorRailsShim::SHAREABLE_FALLBACK[#{key_str}]
|
|
173
186
|
return fb unless fb.nil?
|
|
174
187
|
RactorRailsShim::CLASS_ATTR_VALUES[#{key_str}] if Ractor.main?
|
|
@@ -120,6 +120,29 @@ module RactorRailsShim
|
|
|
120
120
|
end
|
|
121
121
|
attr_writer :version_policy
|
|
122
122
|
|
|
123
|
+
# When true, swallowed exceptions in freeze/shareability paths are
|
|
124
|
+
# reported to $stderr so a worker Ractor that later crashes on an
|
|
125
|
+
# unshareable value has a traceable cause. Default false (silent, the
|
|
126
|
+
# historical behaviour). Enable for diagnosis:
|
|
127
|
+
# RactorRailsShim.debug = true
|
|
128
|
+
def debug?
|
|
129
|
+
defined?(@debug) ? @debug : false
|
|
130
|
+
end
|
|
131
|
+
attr_writer :debug
|
|
132
|
+
|
|
133
|
+
# Swallow an exception raised by the block, optionally logging it when
|
|
134
|
+
# `debug?` is on. Used by freeze/shareability paths where individual
|
|
135
|
+
# failures are expected (some ivars hold intrinsically unshareable
|
|
136
|
+
# values like Procs) but a worker crash on the same value later has no
|
|
137
|
+
# visible cause. `label` identifies the call site (e.g. "freeze AR ivar
|
|
138
|
+
# Post@column_defaults"). Keep the label short — it's only for grepping.
|
|
139
|
+
def _swallow(label)
|
|
140
|
+
yield
|
|
141
|
+
rescue => e
|
|
142
|
+
warn "[ractor_rails_shim] #{label}: #{e.class}: #{e.message[0, 120]}" if debug?
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
123
146
|
SUPPORTED_RUBY = RactorRailsShim::Version::SUPPORTED_RUBY
|
|
124
147
|
SUPPORTED_RAILS = "8.1"
|
|
125
148
|
|
|
@@ -283,11 +306,7 @@ module RactorRailsShim
|
|
|
283
306
|
# with `equal?`, and a frozen Symbol is always shareable.
|
|
284
307
|
Ractor.make_shareable(:"__shim_unshareable_sentinel__")
|
|
285
308
|
else
|
|
286
|
-
|
|
287
|
-
Ractor.make_shareable(val)
|
|
288
|
-
rescue => e
|
|
289
|
-
nil
|
|
290
|
-
end
|
|
309
|
+
_swallow("make_value_shareable #{val.class}") { Ractor.make_shareable(val) }
|
|
291
310
|
end
|
|
292
311
|
end
|
|
293
312
|
|
|
@@ -301,21 +320,6 @@ module RactorRailsShim
|
|
|
301
320
|
[parent, parts.last.to_sym]
|
|
302
321
|
end
|
|
303
322
|
|
|
304
|
-
# Public API: run after Rails.application.initialize! and BEFORE spawning
|
|
305
|
-
# worker Ractors. Makes every registered constant shareable (deep-freeze).
|
|
306
|
-
# Constants that didn't exist at install time (e.g. Rails::Railtie, loaded
|
|
307
|
-
# after `module Rails` opens) get fixed here. Idempotent; safe to call
|
|
308
|
-
# multiple times. Must run in the main Ractor.
|
|
309
|
-
#
|
|
310
|
-
# NOTE: this does NOT build the framework-config shareable fallback. That
|
|
311
|
-
# step is folded into `make_app_shareable!` because some class_attribute /
|
|
312
|
-
# mattr_accessor values reference the app graph, and making them shareable
|
|
313
|
-
# must happen AFTER the app itself is already frozen (otherwise the app
|
|
314
|
-
# gets frozen prematurely and precompute/proc-replacement can't mutate
|
|
315
|
-
# it). If you call prepare_for_ractors! standalone (without
|
|
316
|
-
# make_app_shareable!), worker Ractors will see nil for framework config
|
|
317
|
-
# values that couldn't be shared without freezing the app — set them
|
|
318
|
-
# explicitly per worker, or use make_app_shareable!.
|
|
319
323
|
# Shared list of every framework patch install method. Both
|
|
320
324
|
# `prepare_for_ractors!` (pre-worker boot) and `make_app_shareable!`
|
|
321
325
|
# (post-boot, pre-freeze) call this, so the two no longer duplicate the
|
|
@@ -403,6 +407,21 @@ module RactorRailsShim
|
|
|
403
407
|
_install_hash_compute_if_absent_patch
|
|
404
408
|
end
|
|
405
409
|
|
|
410
|
+
# Public API: run after Rails.application.initialize! and BEFORE spawning
|
|
411
|
+
# worker Ractors. Makes every registered constant shareable (deep-freeze).
|
|
412
|
+
# Constants that didn't exist at install time (e.g. Rails::Railtie, loaded
|
|
413
|
+
# after `module Rails` opens) get fixed here. Idempotent; safe to call
|
|
414
|
+
# multiple times. Must run in the main Ractor.
|
|
415
|
+
#
|
|
416
|
+
# NOTE: this does NOT build the framework-config shareable fallback. That
|
|
417
|
+
# step is folded into `make_app_shareable!` because some class_attribute /
|
|
418
|
+
# mattr_accessor values reference the app graph, and making them shareable
|
|
419
|
+
# must happen AFTER the app itself is already frozen (otherwise the app
|
|
420
|
+
# gets frozen prematurely and precompute/proc-replacement can't mutate
|
|
421
|
+
# it). If you call prepare_for_ractors! standalone (without
|
|
422
|
+
# make_app_shareable!), worker Ractors will see nil for framework config
|
|
423
|
+
# values that couldn't be shared without freezing the app — set them
|
|
424
|
+
# explicitly per worker, or use make_app_shareable!.
|
|
406
425
|
def prepare_for_ractors!
|
|
407
426
|
do_install_shareable_constants
|
|
408
427
|
RactorRailsShim._freeze_shareable_class_ivars! if RactorRailsShim.respond_to?(:_freeze_shareable_class_ivars!)
|
|
@@ -416,9 +435,9 @@ module RactorRailsShim
|
|
|
416
435
|
_freeze_global_class_ivars!
|
|
417
436
|
_freeze_global_constants!
|
|
418
437
|
_freeze_messages_constants!
|
|
419
|
-
|
|
438
|
+
end
|
|
420
439
|
|
|
421
|
-
|
|
440
|
+
# Verify the runtime matches the versions the shim was developed against.
|
|
422
441
|
# The shim's patches target specific Rails 8.1 class layouts and Ruby 4.0
|
|
423
442
|
# Ractor semantics. On other versions, the patches may silently miss or
|
|
424
443
|
# break things. Behavior on mismatch is governed by `version_policy`:
|
|
@@ -434,9 +453,10 @@ module RactorRailsShim
|
|
|
434
453
|
def _check_version_support
|
|
435
454
|
@version_policy ||= :warn
|
|
436
455
|
unless RactorRailsShim::Version.supported_ruby?
|
|
437
|
-
msg = "ractor-rails-shim: Ruby #{RUBY_VERSION} — shim
|
|
438
|
-
"
|
|
439
|
-
"
|
|
456
|
+
msg = "ractor-rails-shim: Ruby #{RUBY_VERSION} — shim requires " \
|
|
457
|
+
"Ruby >= #{SUPPORTED_RUBY} (frozen-iseq call-cache fix #22075 " \
|
|
458
|
+
"and cross-ractor env-string fix both shipped in 4.0.6). " \
|
|
459
|
+
"Proceeding anyway."
|
|
440
460
|
_version_mismatch(msg)
|
|
441
461
|
end
|
|
442
462
|
if RactorRailsShim::Version.rails &&
|
|
@@ -508,9 +528,27 @@ module RactorRailsShim
|
|
|
508
528
|
# `Zeitwerk::Loader.new` raises IsolationError off the main Ractor).
|
|
509
529
|
def capture_app_constants
|
|
510
530
|
map = {}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
531
|
+
unless defined?(::Rails) && Rails.respond_to?(:autoloaders)
|
|
532
|
+
return map.freeze
|
|
533
|
+
end
|
|
534
|
+
autoloaders = Rails.autoloaders
|
|
535
|
+
# Guard against non-Zeitwerk configurations: Rails.autoloaders may be
|
|
536
|
+
# present (the method exists) but not expose `main`/`once` (e.g. classic
|
|
537
|
+
# loader mode, or `config.autoloaders = false` returning a null object).
|
|
538
|
+
# `main`/`once` may also individually be nil when only one loader is
|
|
539
|
+
# configured. Filter to the loaders that actually expose
|
|
540
|
+
# `all_expected_cpaths` (the Zeitwerk introspection API the capture
|
|
541
|
+
# relies on).
|
|
542
|
+
loaders =
|
|
543
|
+
if autoloaders.respond_to?(:main) && autoloaders.respond_to?(:once)
|
|
544
|
+
[autoloaders.main, autoloaders.once]
|
|
545
|
+
elsif autoloaders.respond_to?(:each)
|
|
546
|
+
autoloaders.to_a
|
|
547
|
+
else
|
|
548
|
+
[]
|
|
549
|
+
end
|
|
550
|
+
loaders.each do |loader|
|
|
551
|
+
next unless loader && loader.respond_to?(:all_expected_cpaths)
|
|
514
552
|
begin
|
|
515
553
|
loader.all_expected_cpaths.values.each do |cpath|
|
|
516
554
|
obj = Object.const_get(cpath) rescue next
|
|
@@ -532,10 +570,16 @@ module RactorRailsShim
|
|
|
532
570
|
# constants in the main Ractor and wraps the frozen, shareable app in a
|
|
533
571
|
# WorkerApp that rebinds those constants (and initializes the worker's
|
|
534
572
|
# ActiveRecord connection) on the first request served by each worker
|
|
535
|
-
# Ractor. Returns a shareable WorkerApp instance
|
|
573
|
+
# Ractor. Returns a shareable WorkerApp instance — frozen and
|
|
574
|
+
# `Ractor.make_shareable`'d — so it can be passed directly to
|
|
575
|
+
# `Ractor.new(worker_app) { |a| a.call(env) }` without the caller having
|
|
576
|
+
# to know the shareability contract.
|
|
536
577
|
def worker_app(frozen_app)
|
|
537
578
|
bindings = capture_app_constants
|
|
538
|
-
WorkerApp.new(frozen_app, bindings)
|
|
579
|
+
wa = WorkerApp.new(frozen_app, bindings)
|
|
580
|
+
wa.freeze
|
|
581
|
+
Ractor.make_shareable(wa)
|
|
582
|
+
wa
|
|
539
583
|
end
|
|
540
584
|
|
|
541
585
|
# See patches/active_model_attribute.rb. When the frozen `:ractor` graph is
|
|
@@ -575,11 +619,25 @@ module RactorRailsShim
|
|
|
575
619
|
# them. But Rails code calls Concurrent::Map#compute_if_absent on these
|
|
576
620
|
# caches (e.g. ActiveModel::AttributeMethods' attribute_method_patterns_
|
|
577
621
|
# cache). Plain Hash lacks that method, so we add a compatible definition.
|
|
622
|
+
#
|
|
623
|
+
# Semantics MUST match Concurrent::Map#compute_if_absent exactly:
|
|
624
|
+
# - if the key is present (even with a nil value), return the stored
|
|
625
|
+
# value WITHOUT invoking the block;
|
|
626
|
+
# - otherwise invoke the block, STORE the result (including nil), and
|
|
627
|
+
# return it.
|
|
628
|
+
# The nil-storing behaviour is load-bearing: Concurrent::Map stores nil
|
|
629
|
+
# results so the block doesn't re-run; the original shim implementation
|
|
630
|
+
# used `IES[key] ||= yield` in the frozen branch, which SKIPS nil — a
|
|
631
|
+
# divergent semantics that re-invokes the block on every lookup for any
|
|
632
|
+
# cache slot whose computed value is nil.
|
|
633
|
+
#
|
|
578
634
|
# For a MUTABLE cache the shim freezes the replaced Hash (so it is shareable
|
|
579
635
|
# across workers); mutating it would raise FrozenError. So when the receiver
|
|
580
636
|
# is frozen we route the store to per-Ractor IES keyed by the Hash identity
|
|
581
637
|
# and the key — giving each worker its own cache entry without mutating the
|
|
582
|
-
# shared object.
|
|
638
|
+
# shared object. The frozen branch mirrors the mutable one: check presence
|
|
639
|
+
# first (via IES key?), invoke the block only on miss, store the result
|
|
640
|
+
# (including nil) via IES[]=.
|
|
583
641
|
def _install_hash_compute_if_absent_patch
|
|
584
642
|
return @hash_compute_if_absent_patched if defined?(@hash_compute_if_absent_patched) && @hash_compute_if_absent_patched
|
|
585
643
|
@hash_compute_if_absent_patched = true
|
|
@@ -587,8 +645,17 @@ module RactorRailsShim
|
|
|
587
645
|
::Hash.prepend(Module.new do
|
|
588
646
|
def compute_if_absent(key)
|
|
589
647
|
if frozen?
|
|
590
|
-
|
|
591
|
-
|
|
648
|
+
# Check the receiver first — a present key (even nil-valued)
|
|
649
|
+
# returns without invoking the block, matching Concurrent::Map.
|
|
650
|
+
return self[key] if key?(key)
|
|
651
|
+
# Per-Ractor IES slot, namespaced by this Hash's object_id so two
|
|
652
|
+
# frozen hashes with the same key don't collide. Use a two-level
|
|
653
|
+
# structure (Hash per receiver) so the presence check (`key?`)
|
|
654
|
+
# and the store share the same slot.
|
|
655
|
+
slot = ActiveSupport::IsolatedExecutionState[:rrs_cia] ||= {}
|
|
656
|
+
bucket = slot[object_id] ||= {}
|
|
657
|
+
return bucket[key] if bucket.key?(key)
|
|
658
|
+
bucket[key] = yield(key)
|
|
592
659
|
elsif key?(key)
|
|
593
660
|
self[key]
|
|
594
661
|
else
|
|
@@ -617,11 +684,9 @@ module RactorRailsShim
|
|
|
617
684
|
# be shareable.
|
|
618
685
|
klass.instance_variables.each do |ivar|
|
|
619
686
|
val = klass.instance_variable_get(ivar)
|
|
620
|
-
next if
|
|
621
|
-
|
|
687
|
+
next if Ractor.shareable?(val)
|
|
688
|
+
_swallow("freeze AR ivar #{klass.name}#{ivar}") do
|
|
622
689
|
Ractor.make_shareable(val)
|
|
623
|
-
rescue
|
|
624
|
-
nil
|
|
625
690
|
end
|
|
626
691
|
end
|
|
627
692
|
end
|
|
@@ -638,11 +703,9 @@ module RactorRailsShim
|
|
|
638
703
|
classes.each do |klass|
|
|
639
704
|
klass.instance_variables.each do |ivar|
|
|
640
705
|
val = klass.instance_variable_get(ivar)
|
|
641
|
-
next if
|
|
642
|
-
|
|
706
|
+
next if Ractor.shareable?(val)
|
|
707
|
+
_swallow("freeze global ivar #{klass}#{ivar}") do
|
|
643
708
|
Ractor.make_shareable(val)
|
|
644
|
-
rescue
|
|
645
|
-
nil
|
|
646
709
|
end
|
|
647
710
|
end
|
|
648
711
|
end
|
|
@@ -699,11 +762,14 @@ module RactorRailsShim
|
|
|
699
762
|
# non-frozen arrays; load hooks never fire again in workers.
|
|
700
763
|
# Gem::LoadError is a ScriptError (not StandardError), so a bare
|
|
701
764
|
# `rescue nil` on the require would NOT catch a missing msgpack gem.
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
765
|
+
# Pre-check for the msgpack gem: active_support/message_pack loads
|
|
766
|
+
# without it but prints a "requires the msgpack gem" warning to $stderr
|
|
767
|
+
# before raising LoadError. Checking first avoids the warning entirely
|
|
768
|
+
# (no stderr suppression needed) and skips the freeze step cleanly when
|
|
769
|
+
# the C extension isn't installed (e.g. in the gem's no-Rails unit specs).
|
|
770
|
+
return unless Gem::Specification.find_all_by_name("msgpack").any?
|
|
771
|
+
|
|
772
|
+
require "active_support/message_pack"
|
|
707
773
|
|
|
708
774
|
mod = (Object.const_get(:ActiveSupport) rescue nil)&.const_get(:Messages, false) rescue nil
|
|
709
775
|
mod = mod&.const_get(:Metadata, false) rescue nil
|
|
@@ -766,8 +832,9 @@ module RactorRailsShim
|
|
|
766
832
|
# 2. ensures the worker's ActiveRecord connection handler is initialized.
|
|
767
833
|
#
|
|
768
834
|
# The wrapper holds only shareable state (@app, @bindings), so the instance
|
|
769
|
-
# is Ractor.make_shareable
|
|
770
|
-
#
|
|
835
|
+
# is `Ractor.make_shareable`'d by `worker_app` before being handed to worker
|
|
836
|
+
# Ractors. `Ractor.current` provides per-worker storage for the one-time
|
|
837
|
+
# guard, avoiding any top-level constant reference.
|
|
771
838
|
class WorkerApp
|
|
772
839
|
def initialize(app, bindings)
|
|
773
840
|
@app = app
|
|
@@ -784,9 +851,12 @@ module RactorRailsShim
|
|
|
784
851
|
def setup_once!
|
|
785
852
|
# All threads inside a worker Ractor share Ractor.current, so a single
|
|
786
853
|
# per-Ractor mutex serializes the one-time init across the worker's
|
|
787
|
-
# threads.
|
|
788
|
-
#
|
|
789
|
-
#
|
|
854
|
+
# threads. `Ractor.current[:key] ||= Thread::Mutex.new` is atomic in
|
|
855
|
+
# Ruby 4.0.6 (verified: 100 concurrent threads produce exactly one
|
|
856
|
+
# mutex object), so the TOCTOU race that would let two threads each
|
|
857
|
+
# create their own mutex does not occur. The `:rrs_worker_ready` guard
|
|
858
|
+
# inside `synchronize` then ensures `rebind_constants` runs exactly
|
|
859
|
+
# once per worker; later threads return early.
|
|
790
860
|
m = Ractor.current[:rrs_worker_mutex] ||= Thread::Mutex.new
|
|
791
861
|
m.synchronize do
|
|
792
862
|
return if Ractor.current[:rrs_worker_ready]
|
|
@@ -91,7 +91,7 @@ module RactorRailsShim
|
|
|
91
91
|
def devise_parameter_filter
|
|
92
92
|
key = :"ractor_rails_shim_devise_param_filter_\#{object_id}"
|
|
93
93
|
v = ActiveSupport::IsolatedExecutionState[key]
|
|
94
|
-
return v
|
|
94
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(key)
|
|
95
95
|
f = Devise::ParameterFilter.new(case_insensitive_keys, strip_whitespace_keys)
|
|
96
96
|
ActiveSupport::IsolatedExecutionState[key] = f
|
|
97
97
|
f
|
|
@@ -42,7 +42,7 @@ module RactorRailsShim
|
|
|
42
42
|
ew.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
43
43
|
def active_key
|
|
44
44
|
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
45
|
-
return v
|
|
45
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{key_str})
|
|
46
46
|
sym = :"active_execution_wrapper_\#{object_id}"
|
|
47
47
|
ActiveSupport::IsolatedExecutionState[#{key_str}] = sym
|
|
48
48
|
sym
|
|
@@ -62,7 +62,7 @@ module RactorRailsShim
|
|
|
62
62
|
::Kaminari.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
63
63
|
def config
|
|
64
64
|
v = ActiveSupport::IsolatedExecutionState[#{k_key_str}]
|
|
65
|
-
return v
|
|
65
|
+
return v if ActiveSupport::IsolatedExecutionState.key?(#{k_key_str})
|
|
66
66
|
if Ractor.main? && instance_variable_defined?(:@_config)
|
|
67
67
|
@_config
|
|
68
68
|
else
|