mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
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
9 changed files with 137 additions and 18 deletions
|
@ -47,6 +47,7 @@ module ActiveSupport
|
|||
autoload :EventedFileUpdateChecker
|
||||
autoload :ForkTracker
|
||||
autoload :LogSubscriber
|
||||
autoload :IsolatedExecutionState
|
||||
autoload :Notifications
|
||||
autoload :Reloader
|
||||
autoload :SecureCompareRotator
|
||||
|
@ -115,10 +116,6 @@ module ActiveSupport
|
|||
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
|
||||
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"
|
||||
end
|
||||
|
||||
|
|
|
@ -155,24 +155,13 @@ module ActiveSupport
|
|||
current_instances.clear
|
||||
end
|
||||
|
||||
def _use_thread_variables=(value) # :nodoc:
|
||||
clear_all
|
||||
@@use_thread_variables = value
|
||||
end
|
||||
@@use_thread_variables = false
|
||||
|
||||
private
|
||||
def generated_attribute_methods
|
||||
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
||||
end
|
||||
|
||||
def current_instances
|
||||
if @@use_thread_variables
|
||||
Thread.current.thread_variable_get(:current_attributes_instances) ||
|
||||
Thread.current.thread_variable_set(:current_attributes_instances, {})
|
||||
else
|
||||
Thread.current[:current_attributes_instances] ||= {}
|
||||
end
|
||||
IsolatedExecutionState[:current_attributes_instances] ||= {}
|
||||
end
|
||||
|
||||
def current_instances_key
|
||||
|
|
52
activesupport/lib/active_support/isolated_execution_state.rb
Normal file
52
activesupport/lib/active_support/isolated_execution_state.rb
Normal file
|
@ -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
|
||||
|
||||
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|
|
||||
config.after_initialize do
|
||||
if app.config.active_support.remove_deprecated_time_with_zone_name
|
||||
|
|
|
@ -177,21 +177,28 @@ class CurrentAttributesTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "CurrentAttributes use fiber-local variables" do
|
||||
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
|
||||
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
||||
|
||||
Session.current = 42
|
||||
enumerator = Enumerator.new do |yielder|
|
||||
yielder.yield Session.current
|
||||
end
|
||||
assert_nil enumerator.next
|
||||
ensure
|
||||
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
|
||||
end
|
||||
|
||||
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
|
||||
enumerator = Enumerator.new do |yielder|
|
||||
yielder.yield Session.current
|
||||
end
|
||||
assert_equal 42, enumerator.next
|
||||
ensure
|
||||
ActiveSupport::CurrentAttributes._use_thread_variables = false
|
||||
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
|
||||
end
|
||||
end
|
||||
|
|
56
activesupport/test/isolated_execution_state_test.rb
Normal file
56
activesupport/test/isolated_execution_state_test.rb
Normal file
|
@ -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.
|
||||
|
||||
#### `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`
|
||||
|
||||
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.cache_format_version`: `6.1`
|
||||
- `config.active_support.executor_around_test_case`: `false`
|
||||
- `active_support.isolation_level`: `:thread`
|
||||
- ``config.active_support.use_rfc4122_namespaced_uuids``: `false`
|
||||
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `true`
|
||||
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`
|
||||
|
|
|
@ -215,6 +215,7 @@ module Rails
|
|||
active_support.cache_format_version = 7.0
|
||||
active_support.use_rfc4122_namespaced_uuids = true
|
||||
active_support.executor_around_test_case = true
|
||||
active_support.isolation_level = :thread
|
||||
end
|
||||
|
||||
if respond_to?(:action_mailer)
|
||||
|
|
|
@ -51,6 +51,11 @@
|
|||
# and asynchronous queries will then be enabled.
|
||||
# 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.
|
||||
# Rails.application.config.action_mailer.smtp_timeout = 5
|
||||
|
||||
|
|
Loading…
Reference in a new issue