Skip to content

Commit 192a652

Browse files
authored
Merge pull request rails#43596 from Shopify/active-support-local-store
Introduce `ActiveSupport::IsolatedExecutionState` for internal use
2 parents 2c96efb + 540d2f4 commit 192a652

File tree

9 files changed

+137
-18
lines changed

9 files changed

+137
-18
lines changed

activesupport/lib/active_support.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module ActiveSupport
4747
autoload :EventedFileUpdateChecker
4848
autoload :ForkTracker
4949
autoload :LogSubscriber
50+
autoload :IsolatedExecutionState
5051
autoload :Notifications
5152
autoload :Reloader
5253
autoload :SecureCompareRotator
@@ -115,10 +116,6 @@ def self.utc_to_local_returns_utc_offset_times=(value)
115116
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
116117
end
117118

118-
def self.current_attributes_use_thread_variables=(value)
119-
CurrentAttributes._use_thread_variables = value
120-
end
121-
122119
@has_native_class_descendants = Class.method_defined?(:descendants) # RUBY_VERSION >= "3.1"
123120
end
124121

activesupport/lib/active_support/current_attributes.rb

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,13 @@ def clear_all # :nodoc:
155155
current_instances.clear
156156
end
157157

158-
def _use_thread_variables=(value) # :nodoc:
159-
clear_all
160-
@@use_thread_variables = value
161-
end
162-
@@use_thread_variables = false
163-
164158
private
165159
def generated_attribute_methods
166160
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
167161
end
168162

169163
def current_instances
170-
if @@use_thread_variables
171-
Thread.current.thread_variable_get(:current_attributes_instances) ||
172-
Thread.current.thread_variable_set(:current_attributes_instances, {})
173-
else
174-
Thread.current[:current_attributes_instances] ||= {}
175-
end
164+
IsolatedExecutionState[:current_attributes_instances] ||= {}
176165
end
177166

178167
def current_instances_key
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
require "fiber"
4+
5+
module ActiveSupport
6+
module IsolatedExecutionState # :nodoc:
7+
@isolation_level = :thread
8+
9+
Thread.attr_accessor :active_support_execution_state
10+
Fiber.attr_accessor :active_support_execution_state
11+
12+
class << self
13+
attr_reader :isolation_level
14+
15+
def isolation_level=(level)
16+
unless %i(thread fiber).include?(level)
17+
raise ArgumentError, "isolation_level must be `:thread` or `:fiber`, got: `#{level.inspect}`"
18+
end
19+
20+
if level != isolation_level
21+
clear
22+
singleton_class.alias_method(:current, "current_#{level}")
23+
singleton_class.send(:private, :current)
24+
@isolation_level = level
25+
end
26+
end
27+
28+
def [](key)
29+
current[key]
30+
end
31+
32+
def []=(key, value)
33+
current[key] = value
34+
end
35+
36+
def clear
37+
current.clear
38+
end
39+
40+
private
41+
def current_thread
42+
Thread.current.active_support_execution_state ||= {}
43+
end
44+
45+
def current_fiber
46+
Fiber.current.active_support_execution_state ||= {}
47+
end
48+
49+
alias_method :current, :current_thread
50+
end
51+
end
52+
end

activesupport/lib/active_support/railtie.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ class Railtie < Rails::Railtie # :nodoc:
99

1010
config.eager_load_namespaces << ActiveSupport
1111

12+
initializer "active_support.isolation_level" do |app|
13+
if level = app.config.active_support.delete(:isolation_level)
14+
ActiveSupport::IsolatedExecutionState.isolation_level = level
15+
end
16+
end
17+
1218
initializer "active_support.remove_deprecated_time_with_zone_name" do |app|
1319
config.after_initialize do
1420
if app.config.active_support.remove_deprecated_time_with_zone_name

activesupport/test/current_attributes_test.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,28 @@ def after_teardown
177177
end
178178

179179
test "CurrentAttributes use fiber-local variables" do
180+
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
181+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
182+
180183
Session.current = 42
181184
enumerator = Enumerator.new do |yielder|
182185
yielder.yield Session.current
183186
end
184187
assert_nil enumerator.next
188+
ensure
189+
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
185190
end
186191

