Skip to content

Commit c354e23

Browse files
authored
Merge pull request rails#46553 from Shopify/thread-lock
Make AbstractAdapter#lock thread local by default
2 parents d79b7d5 + 6253874 commit c354e23

File tree

5 files changed

+93
-13
lines changed

5 files changed

+93
-13
lines changed

activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def lock_thread=(lock_thread)
175175
end
176176

177177
if (active_connection = @thread_cached_conns[connection_cache_key(current_thread)])
178-
active_connection.synchronized = lock_thread
178+
active_connection.lock_thread = @lock_thread
179179
end
180180
end
181181

@@ -357,7 +357,7 @@ def clear_reloadable_connections!
357357
# - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
358358
def checkout(checkout_timeout = @checkout_timeout)
359359
connection = checkout_and_verify(acquire_connection(checkout_timeout))
360-
connection.synchronized = @lock_thread
360+
connection.lock_thread = @lock_thread
361361
connection
362362
end
363363

@@ -375,6 +375,7 @@ def checkin(conn)
375375
conn.expire
376376
end
377377

378+
conn.lock_thread = nil
378379
@available.add conn
379380
end
380381
end

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def initialize(config_or_deprecated_connection, deprecated_logger = nil, depreca
156156
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
157157
@visitor = arel_visitor
158158
@statements = build_statement_pool
159-
self.synchronized = false
159+
self.lock_thread = nil
160160

161161
@prepared_statements = self.class.type_cast_config_to_boolean(
162162
@config.fetch(:prepared_statements) { default_prepared_statements }
@@ -172,8 +172,12 @@ def initialize(config_or_deprecated_connection, deprecated_logger = nil, depreca
172172
@verified = false
173173
end
174174

175-
def synchronized=(synchronized) # :nodoc:
176-
@lock = if synchronized
175+
def lock_thread=(lock_thread) # :nodoc:
176+
@lock =
177+
case lock_thread
178+
when Thread
179+
ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
180+
when Fiber
177181
ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
178182
else
179183
ActiveSupport::Concurrency::NullLock

activerecord/test/cases/connection_pool_test.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,19 @@ def setup
801801
@connection_test_model_class = ThreadConnectionTestModel
802802
end
803803

804+
def test_lock_thread_allow_fiber_reentrency
805+
@pool.lock_thread = true
806+
connection = @pool.checkout
807+
connection.transaction do
808+
enumerator = Enumerator.new do |yielder|
809+
connection.transaction do
810+
yielder.yield 1
811+
end
812+
end
813+
assert_equal 1, enumerator.next
814+
end
815+
end
816+
804817
private
805818
def new_thread(...)
806819
Thread.new(...)

activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
module ActiveSupport
66
module Concurrency
7-
# A monitor that will permit dependency loading while blocked waiting for
8-
# the lock.
9-
class LoadInterlockAwareMonitor < Monitor
7+
module LoadInterlockAwareMonitorMixin # :nodoc:
108
EXCEPTION_NEVER = { Exception => :never }.freeze
119
EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
1210
private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
@@ -29,5 +27,46 @@ def synchronize(&block)
2927
end
3028
end
3129
end
30+
# A monitor that will permit dependency loading while blocked waiting for
31+
# the lock.
32+
class LoadInterlockAwareMonitor < Monitor
33+
include LoadInterlockAwareMonitorMixin
34+
end
35+
36+
class ThreadLoadInterlockAwareMonitor # :nodoc:
37+
prepend LoadInterlockAwareMonitorMixin
38+
39+
def initialize
40+
@owner = nil
41+
@count = 0
42+
@mutex = Mutex.new
43+
end
44+
45+
private
46+
def mon_try_enter
47+
if @owner != Thread.current
48+
return false unless @mutex.try_lock
49+
@owner = Thread.current
50+
end
51+
@count += 1
52+
end
53+
54+
def mon_enter
55+
@mutex.lock if @owner != Thread.current
56+
@owner = Thread.current
57+
@count += 1
58+
end
59+
60+
def mon_exit
61+
unless @owner == Thread.current
62+
raise ThreadError, "current thread not owner"
63+
end
64+
65+
@count -= 1
66+
return unless @count == 0
67+
@owner = nil
68+
@mutex.unlock
69+
end
70+
end
3271
end
3372
end

activesupport/test/concurrency/load_interlock_aware_monitor_test.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66

77
module ActiveSupport
88
module Concurrency
9-
class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase
10-
def setup
11-
@monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
12-
end
13-
9+
module LoadInterlockAwareMonitorTests
1410
def test_entering_with_no_blocking
1511
assert @monitor.mon_enter
1612
end
@@ -51,5 +47,32 @@ def test_entering_with_blocking
5147
assert able_to_load
5248
end
5349
end
50+
51+
class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase
52+
include LoadInterlockAwareMonitorTests
53+
54+
def setup
55+
@monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
56+
end
57+
end
58+
59+
class ThreadLoadInterlockAwareMonitorTest < ActiveSupport::TestCase
60+
include LoadInterlockAwareMonitorTests
61+
62+
def setup
63+
@monitor = ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
64+
end
65+
66+
def test_lock_owned_by_thread
67+
@monitor.synchronize do
68+
enumerator = Enumerator.new do |yielder|
69+
@monitor.synchronize do
70+
yielder.yield 42
71+
end
72+
end
73+
assert_equal 42, enumerator.next
74+
end
75+
end
76+
end
5477
end
5578
end

0 commit comments

Comments
 (0)