Use `IsolatedExecutionState` across Active Support

Ref: https://github.com/rails/rails/pull/43596

This allow users to declare wether their unit of work is isolated by
fibers or by threads.

`PerThreadRegistry` and `thread_mattr_accessor` were intentionally left
out as they require documentation change. I'll submit them in separate
pull requests.
This commit is contained in:
Jean Boussier 2021-11-19 09:57:04 +01:00
parent 3b01e92d52
commit 0ea374c81f
11 changed files with 54 additions and 26 deletions

View File

@ -17,7 +17,7 @@ class Date
# If <tt>Date.beginning_of_week</tt> has not been set for the current request, returns the week start specified in <tt>config.beginning_of_week</tt>.
# If no config.beginning_of_week was specified, returns :monday.
def beginning_of_week
Thread.current[:beginning_of_week] || beginning_of_week_default || :monday
::ActiveSupport::IsolatedExecutionState[:beginning_of_week] || beginning_of_week_default || :monday
end
# Sets <tt>Date.beginning_of_week</tt> to a week start (e.g. :monday) for current request/thread.
@ -25,7 +25,7 @@ class Date
# This method accepts any of the following day symbols:
# :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
def beginning_of_week=(week_start)
Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
::ActiveSupport::IsolatedExecutionState[:beginning_of_week] = find_beginning_of_week!(week_start)
end
# Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.

View File

@ -12,7 +12,7 @@ class Time
# Returns the TimeZone for the current request, if this has been set (via Time.zone=).
# If <tt>Time.zone</tt> has not been set for the current request, returns the TimeZone specified in <tt>config.time_zone</tt>.
def zone
Thread.current[:time_zone] || zone_default
::ActiveSupport::IsolatedExecutionState[:time_zone] || zone_default
end
# Sets <tt>Time.zone</tt> to a TimeZone object for the current request/thread.
@ -39,7 +39,7 @@ class Time
# end
# end
def zone=(time_zone)
Thread.current[:time_zone] = find_zone!(time_zone)
::ActiveSupport::IsolatedExecutionState[:time_zone] = find_zone!(time_zone)
end
# Allows override of <tt>Time.zone</tt> locally inside supplied block;

View File

@ -46,7 +46,7 @@ module ActiveSupport
private
def store
Thread.current[:active_support_execution_context] ||= {}
IsolatedExecutionState[:active_support_execution_context] ||= {}
end
end
end

View File

@ -113,11 +113,11 @@ module ActiveSupport
self.active = Concurrent::Hash.new
def self.active? # :nodoc:
@active[Thread.current]
@active[IsolatedExecutionState.unique_id]
end
def run! # :nodoc:
self.class.active[Thread.current] = true
self.class.active[IsolatedExecutionState.unique_id] = true
run
end
@ -132,7 +132,7 @@ module ActiveSupport
def complete!
complete
ensure
self.class.active.delete Thread.current
self.class.active.delete(IsolatedExecutionState.unique_id)
end
def complete # :nodoc:

View File

@ -25,6 +25,10 @@ module ActiveSupport
end
end
def unique_id
self[:__id__] ||= Object.new
end
def [](key)
current[key]
end

View File

@ -18,8 +18,7 @@ module ActiveSupport
end
def local_level
# Note: Thread#[] is fiber-local
Thread.current[:logger_thread_safe_level]
IsolatedExecutionState[:logger_thread_safe_level]
end
def local_level=(level)
@ -31,7 +30,7 @@ module ActiveSupport
else
raise ArgumentError, "Invalid log level: #{level.inspect}"
end
Thread.current[:logger_thread_safe_level] = level
IsolatedExecutionState[:logger_thread_safe_level] = level
end
def level

View File