187192
test "CurrentAttributes can use thread-local variables" do
188-
ActiveSupport::CurrentAttributes._use_thread_variables = true
193+
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
194+
ActiveSupport::IsolatedExecutionState.isolation_level = :thread
195+
189196
Session.current = 42
190197
enumerator = Enumerator.new do |yielder|
191198
yielder.yield Session.current
192199
end
193200
assert_equal 42, enumerator.next
194201
ensure
195-
ActiveSupport::CurrentAttributes._use_thread_variables = false
202+
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
196203
end
197204
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "abstract_unit"
4+
5+
class IsolatedExecutionStateTest < ActiveSupport::TestCase
6+
setup do
7+
ActiveSupport::IsolatedExecutionState.clear
8+
@original_isolation_level = ActiveSupport::IsolatedExecutionState.isolation_level
9+
end
10+
11+
teardown do
12+
ActiveSupport::IsolatedExecutionState.clear
13+
ActiveSupport::IsolatedExecutionState.isolation_level = @original_isolation_level
14+
end
15+
16+
test "#[] when isolation level is :fiber" do
17+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
18+
19+
ActiveSupport::IsolatedExecutionState[:test] = 42
20+
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
21+
enumerator = Enumerator.new do |yielder|
22+
yielder.yield ActiveSupport::IsolatedExecutionState[:test]
23+
end
24+
assert_nil enumerator.next
25+
26+
assert_nil Thread.new { ActiveSupport::IsolatedExecutionState[:test] }.value
27+
end
28+
29+
test "#[] when isolation level is :thread" do
30+
ActiveSupport::IsolatedExecutionState.isolation_level = :thread
31+
32+
ActiveSupport::IsolatedExecutionState[:test] = 42
33+
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
34+
enumerator = Enumerator.new do |yielder|
35+
yielder.yield ActiveSupport::IsolatedExecutionState[:test]
36+
end
37+
assert_equal 42, enumerator.next
38+
39+
assert_nil Thread.new { ActiveSupport::IsolatedExecutionState[:test] }.value
40+
end
41+
42+
test "changing the isolation level clear the old store" do
43+
original = ActiveSupport::IsolatedExecutionState.isolation_level
44+
other = ActiveSupport::IsolatedExecutionState.isolation_level == :fiber ? :thread : :fiber
45+
46+
ActiveSupport::IsolatedExecutionState[:test] = 42
47+
ActiveSupport::IsolatedExecutionState.isolation_level = original
48+
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
49+
50+
ActiveSupport::IsolatedExecutionState.isolation_level = other
51+
assert_nil ActiveSupport::IsolatedExecutionState[:test]
52+
53+
ActiveSupport::IsolatedExecutionState.isolation_level = original
54+
assert_nil ActiveSupport::IsolatedExecutionState[:test]
55+
end
56+
end

guides/source/configuring.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,11 @@ Configures deprecation warnings that the Application considers disallowed. This
13841384
13851385
Allows you to disable all deprecation warnings (including disallowed deprecations); it makes `ActiveSupport::Deprecation.warn` a no-op. This is enabled by default in production.
13861386
1387+
#### `active_support.isolation_level`
1388+
1389+
Configures the locality of most of Rails internal state. If you use a fiber based server or job processor (e.g. `falcon`), you should set it to `:fiber`.
1390+
Otherwise it is best to use `:thread` locality.
1391+
13871392
#### `config.active_support.use_rfc4122_namespaced_uuids`
13881393
13891394
Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
@@ -1809,6 +1814,7 @@ Accepts a string for the HTML tag used to wrap attachments. Defaults to `"action
18091814
- `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA1`
18101815
- `config.active_support.cache_format_version`: `6.1`
18111816
- `config.active_support.executor_around_test_case`: `false`
1817+
- `active_support.isolation_level`: `:thread`
18121818
- ``config.active_support.use_rfc4122_namespaced_uuids``: `false`
18131819
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `true`
18141820
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`

railties/lib/rails/application/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def load_defaults(target_version)
214214
active_support.cache_format_version = 7.0
215215
active_support.use_rfc4122_namespaced_uuids = true
216216
active_support.executor_around_test_case = true
217+
active_support.isolation_level = :thread
217218
end
218219

219220
if respond_to?(:action_mailer)

railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_0.rb.tt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
# and asynchronous queries will then be enabled.
5252
# Rails.application.config.active_support.executor_around_test_case = true
5353

54+
# Define the isolation level of most of Rails internal state.
55+
# If you use a fiber based server or job processor, you should set it to `:fiber`.
56+
# Otherwise the default of `:thread` if preferable.
57+
# Rails.application.config.active_support.isolation_level = :thread
58+
5459
# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
5560
# Rails.application.config.action_mailer.smtp_timeout = 5
5661

0 commit comments

Comments
 (0)