Skip to content

Commit 2fb0930

Browse files
byroothcmaATshopify
authored andcommitted
WIP
1 parent e19d0d4 commit 2fb0930

File tree

7 files changed

+140
-45
lines changed

7 files changed

+140
-45
lines changed

activerecord/lib/active_record/associations/association.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def async_load_target
191191
@target = find_target(async: true) if (@stale_state && stale_target?) || find_target?
192192

193193
loaded! unless loaded?
194-
target
194+
@target
195195
end
196196

197197
# We can't dump @reflection and @through_reflection since it contains the scope proc
@@ -243,7 +243,13 @@ def find_target(async: false)
243243
end
244244

245245
scope = self.scope
246-
return scope.to_a if skip_statement_cache?(scope)
246+
if skip_statement_cache?(scope)
247+
if async
248+
return scope.load_async.then(&:to_a)
249+
else
250+
return scope.to_a
251+
end
252+
end
247253

248254
sc = reflection.association_scope_cache(klass, owner) do |params|
249255
as = AssociationScope.create { params.bind }

activerecord/lib/active_record/relation.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,16 @@ def load_async
11501150
self
11511151
end
11521152

1153+
def then(&block)
1154+
if @future_result
1155+
@future_result.then do
1156+
yield self
1157+
end
1158+
else
1159+
super
1160+
end
1161+
end
1162+
11531163
# Returns <tt>true</tt> if the relation was scheduled on the background
11541164
# thread pool.
11551165
def scheduled?

activerecord/test/cases/associations/belongs_to_associations_test.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,19 +1841,33 @@ def test_destroy_linked_models
18411841
end
18421842

18431843
class AsyncBelongsToAssociationsTest < ActiveRecord::TestCase
1844+
include WaitForAsyncTestHelper
1845+
18441846
fixtures :companies
18451847

18461848
self.use_transactional_tests = false
18471849

1848-
def test_temp_async_load_belongs_to
1849-
# TODO: proper test?
1850+
def test_async_load_belongs_to
18501851
client = Client.find(3)
18511852
first_firm = companies(:first_firm)
1852-
assert_queries_match(/LIMIT|ROWNUM <=|FETCH FIRST/) do
1853-
client.association(:firm).async_load_target
18541853

1854+
promise = client.association(:firm).async_load_target
1855+
wait_for_async_query
1856+
1857+
events = []
1858+
callback = -> (event) do
1859+
events << event unless event.payload[:name] == "SCHEMA"
1860+
end
1861+
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
1862+
client.firm
1863+
end
1864+
1865+
assert_no_queries do
18551866
assert_equal first_firm, client.firm
18561867
assert_equal first_firm.name, client.firm.name
18571868
end
1869+
1870+
assert_equal 1, events.size
1871+
assert_equal true, events.first.payload[:async]
18581872
end
18591873
end

activerecord/test/cases/associations/has_many_associations_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3252,3 +3252,34 @@ def force_signal37_to_load_all_clients_of_firm
32523252
companies(:first_firm).clients_of_firm.load_target
32533253
end
32543254
end
3255+
3256+
class AsyncHasOneAssociationsTest < ActiveRecord::TestCase
3257+
include WaitForAsyncTestHelper
3258+
3259+
fixtures :companies
3260+
3261+
self.use_transactional_tests = false
3262+
3263+
def test_async_load_has_many
3264+
firm = companies(:first_firm)
3265+
3266+
promise = firm.association(:clients).async_load_target
3267+
wait_for_async_query
3268+
3269+
events = []
3270+
callback = -> (event) do
3271+
events << event unless event.payload[:name] == "SCHEMA"
3272+
end
3273+
3274+
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
3275+
assert_equal 3, firm.clients.size
3276+
end
3277+
3278+
assert_no_queries do
3279+
assert_not_nil firm.clients[2]
3280+
end
3281+
3282+
assert_equal 1, events.size
3283+
assert_equal true, events.first.payload[:async]
3284+
end
3285+
end

