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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/app/models/delayed/job.rb +25 -18
  4. data/db/migrate/08_add_run_at_and_name_not_null_check.rb +34 -0
  5. data/db/migrate/09_validate_run_at_and_name_not_null.rb +41 -0
  6. data/lib/delayed/active_job_adapter.rb +41 -8
  7. data/lib/delayed/backend/base.rb +67 -11
  8. data/lib/delayed/backend/job_preparer.rb +33 -0
  9. data/lib/delayed/exceptions.rb +2 -0
  10. data/lib/delayed/lifecycle.rb +1 -1
  11. data/lib/delayed/monitor.rb +69 -43
  12. data/lib/delayed/plugins/instrumentation.rb +9 -3
  13. data/lib/delayed/version.rb +1 -1
  14. data/lib/delayed/worker.rb +2 -8
  15. data/spec/delayed/__snapshots__/monitor_spec.rb.snap +447 -1170
  16. data/spec/delayed/active_job_adapter_spec.rb +285 -46
  17. data/spec/delayed/job_spec.rb +218 -32
  18. data/spec/delayed/monitor_spec.rb +18 -19
  19. data/spec/delayed/plugins/instrumentation_spec.rb +41 -0
  20. data/spec/helper.rb +18 -6
  21. data/spec/lifecycle_spec.rb +1 -1
  22. data/spec/message_sending_spec.rb +3 -13
  23. data/spec/performable_method_spec.rb +0 -6
  24. data/spec/sample_jobs.rb +0 -10
  25. metadata +12 -10
  26. /data/db/migrate/{1_create_delayed_jobs.rb → 01_create_delayed_jobs.rb} +0 -0
  27. /data/db/migrate/{2_add_name_to_delayed_jobs.rb → 02_add_name_to_delayed_jobs.rb} +0 -0
  28. /data/db/migrate/{3_add_index_to_delayed_jobs_name.rb → 03_add_index_to_delayed_jobs_name.rb} +0 -0
  29. /data/db/migrate/{4_index_live_jobs.rb → 04_index_live_jobs.rb} +0 -0
  30. /data/db/migrate/{5_index_failed_jobs.rb → 05_index_failed_jobs.rb} +0 -0
  31. /data/db/migrate/{6_set_postgres_fillfactor.rb → 06_set_postgres_fillfactor.rb} +0 -0
  32. /data/db/migrate/{7_remove_legacy_index.rb → 07_remove_legacy_index.rb} +0 -0
@@ -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.create(payload_object: ErrorJob.new).run_at).not_to be_nil
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.create(payload_object: ErrorJob.new, run_at: later)
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(['enqueue'])
329
+ expect(CallbackJob.messages).to eq([])
154
330
  job.invoke_job
155
- expect(CallbackJob.messages).to eq(%w(enqueue before perform success after))
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(['enqueue', 'before', 'error: RuntimeError', 'after'])
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(['enqueue', 'error: RuntimeError', 'after'])
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.new(payload_object: ErrorJob.new)
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.new(payload_object: job_wrapper)
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.new(payload_object: NamedJob.new)
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.new(payload_object: ErrorJob.new)
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.create(payload_object: ErrorJob.new)
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.create(payload_object: job_wrapper)
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.create payload_object: LongRunningJob.new
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.create! payload_object: LongRunningJob.new
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.create payload_object: SimpleJob.new
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.create payload_object: OnPermanentFailureJob.new
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 '#query_for' do
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 queries_for(metric)
368
- monitor.query_for(metric)
369
- queries.map { |q| QueryUnderTest.for(q) }
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
- described_class::METRICS.each do |metric|
373
- context "('#{metric}')" do
374
- it "runs the expected #{current_adapter} query for #{metric}" do
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
- context 'when using the legacy index', :with_legacy_table_index do
383
- it "[legacy index] produces the expected #{current_adapter} query plan for #{metric}" do
384
- expect(queries_for(metric).map(&:explain).join("\n")).to match_snapshot
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
- if @approximately && current_adapter != 'postgresql'
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
- 10.times do
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,
@@ -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) { [1] }
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 'does delay when delay_jobs is a proc returning true' do
140
- Delayed::Worker.delay_jobs = ->(_job) { true }
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
- }.not_to change { fairy_tail.happy_ending }
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