Introduce ActiveSupport::IsolatedExecutionState for internal use
Many places in Active Support and Rails in general use `Thread.current#[]` to store "request (or job) local data". This often cause problems with `Enumerator` because it runs in a different fiber. On the other hand, some places migrated to `Thread#thread_variable_get` which cause issues with fiber based servers (`falcon`). Based on this, I believe the isolation level should be an application configuration. For backward compatibility it could ship with `:fiber` isolation as a default but longer term :thread would make more sense as it would work fine for all deployment targets except falcon. Ref: https://github.com/rails/rails/pull/38905 Ref: https://github.com/rails/rails/pull/39428 Ref: https://github.com/rails/rails/pull/34495 (and possibly many others)
This commit is contained in:
parent
1a06f5dc09
commit
540d2f41f6
|
@ -47,6 +47,7 @@ module ActiveSupport
|
||||||
autoload :EventedFileUpdateChecker
|
autoload :EventedFileUpdateChecker
|
||||||
autoload :ForkTracker
|
autoload :ForkTracker
|
||||||
autoload :LogSubscriber
|
autoload :LogSubscriber
|
||||||
|
autoload :IsolatedExecutionState
|
||||||
autoload :Notifications
|
autoload :Notifications
|
||||||
autoload :Reloader
|
autoload :Reloader
|
||||||
autoload :SecureCompareRotator
|
autoload :SecureCompareRotator
|
||||||
|
@ -115,10 +116,6 @@ module ActiveSupport
|
||||||
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
|
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.current_attributes_use_thread_variables=(value)
|
|
||||||
CurrentAttributes._use_thread_variables = value
|
|
||||||
end
|
|
||||||
|
|
||||||
@has_native_class_descendants = Class.method_defined?(:descendants) # RUBY_VERSION >= "3.1"
|
@has_native_class_descendants = Class.method_defined?(:descendants) # RUBY_VERSION >= "3.1"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -155,24 +155,13 @@ module ActiveSupport
|
||||||
current_instances.clear
|
current_instances.clear
|
||||||
end
|
end
|
||||||
|
|
||||||
def _use_thread_variables=(value) # :nodoc:
|
|
||||||
clear_all
|
|
||||||
@@use_thread_variables = value
|
|
||||||
end
|
|
||||||
@@use_thread_variables = false
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def generated_attribute_methods
|
def generated_attribute_methods
|
||||||
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_instances
|
def current_instances
|
||||||
if @@use_thread_variables
|
IsolatedExecutionState[:current_attributes_instances] ||= {}
|
||||||
Thread.current.thread_variable_get(:current_attributes_instances) ||
|
|
||||||
Thread.current.thread_variable_set(:current_attributes_instances, {})
|
|
||||||
else
|
|
||||||
Thread.current[:current_attributes_instances] ||= {}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_instances_key
|
def current_instances_key
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "fiber"
|
||||||
|
|
||||||
|
module ActiveSupport
|
||||||
|
module IsolatedExecutionState # :nodoc:
|
||||||
|
@isolation_level = :thread
|
||||||
|
|
||||||
|
Thread.attr_accessor :active_support_execution_state
|
||||||
|
Fiber.attr_accessor :active_support_execution_state
|
||||||
|
|
||||||
|
class << self
|
||||||
|
attr_reader :isolation_level
|
||||||
|
|
||||||
|
def isolation_level=(level)
|
||||||
|
unless %i(thread fiber).include?(level)
|
||||||
|
raise ArgumentError, "isolation_level must be `:thread` or `:fiber`, got: `#{level.inspect}`"
|
||||||
|
end
|
||||||
|
|
||||||
|
if level != isolation_level
|
||||||
|
clear
|
||||||
|
singleton_class.alias_method(:current, "current_#{level}")
|
||||||
|
singleton_class.send(:private, :current)
|
||||||
|
@isolation_level = level
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
current[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(key, value)
|
||||||
|
current[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
current.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def current_thread
|
||||||
|
Thread.current.active_support_execution_state ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_fiber
|
||||||
|
Fiber.current.active_support_execution_state ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :current, :current_thread
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,12 @@ module ActiveSupport
|
||||||
|
|
||||||
config.eager_load_namespaces << ActiveSupport
|
config.eager_load_namespaces << ActiveSupport
|
||||||
|
|
||||||
|
initializer "active_support.isolation_level" do |app|
|
||||||
|
if level = app.config.active_support.delete(:isolation_level)
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = level
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
initializer "active_support.remove_deprecated_time_with_zone_name" do |app|
|
initializer "active_support.remove_deprecated_time_with_zone_name" do |app|
|
||||||
config.after_initialize do
|
config.after_initialize do
|
||||||
if app.config.active_support.remove_deprecated_time_with_zone_name
|
if app.config.active_support.remove_deprecated_time_with_zone_name
|
||||||
|
|
|
@ -177,21 +177,28 @@ class CurrentAttributesTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "CurrentAttributes use fiber-local variables" do
|
test "CurrentAttributes use fiber-local variables" do
|
||||||
|
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
||||||
|
|
||||||
Session.current = 42
|
Session.current = 42
|
||||||
enumerator = Enumerator.new do |yielder|
|
enumerator = Enumerator.new do |yielder|
|
||||||
yielder.yield Session.current
|
yielder.yield Session.current
|
||||||
end
|
end
|
||||||
assert_nil enumerator.next
|
assert_nil enumerator.next
|
||||||
|
ensure
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
|
||||||
end
|
end
|
||||||
|
|
||||||
test "CurrentAttributes can use thread-local variables" do
|
test "CurrentAttributes can use thread-local variables" do
|
||||||
ActiveSupport::CurrentAttributes._use_thread_variables = true
|
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = :thread
|
||||||
|
|
||||||
Session.current = 42
|
Session.current = 42
|
||||||
enumerator = Enumerator.new do |yielder|
|
enumerator = Enumerator.new do |yielder|
|
||||||
yielder.yield Session.current
|
yielder.yield Session.current
|
||||||
end
|
end
|
||||||
assert_equal 42, enumerator.next
|
assert_equal 42, enumerator.next
|
||||||
ensure
|
ensure
|
||||||
ActiveSupport::CurrentAttributes._use_thread_variables = false
|
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "abstract_unit"
|
||||||
|
|
||||||
|
class IsolatedExecutionStateTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
ActiveSupport::IsolatedExecutionState.clear
|
||||||
|
@original_isolation_level = ActiveSupport::IsolatedExecutionState.isolation_level
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ActiveSupport::IsolatedExecutionState.clear
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = @original_isolation_level
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#[] when isolation level is :fiber" do
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
||||||
|
|
||||||
|
ActiveSupport::IsolatedExecutionState[:test] = 42
|
||||||
|
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
enumerator = Enumerator.new do |yielder|
|
||||||
|
yielder.yield ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
end
|
||||||
|
assert_nil enumerator.next
|
||||||
|
|
||||||
|
assert_nil Thread.new { ActiveSupport::IsolatedExecutionState[:test] }.value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#[] when isolation level is :thread" do
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = :thread
|
||||||
|
|
||||||
|
ActiveSupport::IsolatedExecutionState[:test] = 42
|
||||||
|
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
enumerator = Enumerator.new do |yielder|
|
||||||
|
yielder.yield ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
end
|
||||||
|
assert_equal 42, enumerator.next
|
||||||
|
|
||||||
|
assert_nil Thread.new { ActiveSupport::IsolatedExecutionState[:test] }.value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "changing the isolation level clear the old store" do
|
||||||
|
original = ActiveSupport::IsolatedExecutionState.isolation_level
|
||||||
|
other = ActiveSupport::IsolatedExecutionState.isolation_level == :fiber ? :thread : :fiber
|
||||||
|
|
||||||
|
ActiveSupport::IsolatedExecutionState[:test] = 42
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = original
|
||||||
|
assert_equal 42, ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = other
|
||||||
|
assert_nil ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
|
||||||
|
ActiveSupport::IsolatedExecutionState.isolation_level = original
|
||||||
|
assert_nil ActiveSupport::IsolatedExecutionState[:test]
|
||||||
|
end
|
||||||
|
end
|
|
@ -1384,6 +1384,11 @@ Configures deprecation warnings that the Application considers disallowed. This
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
#### `active_support.isolation_level`
|
||||||
|
|
||||||
|
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`.
|
||||||
|
Otherwise it is best to use `:thread` locality.
|
||||||
|
|
||||||
#### `config.active_support.use_rfc4122_namespaced_uuids`
|
#### `config.active_support.use_rfc4122_namespaced_uuids`
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
@ -1816,6 +1821,7 @@ Accepts a string for the HTML tag used to wrap attachments. Defaults to `"action
|
||||||
- `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA1`
|
- `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA1`
|
||||||
- `config.active_support.cache_format_version`: `6.1`
|
- `config.active_support.cache_format_version`: `6.1`
|
||||||
- `config.active_support.executor_around_test_case`: `false`
|
- `config.active_support.executor_around_test_case`: `false`
|
||||||
|
- `active_support.isolation_level`: `:thread`
|
||||||
- ``config.active_support.use_rfc4122_namespaced_uuids``: `false`
|
- ``config.active_support.use_rfc4122_namespaced_uuids``: `false`
|
||||||
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `true`
|
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `true`
|
||||||
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`
|
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`
|
||||||
|
|
|
@ -215,6 +215,7 @@ module Rails
|
||||||
active_support.cache_format_version = 7.0
|
active_support.cache_format_version = 7.0
|
||||||
active_support.use_rfc4122_namespaced_uuids = true
|
active_support.use_rfc4122_namespaced_uuids = true
|
||||||
active_support.executor_around_test_case = true
|
active_support.executor_around_test_case = true
|
||||||
|
active_support.isolation_level = :thread
|
||||||
end
|
end
|
||||||
|
|
||||||
if respond_to?(:action_mailer)
|
if respond_to?(:action_mailer)
|
||||||
|
|
|
@ -51,6 +51,11 @@
|
||||||
# and asynchronous queries will then be enabled.
|
# and asynchronous queries will then be enabled.
|
||||||
# Rails.application.config.active_support.executor_around_test_case = true
|
# Rails.application.config.active_support.executor_around_test_case = true
|
||||||
|
|
||||||
|
# Define the isolation level of most of Rails internal state.
|
||||||
|
# If you use a fiber based server or job processor, you should set it to `:fiber`.
|
||||||
|
# Otherwise the default of `:thread` if preferable.
|
||||||
|
# Rails.application.config.active_support.isolation_level = :thread
|
||||||
|
|
||||||
# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
|
# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
|
||||||
# Rails.application.config.action_mailer.smtp_timeout = 5
|
# Rails.application.config.action_mailer.smtp_timeout = 5
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue