delayed 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/app/models/delayed/job.rb +25 -18
- data/db/migrate/08_add_run_at_and_name_not_null_check.rb +34 -0
- data/db/migrate/09_validate_run_at_and_name_not_null.rb +41 -0
- data/lib/delayed/active_job_adapter.rb +41 -8
- data/lib/delayed/backend/base.rb +67 -11
- data/lib/delayed/backend/job_preparer.rb +33 -0
- data/lib/delayed/exceptions.rb +2 -0
- data/lib/delayed/lifecycle.rb +1 -1
- data/lib/delayed/monitor.rb +69 -43
- data/lib/delayed/plugins/instrumentation.rb +9 -3
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/worker.rb +2 -8
- data/spec/delayed/__snapshots__/monitor_spec.rb.snap +447 -1170
- data/spec/delayed/active_job_adapter_spec.rb +285 -46
- data/spec/delayed/job_spec.rb +218 -32
- data/spec/delayed/monitor_spec.rb +18 -19
- data/spec/delayed/plugins/instrumentation_spec.rb +41 -0
- data/spec/helper.rb +18 -6
- data/spec/lifecycle_spec.rb +1 -1
- data/spec/message_sending_spec.rb +3 -13
- data/spec/performable_method_spec.rb +0 -6
- data/spec/sample_jobs.rb +0 -10
- metadata +12 -10
- /data/db/migrate/{1_create_delayed_jobs.rb → 01_create_delayed_jobs.rb} +0 -0
- /data/db/migrate/{2_add_name_to_delayed_jobs.rb → 02_add_name_to_delayed_jobs.rb} +0 -0
- /data/db/migrate/{3_add_index_to_delayed_jobs_name.rb → 03_add_index_to_delayed_jobs_name.rb} +0 -0
- /data/db/migrate/{4_index_live_jobs.rb → 04_index_live_jobs.rb} +0 -0
- /data/db/migrate/{5_index_failed_jobs.rb → 05_index_failed_jobs.rb} +0 -0
- /data/db/migrate/{6_set_postgres_fillfactor.rb → 06_set_postgres_fillfactor.rb} +0 -0
- /data/db/migrate/{7_remove_legacy_index.rb → 07_remove_legacy_index.rb} +0 -0
data/spec/delayed/job_spec.rb
CHANGED
|
@@ -13,18 +13,19 @@ describe Delayed::Job do
|
|
|
13
13
|
Delayed::Worker.max_claims = 1 # disable multithreading because SimpleJob is not threadsafe
|
|
14
14
|
Delayed::Worker.default_priority = 99
|
|
15
15
|
Delayed::Worker.delay_jobs = true
|
|
16
|
+
Delayed::Worker.deny_stale_enqueues = false
|
|
16
17
|
Delayed::Worker.default_queue_name = 'default_tracking'
|
|
17
18
|
SimpleJob.runs = 0
|
|
18
19
|
described_class.delete_all
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
it 'sets run_at automatically if not set' do
|
|
22
|
-
expect(described_class.
|
|
23
|
+
expect(described_class.enqueue(payload_object: ErrorJob.new).run_at).not_to be_nil
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
it 'does not set run_at automatically if already set' do
|
|
26
27
|
later = described_class.db_time_now + 5.minutes
|
|
27
|
-
job = described_class.
|
|
28
|
+
job = described_class.enqueue(payload_object: ErrorJob.new, run_at: later)
|
|
28
29
|
expect(job.run_at).to be_within(1).of(later)
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -36,12 +37,6 @@ describe Delayed::Job do
|
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
describe 'enqueue' do
|
|
39
|
-
it "allows enqueue hook to modify job at DB level" do
|
|
40
|
-
later = described_class.db_time_now + 20.minutes
|
|
41
|
-
job = described_class.enqueue payload_object: EnqueueJobMod.new
|
|
42
|
-
expect(described_class.find(job.id).run_at).to be_within(1).of(later)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
40
|
context 'with a hash' do
|
|
46
41
|
it "raises ArgumentError when handler doesn't respond_to :perform" do
|
|
47
42
|
expect { described_class.enqueue(payload_object: Object.new) }.to raise_error(ArgumentError)
|
|
@@ -133,6 +128,187 @@ describe Delayed::Job do
|
|
|
133
128
|
expect(described_class.enqueue(SimpleJob.new)).to be_instance_of(described_class)
|
|
134
129
|
end
|
|
135
130
|
end
|
|
131
|
+
|
|
132
|
+
context 'with deny_stale_enqueues = true' do
|
|
133
|
+
before { Delayed::Worker.deny_stale_enqueues = true }
|
|
134
|
+
|
|
135
|
+
it 'raises StaleEnqueueError when run_at is beyond lock_timeout in the past' do
|
|
136
|
+
stale_time = described_class.db_time_now - described_class.lock_timeout - 1.minute
|
|
137
|
+
expect {
|
|
138
|
+
described_class.enqueue SimpleJob.new, run_at: stale_time
|
|
139
|
+
}.to raise_error(Delayed::StaleEnqueueError, /Cannot enqueue a job in the distant past/)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'allows run_at within lock_timeout of now' do
|
|
143
|
+
recent_past = described_class.db_time_now - described_class.lock_timeout + 1.minute
|
|
144
|
+
job = described_class.enqueue SimpleJob.new, run_at: recent_past
|
|
145
|
+
expect(job).to be_persisted
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'allows run_at in the future' do
|
|
149
|
+
future = described_class.db_time_now + 5.minutes
|
|
150
|
+
job = described_class.enqueue SimpleJob.new, run_at: future
|
|
151
|
+
expect(job).to be_persisted
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'allows enqueue without run_at' do
|
|
155
|
+
job = described_class.enqueue SimpleJob.new
|
|
156
|
+
expect(job).to be_persisted
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context 'with deny_stale_enqueues = false (default)' do
|
|
161
|
+
it 'allows run_at far in the past' do
|
|
162
|
+
stale_time = described_class.db_time_now - 1.day
|
|
163
|
+
job = described_class.enqueue SimpleJob.new, run_at: stale_time
|
|
164
|
+
expect(job).to be_persisted
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context 'when payload defines an :enqueue hook' do
|
|
169
|
+
before do
|
|
170
|
+
stub_const('JobWithEnqueueHook', Class.new do
|
|
171
|
+
def enqueue(_job); end
|
|
172
|
+
|
|
173
|
+
def perform; end
|
|
174
|
+
end)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'raises an error naming the payload class' do
|
|
178
|
+
expect { described_class.enqueue(JobWithEnqueueHook.new) }
|
|
179
|
+
.to raise_error(RuntimeError, ':enqueue hook on JobWithEnqueueHook is no longer supported')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'does not invoke the payload enqueue method' do
|
|
183
|
+
payload = JobWithEnqueueHook.new
|
|
184
|
+
expect(payload).not_to receive(:enqueue)
|
|
185
|
+
expect { described_class.enqueue(payload) }.to raise_error(RuntimeError, /:enqueue hook/)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
context 'when payload does not define :enqueue' do
|
|
190
|
+
it 'does not raise' do
|
|
191
|
+
expect { described_class.enqueue(SimpleJob.new) }.not_to raise_error
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
context 'when payload is an ActiveJob wrapper that responds to :enqueue' do
|
|
196
|
+
it 'does not raise' do
|
|
197
|
+
wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
|
|
198
|
+
expect { described_class.enqueue(payload_object: wrapper) }.not_to raise_error
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context 'when payload is a bare ActiveJob::Base instance' do
|
|
203
|
+
it 'raises' do
|
|
204
|
+
expect { described_class.enqueue(payload_object: ActiveJobJob.new) }.to raise_error(RuntimeError, /Delayed::Job enqueue methods do not accept ActiveJobs/)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
context 'when passed a bare ActiveJob::Base instance' do
|
|
209
|
+
it 'raises' do
|
|
210
|
+
expect { described_class.enqueue(ActiveJobJob.new) }.to raise_error(RuntimeError, /Delayed::Job enqueue methods do not accept ActiveJobs/)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
describe '.enqueue_all' do
|
|
216
|
+
def build_job(payload = SimpleJob.new, opts = {})
|
|
217
|
+
described_class.new(Delayed::Backend::JobPreparer.new(payload, opts).prepare)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'returns 0 when given no jobs' do
|
|
221
|
+
expect(described_class.enqueue_all([])).to eq(0)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'inserts all jobs in a single INSERT' do
|
|
225
|
+
jobs = Array.new(3) { build_job }
|
|
226
|
+
expect { described_class.enqueue_all(jobs) }
|
|
227
|
+
.to emit_notification('sql.active_record').with_payload(hash_including(sql: a_string_matching(/\AINSERT INTO/i)))
|
|
228
|
+
expect(described_class.count).to eq(3)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'returns the count of enqueued jobs' do
|
|
232
|
+
jobs = Array.new(3) { build_job }
|
|
233
|
+
expect(described_class.enqueue_all(jobs)).to eq(3)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'sets id on each job from INSERT RETURNING when supported' do
|
|
237
|
+
skip 'requires INSERT ... RETURNING support' unless described_class.connection.supports_insert_returning?
|
|
238
|
+
|
|
239
|
+
jobs = Array.new(2) { build_job }
|
|
240
|
+
described_class.enqueue_all(jobs)
|
|
241
|
+
expect(jobs.map(&:id)).to match_array(described_class.pluck(:id))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'fires :enqueue lifecycle once with the jobs array' do
|
|
245
|
+
observed = []
|
|
246
|
+
lifecycle_was = Delayed.lifecycle
|
|
247
|
+
Delayed.instance_variable_set(:@lifecycle, Delayed::Lifecycle.new)
|
|
248
|
+
Delayed.lifecycle.before(:enqueue) { |jobs| observed << jobs }
|
|
249
|
+
|
|
250
|
+
jobs = [build_job]
|
|
251
|
+
described_class.enqueue_all(jobs)
|
|
252
|
+
|
|
253
|
+
expect(observed.size).to eq(1)
|
|
254
|
+
expect(observed.first).to eq(jobs)
|
|
255
|
+
ensure
|
|
256
|
+
Delayed.instance_variable_set(:@lifecycle, lifecycle_was)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
context 'when delay_jobs is false' do
|
|
260
|
+
before { Delayed::Worker.delay_jobs = false }
|
|
261
|
+
|
|
262
|
+
it 'inline-invokes each job and inserts nothing' do
|
|
263
|
+
payload = SimpleJob.new
|
|
264
|
+
expect(payload).to receive(:perform)
|
|
265
|
+
described_class.enqueue_all([build_job(payload)])
|
|
266
|
+
expect(described_class.count).to eq(0)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'when a job payload defines an :enqueue hook' do
|
|
271
|
+
before do
|
|
272
|
+
stub_const('JobWithEnqueueHook', Class.new do
|
|
273
|
+
def enqueue(_job); end
|
|
274
|
+
|
|
275
|
+
def perform; end
|
|
276
|
+
end)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'raises before inserting anything' do
|
|
280
|
+
jobs = [build_job(JobWithEnqueueHook.new)]
|
|
281
|
+
expect { described_class.enqueue_all(jobs) }
|
|
282
|
+
.to raise_error(RuntimeError, ':enqueue hook on JobWithEnqueueHook is no longer supported')
|
|
283
|
+
expect(described_class.count).to eq(0)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
context 'when a job payload is a bare ActiveJob::Base instance' do
|
|
288
|
+
it 'raises' do
|
|
289
|
+
jobs = [build_job(ActiveJobJob.new)]
|
|
290
|
+
expect { described_class.enqueue_all(jobs) }.to raise_error(RuntimeError, /Delayed::Job enqueue methods do not accept ActiveJobs/)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
describe '#hook' do
|
|
296
|
+
context 'with :enqueue' do
|
|
297
|
+
before do
|
|
298
|
+
stub_const('JobWithEnqueueHook', Class.new do
|
|
299
|
+
def enqueue(_job); end
|
|
300
|
+
|
|
301
|
+
def perform; end
|
|
302
|
+
end)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'raises rather than invoking the payload hook' do
|
|
306
|
+
job = described_class.new(payload_object: JobWithEnqueueHook.new)
|
|
307
|
+
expect(job.payload_object).not_to receive(:enqueue)
|
|
308
|
+
expect { job.hook(:enqueue) }
|
|
309
|
+
.to raise_error(RuntimeError, ':enqueue hook is no longer supported')
|
|
310
|
+
end
|
|
311
|
+
end
|
|
136
312
|
end
|
|
137
313
|
|
|
138
314
|
describe 'callbacks' do
|
|
@@ -150,9 +326,9 @@ describe Delayed::Job do
|
|
|
150
326
|
|
|
151
327
|
it 'calls before and after callbacks' do
|
|
152
328
|
job = described_class.enqueue(CallbackJob.new)
|
|
153
|
-
expect(CallbackJob.messages).to eq([
|
|
329
|
+
expect(CallbackJob.messages).to eq([])
|
|
154
330
|
job.invoke_job
|
|
155
|
-
expect(CallbackJob.messages).to eq(%w(
|
|
331
|
+
expect(CallbackJob.messages).to eq(%w(before perform success after))
|
|
156
332
|
end
|
|
157
333
|
|
|
158
334
|
it 'calls the after callback with an error' do
|
|
@@ -160,14 +336,14 @@ describe Delayed::Job do
|
|
|
160
336
|
expect(job.payload_object).to receive(:perform).and_raise(RuntimeError.new('fail'))
|
|
161
337
|
|
|
162
338
|
expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
|
|
163
|
-
expect(CallbackJob.messages).to eq(['
|
|
339
|
+
expect(CallbackJob.messages).to eq(['before', 'error: RuntimeError', 'after'])
|
|
164
340
|
end
|
|
165
341
|
|
|
166
342
|
it 'calls error when before raises an error' do
|
|
167
343
|
job = described_class.enqueue(CallbackJob.new)
|
|
168
344
|
expect(job.payload_object).to receive(:before).and_raise(RuntimeError.new('fail'))
|
|
169
345
|
expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
|
|
170
|
-
expect(CallbackJob.messages).to eq(['
|
|
346
|
+
expect(CallbackJob.messages).to eq(['error: RuntimeError', 'after'])
|
|
171
347
|
end
|
|
172
348
|
end
|
|
173
349
|
|
|
@@ -425,26 +601,23 @@ describe Delayed::Job do
|
|
|
425
601
|
describe '#name' do
|
|
426
602
|
context 'when name column is populated' do
|
|
427
603
|
it 'is the class name of the job that was enqueued' do
|
|
428
|
-
job = described_class.
|
|
604
|
+
job = described_class.enqueue(payload_object: ErrorJob.new)
|
|
429
605
|
expect(job.name).to eq('ErrorJob')
|
|
430
|
-
job.save!
|
|
431
606
|
expect(job.reload.name).to eq('ErrorJob')
|
|
432
607
|
expect(described_class.group(:name).count).to eq('ErrorJob' => 1)
|
|
433
608
|
end
|
|
434
609
|
|
|
435
610
|
it 'is the class name of the performable job if it is an ActiveJob' do
|
|
436
611
|
job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
|
|
437
|
-
job = described_class.
|
|
612
|
+
job = described_class.enqueue(payload_object: job_wrapper)
|
|
438
613
|
expect(job.name).to eq('ActiveJobJob')
|
|
439
|
-
job.save!
|
|
440
614
|
expect(job.reload.name).to eq('ActiveJobJob')
|
|
441
615
|
expect(described_class.group(:name).count).to eq('ActiveJobJob' => 1)
|
|
442
616
|
end
|
|
443
617
|
|
|
444
618
|
it 'is the returned display_name if display_name is defined on the job object' do
|
|
445
|
-
job = described_class.
|
|
619
|
+
job = described_class.enqueue(payload_object: NamedJob.new)
|
|
446
620
|
expect(job.name).to eq('named_job')
|
|
447
|
-
job.save!
|
|
448
621
|
expect(job.reload.name).to eq('named_job')
|
|
449
622
|
expect(described_class.group(:name).count).to eq('named_job' => 1)
|
|
450
623
|
end
|
|
@@ -456,25 +629,38 @@ describe Delayed::Job do
|
|
|
456
629
|
end
|
|
457
630
|
|
|
458
631
|
it 'is the custom name value when set explicitly' do
|
|
459
|
-
job = described_class.
|
|
460
|
-
job.name = 'Custom Name'
|
|
461
|
-
job.save!
|
|
632
|
+
job = described_class.enqueue(payload_object: ErrorJob.new, name: 'Custom Name')
|
|
462
633
|
expect(job.reload.name).to eq('Custom Name')
|
|
463
634
|
expect(described_class.group(:name).count).to eq('Custom Name' => 1)
|
|
464
635
|
end
|
|
465
636
|
end
|
|
466
637
|
|
|
467
638
|
context 'when name column is NULL' do
|
|
639
|
+
before do
|
|
640
|
+
ActiveRecord::Schema.define do
|
|
641
|
+
ValidateRunAtAndNameNotNull.migrate(:down)
|
|
642
|
+
AddRunAtAndNameNotNullCheck.migrate(:down)
|
|
643
|
+
end
|
|
644
|
+
described_class.reset_column_information
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
after do
|
|
648
|
+
described_class.delete_all
|
|
649
|
+
ActiveRecord::Schema.define do
|
|
650
|
+
AddRunAtAndNameNotNullCheck.migrate(:up)
|
|
651
|
+
ValidateRunAtAndNameNotNull.migrate(:up)
|
|
652
|
+
end
|
|
653
|
+
described_class.reset_column_information
|
|
654
|
+
end
|
|
655
|
+
|
|
468
656
|
it 'is the class name of the job that was enqueued' do
|
|
469
|
-
job = described_class.
|
|
470
|
-
job.update_column(:name, nil) # rubocop:disable Rails/SkipsModelValidations
|
|
657
|
+
job = described_class.enqueue(payload_object: ErrorJob.new)
|
|
471
658
|
expect(job.reload.name).to eq('ErrorJob')
|
|
472
659
|
end
|
|
473
660
|
|
|
474
661
|
it 'is the class name of the performable job if it is an ActiveJob' do
|
|
475
662
|
job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
|
|
476
|
-
job = described_class.
|
|
477
|
-
job.update_column(:name, nil) # rubocop:disable Rails/SkipsModelValidations
|
|
663
|
+
job = described_class.enqueue(payload_object: job_wrapper)
|
|
478
664
|
expect(job.reload.name).to eq('ActiveJobJob')
|
|
479
665
|
end
|
|
480
666
|
|
|
@@ -505,7 +691,7 @@ describe Delayed::Job do
|
|
|
505
691
|
end
|
|
506
692
|
|
|
507
693
|
it 'is the class name of the job that was enqueued' do
|
|
508
|
-
job = described_class.new(payload_object: ErrorJob.new)
|
|
694
|
+
job = described_class.new(payload_object: ErrorJob.new, run_at: Time.current)
|
|
509
695
|
expect(job.name).to eq('ErrorJob')
|
|
510
696
|
job.save!
|
|
511
697
|
expect(job.reload.name).to eq('ErrorJob')
|
|
@@ -513,14 +699,14 @@ describe Delayed::Job do
|
|
|
513
699
|
|
|
514
700
|
it 'is the class name of the performable job if it is an ActiveJob' do
|
|
515
701
|
job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
|
|
516
|
-
job = described_class.new(payload_object: job_wrapper)
|
|
702
|
+
job = described_class.new(payload_object: job_wrapper, run_at: Time.current)
|
|
517
703
|
expect(job.name).to eq('ActiveJobJob')
|
|
518
704
|
job.save!
|
|
519
705
|
expect(job.reload.name).to eq('ActiveJobJob')
|
|
520
706
|
end
|
|
521
707
|
|
|
522
708
|
it 'is the returned display_name if display_name is defined on the job object' do
|
|
523
|
-
job = described_class.new(payload_object: NamedJob.new)
|
|
709
|
+
job = described_class.new(payload_object: NamedJob.new, run_at: Time.current)
|
|
524
710
|
expect(job.name).to eq('named_job')
|
|
525
711
|
job.save!
|
|
526
712
|
expect(job.reload.name).to eq('named_job')
|
|
@@ -798,7 +984,7 @@ describe Delayed::Job do
|
|
|
798
984
|
describe 'running a job' do
|
|
799
985
|
it 'fails after Worker.max_run_time' do
|
|
800
986
|
Delayed::Worker.max_run_time = 1.second
|
|
801
|
-
job = described_class.
|
|
987
|
+
job = described_class.enqueue payload_object: LongRunningJob.new
|
|
802
988
|
worker.perform(job)
|
|
803
989
|
expect(job.error).not_to be_nil
|
|
804
990
|
expect(job.reload.last_error).to match(/expired/)
|
|
@@ -808,7 +994,7 @@ describe Delayed::Job do
|
|
|
808
994
|
|
|
809
995
|
context 'when the job raises a deserialization error' do
|
|
810
996
|
it 'marks the job as failed' do
|
|
811
|
-
job = described_class.
|
|
997
|
+
job = described_class.enqueue payload_object: LongRunningJob.new
|
|
812
998
|
job.update_columns(handler: '--- !ruby/object:JobThatDoesNotExist {}') # rubocop:disable Rails/SkipsModelValidations
|
|
813
999
|
expect_any_instance_of(described_class).to receive(:destroy_failed_jobs?).and_return(false)
|
|
814
1000
|
worker.work_off
|
|
@@ -863,13 +1049,13 @@ describe Delayed::Job do
|
|
|
863
1049
|
|
|
864
1050
|
context 'reschedule' do
|
|
865
1051
|
before do
|
|
866
|
-
@job = described_class.
|
|
1052
|
+
@job = described_class.enqueue payload_object: SimpleJob.new
|
|
867
1053
|
end
|
|
868
1054
|
|
|
869
1055
|
shared_examples_for 'any failure more than Worker.max_attempts times' do
|
|
870
1056
|
context "when the job's payload has a #failure hook" do
|
|
871
1057
|
before do
|
|
872
|
-
@job = described_class.
|
|
1058
|
+
@job = described_class.enqueue payload_object: OnPermanentFailureJob.new
|
|
873
1059
|
expect(@job.payload_object).to respond_to(:failure)
|
|
874
1060
|
end
|
|
875
1061
|
|
|
@@ -123,6 +123,7 @@ RSpec.describe Delayed::Monitor do
|
|
|
123
123
|
run_at: now,
|
|
124
124
|
queue: 'default',
|
|
125
125
|
handler: "--- !ruby/object:SimpleJob\n",
|
|
126
|
+
name: 'SimpleJob',
|
|
126
127
|
attempts: 0,
|
|
127
128
|
}
|
|
128
129
|
end
|
|
@@ -345,7 +346,7 @@ RSpec.describe Delayed::Monitor do
|
|
|
345
346
|
end
|
|
346
347
|
end
|
|
347
348
|
|
|
348
|
-
describe '
|
|
349
|
+
describe 'SQL' do
|
|
349
350
|
let(:monitor) { described_class.new }
|
|
350
351
|
let(:queries) { [] }
|
|
351
352
|
let(:now) { '2025-11-10 17:20:13 UTC' }
|
|
@@ -360,30 +361,28 @@ RSpec.describe Delayed::Monitor do
|
|
|
360
361
|
value = value.value if value.is_a?(ActiveModel::Attribute)
|
|
361
362
|
sql = sql.sub(/(\?|\$\d)/, ActiveRecord::Base.connection.quote(value))
|
|
362
363
|
end
|
|
363
|
-
queries << sql
|
|
364
|
+
queries << QueryUnderTest.for(sql)
|
|
365
|
+
queries << "---"
|
|
364
366
|
end
|
|
365
367
|
end
|
|
366
368
|
|
|
367
|
-
def
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
def query_descriptions
|
|
370
|
+
described_class::METRICS.each do |metric|
|
|
371
|
+
queries << "-- QUERIES FOR `#{metric}`:"
|
|
372
|
+
queries << "---------------------------------"
|
|
373
|
+
monitor.query_for(metric)
|
|
374
|
+
queries << "-- (no new queries)" unless queries.last == '---'
|
|
375
|
+
end
|
|
376
|
+
queries.dup.map { |query| query.try(:full_description) || query }
|
|
370
377
|
end
|
|
371
378
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
expect(queries_for(metric).map(&:formatted).join("\n")).to match_snapshot
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
it "produces the expected #{current_adapter} query plan for #{metric}" do
|
|
379
|
-
expect(queries_for(metric).map(&:explain).join("\n")).to match_snapshot
|
|
380
|
-
end
|
|
379
|
+
it "runs the expected #{current_adapter} queries with the expected plans" do
|
|
380
|
+
expect(query_descriptions.join("\n")).to match_snapshot
|
|
381
|
+
end
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
end
|
|
386
|
-
end
|
|
383
|
+
context 'when using the legacy index', :with_legacy_table_index do
|
|
384
|
+
it "[legacy index] runs the expected #{current_adapter} queries with the expected plans" do
|
|
385
|
+
expect(query_descriptions.join("\n")).to match_snapshot
|
|
387
386
|
end
|
|
388
387
|
end
|
|
389
388
|
end
|
|
@@ -3,6 +3,47 @@ require 'helper'
|
|
|
3
3
|
RSpec.describe Delayed::Plugins::Instrumentation do
|
|
4
4
|
let!(:job) { Delayed::Job.enqueue SimpleJob.new, priority: 13, queue: 'test' }
|
|
5
5
|
|
|
6
|
+
it 'emits delayed.job.enqueue when a job is enqueued' do
|
|
7
|
+
expect { Delayed::Job.enqueue SimpleJob.new }.to emit_notification('delayed.job.enqueue').with_payload(
|
|
8
|
+
jobs: [
|
|
9
|
+
hash_including(
|
|
10
|
+
job_name: 'SimpleJob',
|
|
11
|
+
table: 'delayed_jobs',
|
|
12
|
+
database: current_database,
|
|
13
|
+
database_adapter: current_adapter,
|
|
14
|
+
job: an_instance_of(Delayed::Job),
|
|
15
|
+
),
|
|
16
|
+
],
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'emits a single delayed.job.enqueue when a batch is enqueued via the ActiveJob adapter' do
|
|
21
|
+
job_class = Class.new(ActiveJob::Base) do
|
|
22
|
+
def perform; end
|
|
23
|
+
end
|
|
24
|
+
stub_const('BatchedJob', job_class)
|
|
25
|
+
|
|
26
|
+
adapter_was = ActiveJob::Base.queue_adapter
|
|
27
|
+
ActiveJob::Base.queue_adapter = :delayed
|
|
28
|
+
begin
|
|
29
|
+
jobs = Array.new(3) { BatchedJob.new }
|
|
30
|
+
expect { ActiveJob::Base.queue_adapter.enqueue_all(jobs) }
|
|
31
|
+
.to emit_notification('delayed.job.enqueue').with_payload(
|
|
32
|
+
jobs: Array.new(3) {
|
|
33
|
+
hash_including(
|
|
34
|
+
job_name: 'BatchedJob',
|
|
35
|
+
table: 'delayed_jobs',
|
|
36
|
+
database: current_database,
|
|
37
|
+
database_adapter: current_adapter,
|
|
38
|
+
job: an_instance_of(Delayed::Job),
|
|
39
|
+
)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
ensure
|
|
43
|
+
ActiveJob::Base.queue_adapter = adapter_was
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
6
47
|
it 'emits delayed.job.run' do
|
|
7
48
|
expect { Delayed::Worker.new.work_off }.to emit_notification('delayed.job.run').with_payload(
|
|
8
49
|
job_name: 'SimpleJob',
|
data/spec/helper.rb
CHANGED
|
@@ -92,6 +92,8 @@ ActiveRecord::Schema.define do
|
|
|
92
92
|
run_migration(IndexFailedJobs)
|
|
93
93
|
run_migration(SetPostgresFillfactor)
|
|
94
94
|
run_migration(RemoveLegacyIndex)
|
|
95
|
+
run_migration(AddRunAtAndNameNotNullCheck)
|
|
96
|
+
run_migration(ValidateRunAtAndNameNotNull)
|
|
95
97
|
|
|
96
98
|
# Test that these index migrations can be re-applied idempotently.
|
|
97
99
|
# (In case identical indexes had been manually applied previously.)
|
|
@@ -228,10 +230,7 @@ RSpec::Matchers.define :emit_notification do |expected_event_name|
|
|
|
228
230
|
diffable
|
|
229
231
|
|
|
230
232
|
match do |block|
|
|
231
|
-
|
|
232
|
-
@expected_value = a_value_within([2, @expected_value.abs * 0.05].max).of(@expected_value)
|
|
233
|
-
end
|
|
234
|
-
|
|
233
|
+
@expected_value = a_value_within([2, @expected_value.abs * 0.05].max).of(@expected_value) if @approximately
|
|
235
234
|
@expected = { event_name: expected_event_name, payload: expected_payload, value: @expected_value }
|
|
236
235
|
@actuals = []
|
|
237
236
|
callback = ->(name, _started, _finished, _unique_id, payload) do
|
|
@@ -269,11 +268,19 @@ def current_database
|
|
|
269
268
|
end
|
|
270
269
|
end
|
|
271
270
|
|
|
271
|
+
def current_database_name
|
|
272
|
+
current_adapter == 'sqlite3' ? 'tmp/database.sqlite' : 'delayed_job_test'
|
|
273
|
+
end
|
|
274
|
+
|
|
272
275
|
QueryUnderTest = Struct.new(:sql, :connection) do
|
|
273
276
|
def self.for(query, connection: ActiveRecord::Base.connection)
|
|
274
277
|
new(query.respond_to?(:to_sql) ? query.to_sql : query.to_s, connection)
|
|
275
278
|
end
|
|
276
279
|
|
|
280
|
+
def full_description
|
|
281
|
+
[formatted, explain].join("\n\n")
|
|
282
|
+
end
|
|
283
|
+
|
|
277
284
|
def formatted
|
|
278
285
|
fmt = sql.squish
|
|
279
286
|
|
|
@@ -287,6 +294,8 @@ QueryUnderTest = Struct.new(:sql, :connection) do
|
|
|
287
294
|
.gsub(/ (AND|OR) /) { "\n #{Regexp.last_match(1).strip} " }
|
|
288
295
|
# normalize and truncate 'AS' names/aliases (changes across Rails versions)
|
|
289
296
|
.gsub(/AS ("|`)?(\w+)("|`)?/) { "AS #{Regexp.last_match(2)[0...63]}" }
|
|
297
|
+
# newline and indent when aliased columns are listed
|
|
298
|
+
.gsub(/AS (\w+),/) { "AS #{Regexp.last_match(1)},\n " }
|
|
290
299
|
# remove quotes around column names in aggregate functions
|
|
291
300
|
.gsub(/(MIN|MAX|COUNT|SUM)\(("|`)(\w+)("|`)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(3)})" }
|
|
292
301
|
end
|
|
@@ -302,16 +311,18 @@ QueryUnderTest = Struct.new(:sql, :connection) do
|
|
|
302
311
|
def postgresql_explain
|
|
303
312
|
connection.execute("SET seq_page_cost = 100")
|
|
304
313
|
connection.execute("SET enable_hashagg = off")
|
|
314
|
+
connection.execute("SET enable_incremental_sort = off")
|
|
305
315
|
connection.execute("SET plan_cache_mode TO force_generic_plan")
|
|
306
316
|
connection.execute("EXPLAIN (VERBOSE) #{sql}").values.flatten.join("\n")
|
|
307
317
|
ensure
|
|
308
318
|
connection.execute("RESET plan_cache_mode")
|
|
319
|
+
connection.execute("RESET enable_incremental_sort")
|
|
309
320
|
connection.execute("RESET enable_hashagg")
|
|
310
321
|
connection.execute("RESET seq_page_cost")
|
|
311
322
|
end
|
|
312
323
|
|
|
313
324
|
def mysql2_explain
|
|
314
|
-
seed_rows! # MySQL needs a bit of data to reach for indexes
|
|
325
|
+
seed_rows! if Delayed::Job.none? # MySQL needs a bit of data to reach for indexes
|
|
315
326
|
connection.execute("ANALYZE TABLE #{Delayed::Job.table_name}")
|
|
316
327
|
connection.execute("SET SESSION max_seeks_for_key = 1")
|
|
317
328
|
connection.execute("EXPLAIN FORMAT=TREE #{sql}").to_a.map(&:first).join("\n")
|
|
@@ -325,12 +336,13 @@ QueryUnderTest = Struct.new(:sql, :connection) do
|
|
|
325
336
|
|
|
326
337
|
def seed_rows!
|
|
327
338
|
now = Delayed::Job.db_time_now
|
|
328
|
-
|
|
339
|
+
100.times do
|
|
329
340
|
[true, false].repeated_combination(5).each_with_index do |(erroring, failed, locked, future), i|
|
|
330
341
|
Delayed::Job.create!(
|
|
331
342
|
run_at: now + (future ? i.minutes : -i.minutes),
|
|
332
343
|
queue: "queue_#{i}",
|
|
333
344
|
handler: "--- !ruby/object:SimpleJob\n",
|
|
345
|
+
name: 'SimpleJob',
|
|
334
346
|
attempts: erroring ? i : 0,
|
|
335
347
|
failed_at: failed ? now - i.minutes : nil,
|
|
336
348
|
locked_at: locked ? now - i.seconds : nil,
|
data/spec/lifecycle_spec.rb
CHANGED
|
@@ -3,7 +3,7 @@ require 'helper'
|
|
|
3
3
|
describe Delayed::Lifecycle do
|
|
4
4
|
let(:lifecycle) { described_class.new }
|
|
5
5
|
let(:callback) { lambda { |*_args| } }
|
|
6
|
-
let(:arguments) { [
|
|
6
|
+
let(:arguments) { [%i(job_a job_b)] }
|
|
7
7
|
let(:behavior) { double(Object, before!: nil, after!: nil, inside!: nil) }
|
|
8
8
|
let(:wrapped_block) { proc { behavior.inside! } }
|
|
9
9
|
|
|
@@ -136,23 +136,13 @@ describe Delayed::MessageSending do
|
|
|
136
136
|
}.to change { Delayed::Job.count }.by(1)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
it '
|
|
140
|
-
Delayed::Worker.delay_jobs = ->
|
|
139
|
+
it 'raises when delay_jobs is a Proc' do
|
|
140
|
+
Delayed::Worker.delay_jobs = -> { true }
|
|
141
141
|
fairy_tail = FairyTail.new
|
|
142
142
|
expect {
|
|
143
143
|
expect {
|
|
144
144
|
fairy_tail.delay.tell('a', kwarg: 'b')
|
|
145
|
-
}.
|
|
146
|
-
}.to change { Delayed::Job.count }.by(1)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it 'does not delay the job when delay_jobs is a proc returning false' do
|
|
150
|
-
Delayed::Worker.delay_jobs = ->(_job) { false }
|
|
151
|
-
fairy_tail = FairyTail.new
|
|
152
|
-
expect {
|
|
153
|
-
expect {
|
|
154
|
-
fairy_tail.delay.tell('a', kwarg: 'b')
|
|
155
|
-
}.to change { fairy_tail.happy_ending }.from(nil).to %w(a b)
|
|
145
|
+
}.to raise_error('Delayed::Worker.delay_jobs may not be a Proc')
|
|
156
146
|
}.not_to(change { Delayed::Job.count })
|
|
157
147
|
end
|
|
158
148
|
end
|
|
@@ -95,12 +95,6 @@ describe Delayed::PerformableMethod do
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
it 'delegates enqueue hook to object' do
|
|
99
|
-
story = Story.create
|
|
100
|
-
expect(story).to receive(:enqueue).with(an_instance_of(Delayed::Job))
|
|
101
|
-
story.delay.tell
|
|
102
|
-
end
|
|
103
|
-
|
|
104
98
|
it 'delegates error hook to object' do
|
|
105
99
|
story = Story.create
|
|
106
100
|
expect(story).to receive(:error).with(an_instance_of(Delayed::Job), an_instance_of(RuntimeError))
|
data/spec/sample_jobs.rb
CHANGED
|
@@ -77,10 +77,6 @@ end
|
|
|
77
77
|
class CallbackJob
|
|
78
78
|
cattr_accessor :messages
|
|
79
79
|
|
|
80
|
-
def enqueue(_job)
|
|
81
|
-
self.class.messages << 'enqueue'
|
|
82
|
-
end
|
|
83
|
-
|
|
84
80
|
def before(_job)
|
|
85
81
|
self.class.messages << 'before'
|
|
86
82
|
end
|
|
@@ -106,12 +102,6 @@ class CallbackJob
|
|
|
106
102
|
end
|
|
107
103
|
end
|
|
108
104
|
|
|
109
|
-
class EnqueueJobMod < SimpleJob
|
|
110
|
-
def enqueue(job)
|
|
111
|
-
job.run_at = 20.minutes.from_now
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
105
|
class ActiveJobJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
|
|
116
106
|
def perform(*args, **kwargs); end
|
|
117
107
|
end
|