@@ -6,7 +6,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
6
6
self . use_transactional_tests = false
7
7
8
8
setup do
9
- @result = JobResult . create! ( queue_name : "default" , status : "seq: " )
9
+ @result = JobResult . create! ( queue_name : "default" , status : "" )
10
10
11
11
default_worker = { queues : "default" , polling_interval : 0.1 , processes : 3 , threads : 2 }
12
12
dispatcher = { polling_interval : 0.1 , batch_size : 200 , concurrency_maintenance_interval : 1 }
@@ -20,13 +20,13 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
20
20
terminate_process ( @pid ) if process_exists? ( @pid )
21
21
end
22
22
23
- test "run several conflicting jobs over the same record sequentially " do
23
+ test "run several conflicting jobs over the same record without overlapping " do
24
24
( "A" .."F" ) . each do |name |
25
- SequentialUpdateResultJob . perform_later ( @result , name : name , pause : 0.2 . seconds )
25
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name , pause : 0.2 . seconds )
26
26
end
27
27
28
28
( "G" .."K" ) . each do |name |
29
- SequentialUpdateResultJob . perform_later ( @result , name : name )
29
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name )
30
30
end
31
31
32
32
wait_for_jobs_to_finish_for ( 5 . seconds )
@@ -39,11 +39,11 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
39
39
UpdateResultJob . set ( wait : 0.23 . seconds ) . perform_later ( @result , name : "000" , pause : 0.1 . seconds )
40
40
41
41
( "A" .."F" ) . each_with_index do |name , i |
42
- SequentialUpdateResultJob . set ( wait : ( 0.2 + i * 0.01 ) . seconds ) . perform_later ( @result , name : name , pause : 0.3 . seconds )
42
+ NonOverlappingUpdateResultJob . set ( wait : ( 0.2 + i * 0.01 ) . seconds ) . perform_later ( @result , name : name , pause : 0.3 . seconds )
43
43
end
44
44
45
45
( "G" .."K" ) . each_with_index do |name , i |
46
- SequentialUpdateResultJob . set ( wait : ( 0.3 + i * 0.01 ) . seconds ) . perform_later ( @result , name : name )
46
+ NonOverlappingUpdateResultJob . set ( wait : ( 0.3 + i * 0.01 ) . seconds ) . perform_later ( @result , name : name )
47
47
end
48
48
49
49
wait_for_jobs_to_finish_for ( 5 . seconds )
@@ -85,11 +85,11 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
85
85
test "run several jobs over the same record sequentially, with some of them failing" do
86
86
( "A" .."F" ) . each_with_index do |name , i |
87
87
# A, C, E will fail, for i= 0, 2, 4
88
- SequentialUpdateResultJob . perform_later ( @result , name : name , pause : 0.2 . seconds , exception : ( ExpectedTestError if i . even? ) )
88
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name , pause : 0.2 . seconds , exception : ( ExpectedTestError if i . even? ) )
89
89
end
90
90
91
91
( "G" .."K" ) . each do |name |
92
- SequentialUpdateResultJob . perform_later ( @result , name : name )
92
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name )
93
93
end
94
94
95
95
wait_for_jobs_to_finish_for ( 5 . seconds )
@@ -100,7 +100,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
100
100
101
101
test "rely on dispatcher to unblock blocked executions with an available semaphore" do
102
102
# Simulate a scenario where we got an available semaphore and some stuck jobs
103
- job = SequentialUpdateResultJob . perform_later ( @result , name : "A" )
103
+ job = NonOverlappingUpdateResultJob . perform_later ( @result , name : "A" )
104
104
105
105
wait_for_jobs_to_finish_for ( 5 . seconds )
106
106
assert_no_unfinished_jobs
@@ -114,7 +114,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
114
114
# Now enqueue more jobs under that same key. They'll be all locked
115
115
assert_difference -> { SolidQueue ::BlockedExecution . count } , +10 do
116
116
( "B" .."K" ) . each do |name |
117
- SequentialUpdateResultJob . perform_later ( @result , name : name )
117
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name )
118
118
end
119
119
end
120
120
@@ -127,14 +127,12 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
127
127
wait_for_jobs_to_finish_for ( 5 . seconds )
128
128
assert_no_unfinished_jobs
129
129
130
- # We can't ensure the order between B and C, because it depends on which worker wins when
131
- # unblocking, as one will try to unblock B and another C
132
- assert_stored_sequence @result , ( "A" .."K" ) . to_a , [ "A" , "C" , "B" ] + ( "D" .."K" ) . to_a
130
+ assert_stored_sequence @result , ( "A" .."K" ) . to_a
133
131
end
134
132
135
133
test "rely on dispatcher to unblock blocked executions with an expired semaphore" do
136
134
# Simulate a scenario where we got an available semaphore and some stuck jobs
137
- job = SequentialUpdateResultJob . perform_later ( @result , name : "A" )
135
+ job = NonOverlappingUpdateResultJob . perform_later ( @result , name : "A" )
138
136
wait_for_jobs_to_finish_for ( 5 . seconds )
139
137
assert_no_unfinished_jobs
140
138
@@ -147,7 +145,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
147
145
# Now enqueue more jobs under that same key. They'll be all locked
148
146
assert_difference -> { SolidQueue ::BlockedExecution . count } , +10 do
149
147
( "B" .."K" ) . each do |name |
150
- SequentialUpdateResultJob . perform_later ( @result , name : name )
148
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : name )
151
149
end
152
150
end
153
151
@@ -159,13 +157,11 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
159
157
wait_for_jobs_to_finish_for ( 5 . seconds )
160
158
assert_no_unfinished_jobs
161
159
162
- # We can't ensure the order between B and C, because it depends on which worker wins when
163
- # unblocking, as one will try to unblock B and another C
164
- assert_stored_sequence @result , ( "A" .."K" ) . to_a , [ "A" , "C" , "B" ] + ( "D" .."K" ) . to_a
160
+ assert_stored_sequence @result , ( "A" .."K" ) . to_a
165
161
end
166
162
167
163
test "don't block claimed executions that get released" do
168
- SequentialUpdateResultJob . perform_later ( @result , name : "I'll be released to ready" , pause : SolidQueue . shutdown_timeout + 10 . seconds )
164
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : "I'll be released to ready" , pause : SolidQueue . shutdown_timeout + 10 . seconds )
169
165
job = SolidQueue ::Job . last
170
166
171
167
sleep ( 0.2 )
@@ -184,8 +180,8 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
184
180
skip if Rails ::VERSION ::MAJOR == 7 && Rails ::VERSION ::MINOR == 2
185
181
186
182
ActiveRecord ::Base . transaction do
187
- SequentialUpdateResultJob . perform_later ( @result , name : "A" , pause : 0.2 . seconds )
188
- SequentialUpdateResultJob . perform_later ( @result , name : "B" )
183
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : "A" , pause : 0.2 . seconds )
184
+ NonOverlappingUpdateResultJob . perform_later ( @result , name : "B" )
189
185
190
186
begin
191
187
assert_equal 2 , SolidQueue ::Job . count
@@ -219,7 +215,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
219
215
end
220
216
221
217
test "discard on conflict across different concurrency keys" do
222
- another_result = JobResult . create! ( queue_name : "default" , status : "seq: " )
218
+ another_result = JobResult . create! ( queue_name : "default" , status : "" )
223
219
DiscardableUpdateResultJob . perform_later ( @result , name : "1" , pause : 0.2 )
224
220
DiscardableUpdateResultJob . perform_later ( another_result , name : "2" , pause : 0.2 )
225
221
DiscardableUpdateResultJob . perform_later ( @result , name : "3" ) # Should be discarded
@@ -239,6 +235,8 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
239
235
DiscardableUpdateResultJob . perform_later ( @result , name : "2" )
240
236
241
237
wait_for_jobs_to_finish_for ( 5 . seconds )
238
+ wait_for_semaphores_to_be_released_for ( 2 . seconds )
239
+
242
240
assert_no_unfinished_jobs
243
241
244
242
# Enqueue another job that shouldn't be discarded or blocked
@@ -250,10 +248,18 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase
250
248
end
251
249
252
250
private
253
- def assert_stored_sequence ( result , * sequences )
254
- expected = sequences . map { | sequence | "seq: " + sequence . map { |name | "s#{ name } c#{ name } " } . join }
251
+ def assert_stored_sequence ( result , sequence )
252
+ expected = sequence . sort . map { |name | "s#{ name } c#{ name } " } . join
255
253
skip_active_record_query_cache do
256
- assert_includes expected , result . reload . status
254
+ assert_equal expected , result . reload . status . split ( " + " ) . sort . join
255
+ end
256
+ end
257
+
258
+ def wait_for_semaphores_to_be_released_for ( timeout )
259
+ wait_while_with_timeout ( timeout ) do
260
+ skip_active_record_query_cache do
261
+ SolidQueue ::Semaphore . available . invert_where . any?
262
+ end
257
263
end
258
264
end
259
265
end
0 commit comments