@ -226,12 +226,12 @@ module ActiveSupport
end
def start(name, id, payload)
timestack = Thread.current[:_timestack] ||= []
timestack = IsolatedExecutionState[:_timestack] ||= []
timestack.push Time.now
end
def finish(name, id, payload)
timestack = Thread.current[:_timestack]
timestack = IsolatedExecutionState[:_timestack]
started = timestack.pop
@delegate.call(name, started, Time.now, id, payload)
end
@ -243,12 +243,12 @@ module ActiveSupport
end
def start(name, id, payload)
timestack = Thread.current[:_timestack_monotonic] ||= []
timestack = IsolatedExecutionState[:_timestack_monotonic] ||= []
timestack.push Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def finish(name, id, payload)
timestack = Thread.current[:_timestack_monotonic]
timestack = IsolatedExecutionState[:_timestack_monotonic]
started = timestack.pop
@delegate.call(name, started, Process.clock_gettime(Process::CLOCK_MONOTONIC), id, payload)
end
@ -256,14 +256,14 @@ module ActiveSupport
class EventObject < Evented
def start(name, id, payload)
stack = Thread.current[:_event_stack] ||= []
stack = IsolatedExecutionState[:_event_stack] ||= []
event = build_event name, id, payload
event.start!
stack.push event
end
def finish(name, id, payload)
stack = Thread.current[:_event_stack]
stack = IsolatedExecutionState[:_event_stack]
event = stack.pop
event.payload = payload
event.finish!

View File

@ -57,7 +57,7 @@ module ActiveSupport
def current_tags
# We use our object ID here to avoid conflicting with other instances
thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}"
Thread.current[thread_key] ||= []
IsolatedExecutionState[thread_key] ||= []
end
def tags_text

View File

@ -181,11 +181,11 @@ module ActiveSupport
end
def current_thread_backend
Thread.current[:xml_mini_backend]
IsolatedExecutionState[:xml_mini_backend]
end
def current_thread_backend=(name)
Thread.current[:xml_mini_backend] = name && cast_backend_name_to_module(name)
IsolatedExecutionState[:xml_mini_backend] = name && cast_backend_name_to_module(name)
end
def cast_backend_name_to_module(name)

View File

@ -1205,13 +1205,11 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_time_zone_setter_is_thread_safe
Time.use_zone "Paris" do
t1 = Thread.new { Time.zone = "Alaska" }.join
t2 = Thread.new { Time.zone = "Hawaii" }.join
assert t1.stop?, "Thread 1 did not finish running"
assert t2.stop?, "Thread 2 did not finish running"
t1 = Thread.new { Time.zone = "Alaska"; Time.zone }
t2 = Thread.new { Time.zone = "Hawaii"; Time.zone }
assert_equal ActiveSupport::TimeZone["Paris"], Time.zone
assert_equal ActiveSupport::TimeZone["Alaska"], t1[:time_zone]
assert_equal ActiveSupport::TimeZone["Hawaii"], t2[:time_zone]
assert_equal ActiveSupport::TimeZone["Alaska"], t1.value
assert_equal ActiveSupport::TimeZone["Hawaii"], t2.value
end
end

View File

@ -272,6 +272,9 @@ class LoggerTest < ActiveSupport::TestCase
end
def test_logger_level_main_fiber_safety
previous_isolation_level = ActiveSupport::IsolatedExecutionState.isolation_level
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
@logger.level = Logger::INFO
assert_level(Logger::INFO)
@ -283,9 +286,14 @@ class LoggerTest < ActiveSupport::TestCase
assert_level(Logger::ERROR)
fiber.resume
end
ensure
ActiveSupport::IsolatedExecutionState.isolation_level = previous_isolation_level
end
def test_logger_level_local_fiber_safety
previous_isolation_level = ActiveSupport::IsolatedExecutionState.isolation_level
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
@logger.level = Logger::INFO
assert_level(Logger::INFO)
@ -313,6 +321,25 @@ class LoggerTest < ActiveSupport::TestCase
end.resume
assert_level(Logger::INFO)
ensure
ActiveSupport::IsolatedExecutionState.isolation_level = previous_isolation_level
end
def test_logger_level_thread_safety
previous_isolation_level = ActiveSupport::IsolatedExecutionState.isolation_level
ActiveSupport::IsolatedExecutionState.isolation_level = :thread
@logger.level = Logger::INFO
assert_level(Logger::INFO)
enumerator = Enumerator.new do |yielder|
@logger.level = Logger::DEBUG
yielder.yield @logger.level
end
assert_equal Logger::DEBUG, enumerator.next
assert_level(Logger::DEBUG)
ensure
ActiveSupport::IsolatedExecutionState.isolation_level = previous_isolation_level
end
def test_temporarily_logging_at_a_noisier_level