activerecord/test/cases/associations/has_one_associations_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,3 +943,35 @@ def test_has_one_with_touch_option_on_nonpersisted_built_associations_doesnt_upd
943943
MESSAGE
944944
end
945945
end
946+
947+
class AsyncHasOneAssociationsTest < ActiveRecord::TestCase
948+
include WaitForAsyncTestHelper
949+
950+
fixtures :companies, :accounts
951+
952+
self.use_transactional_tests = false
953+
954+
def test_async_load_has_one
955+
firm = companies(:first_firm)
956+
first_account = Account.find(1)
957+
958+
promise = firm.association(:account).async_load_target
959+
wait_for_async_query
960+
961+
events = []
962+
callback = -> (event) do
963+
events << event unless event.payload[:name] == "SCHEMA"
964+
end
965+
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
966+
firm.account
967+
end
968+
969+
assert_no_queries do
970+
assert_equal first_account, firm.account
971+
assert_equal first_account.credit_limit, firm.account.credit_limit
972+
end
973+
974+
assert_equal 1, events.size
975+
assert_equal true, events.first.payload[:async]
976+
end
977+
end

activerecord/test/cases/helper.rb

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,36 +45,53 @@
4545
ActiveRecord::ConnectionAdapters.register("abstract", "ActiveRecord::ConnectionAdapters::AbstractAdapter", "active_record/connection_adapters/abstract_adapter")
4646
ActiveRecord::ConnectionAdapters.register("fake", "FakeActiveRecordAdapter", File.expand_path("../support/fake_adapter.rb", __dir__))
4747

48-
class SQLSubscriber
49-
attr_reader :logged
50-
attr_reader :payloads
48+
class ActiveRecord::TestCase
49+
class SQLSubscriber
50+
attr_reader :logged
51+
attr_reader :payloads
52+
53+
def initialize
54+
@logged = []
55+
@payloads = []
56+
end
57+
58+
def start(name, id, payload)
59+
@payloads << payload
60+
@logged << [payload[:sql].squish, payload[:name], payload[:binds]]
61+
end
5162

52-
def initialize
53-
@logged = []
54-
@payloads = []
63+
def finish(name, id, payload); end
5564
end
5665

57-
def start(name, id, payload)
58-
@payloads << payload
59-
@logged << [payload[:sql].squish, payload[:name], payload[:binds]]
66+
module InTimeZone
67+
private
68+
def in_time_zone(zone)
69+
old_zone = Time.zone
70+
old_tz = ActiveRecord::Base.time_zone_aware_attributes
71+
72+
Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
73+
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
74+
yield
75+
ensure
76+
Time.zone = old_zone
77+
ActiveRecord::Base.time_zone_aware_attributes = old_tz
78+
end
6079
end
6180

62-
def finish(name, id, payload); end
63-
end
81+
module WaitForAsyncTestHelper
82+
private
83+
def wait_for_async_query(connection = ActiveRecord::Base.lease_connection, timeout: 5)
84+
return unless connection.async_enabled?
6485

65-
module InTimeZone
66-
private
67-
def in_time_zone(zone)
68-
old_zone = Time.zone
69-
old_tz = ActiveRecord::Base.time_zone_aware_attributes
70-
71-
Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
72-
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
73-
yield
74-
ensure
75-
Time.zone = old_zone
76-
ActiveRecord::Base.time_zone_aware_attributes = old_tz
77-
end
86+
executor = connection.pool.async_executor
87+
(timeout * 100).times do
88+
return unless executor.scheduled_task_count > executor.completed_task_count
89+
sleep 0.01
90+
end
91+
92+
raise Timeout::Error, "The async executor wasn't drained after #{timeout} seconds"
93+
end
94+
end
7895
end
7996

8097
# Encryption

activerecord/test/cases/relation/load_async_test.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,6 @@
77
require "models/other_dog"
88

99
module ActiveRecord
10-
module WaitForAsyncTestHelper
11-
private
12-
def wait_for_async_query(connection = ActiveRecord::Base.lease_connection, timeout: 5)
13-
return unless connection.async_enabled?
14-
15-
executor = connection.pool.async_executor
16-
(timeout * 100).times do
17-
return unless executor.scheduled_task_count > executor.completed_task_count
18-
sleep 0.01
19-
end
20-
21-
raise Timeout::Error, "The async executor wasn't drained after #{timeout} seconds"
22-
end
23-
end
24-
2510
class LoadAsyncTest < ActiveRecord::TestCase
2611
include WaitForAsyncTestHelper
2712

0 commit comments

Comments
 (0)