delayed 3.1.0 → 4.0.1
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 +158 -0
- data/app/models/delayed/limit.rb +188 -0
- data/db/migrate/10_create_delayed_limits.rb +30 -0
- data/lib/delayed/active_job_adapter.rb +1 -1
- data/lib/delayed/backend/base.rb +9 -10
- data/lib/delayed/backend/job_preparer.rb +6 -0
- data/lib/delayed/helpers/migration.rb +8 -6
- data/lib/delayed/job_wrapper.rb +51 -14
- data/lib/delayed/limitable.rb +83 -0
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed.rb +3 -0
- data/spec/delayed/active_job_adapter_spec.rb +306 -2
- data/spec/delayed/job_spec.rb +62 -2
- data/spec/delayed/limit_spec.rb +182 -0
- data/spec/delayed/limitable_spec.rb +303 -0
- data/spec/delayed_spec.rb +26 -0
- data/spec/helper.rb +7 -2
- metadata +26 -6
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Delayed
|
|
4
|
+
# A mixin that wraps a class's `perform` method (or other methods named via
|
|
5
|
+
# `on:`) in `Delayed::Limit.within_limit`. It is automatically included in
|
|
6
|
+
# ActiveJob classes, and configures the job to retry with a polynomial
|
|
7
|
+
# backoff when the limit's `wait_timeout` would be exceeded:
|
|
8
|
+
#
|
|
9
|
+
# class TouchesThirdPartyApiJob < ApplicationJob
|
|
10
|
+
# with_limit :third_party_api, max: 100, per: 1.minute
|
|
11
|
+
#
|
|
12
|
+
# def perform
|
|
13
|
+
# # ...
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The 'purpose' defaults to the job's underscored class name, and the limit
|
|
18
|
+
# is registered via `Delayed::Limit.register!` (unless the purpose was
|
|
19
|
+
# already registered, e.g. in an initializer, in which case the `max:`/`per:`
|
|
20
|
+
# config may be omitted entirely). Multiple job classes may share a purpose
|
|
21
|
+
# (and its limit) as long as their configs match exactly.
|
|
22
|
+
#
|
|
23
|
+
# Use `on:` to wrap one or more other instance methods instead of `perform`,
|
|
24
|
+
# e.g. if only a portion of the job's work is subject to the limit:
|
|
25
|
+
#
|
|
26
|
+
# with_limit :third_party_api, max: 100, per: 1.minute, on: :deliver!
|
|
27
|
+
#
|
|
28
|
+
# A class may declare `with_limit` more than once (e.g. to apply different
|
|
29
|
+
# limits to different methods), but only the first declaration defines the
|
|
30
|
+
# job's retry behavior (`attempts`, `wait`, and `jitter`, with `wait_timeout`
|
|
31
|
+
# acting as a floor on the computed wait). If two declarations' wait timeouts
|
|
32
|
+
# differ meaningfully, declare the one with the longer `wait_timeout` first.
|
|
33
|
+
#
|
|
34
|
+
# Retries rely on ActiveJob's `retry_on`, so when this mixin is included in
|
|
35
|
+
# a plain (non-ActiveJob) class, the named methods are still wrapped in
|
|
36
|
+
# `within_limit`, but the class must define its own rescue/retry behavior
|
|
37
|
+
# for `Delayed::Limit::LimitExceededError`.
|
|
38
|
+
module Limitable
|
|
39
|
+
extend ActiveSupport::Concern
|
|
40
|
+
|
|
41
|
+
DEFAULT_RETRY_ATTEMPTS = if defined?(ActiveJob) && ActiveJob.gem_version >= Gem::Version.new('7.0')
|
|
42
|
+
:unlimited
|
|
43
|
+
else
|
|
44
|
+
Float::INFINITY
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class_methods do
|
|
48
|
+
def with_limit(
|
|
49
|
+
purpose = name.underscore.to_sym,
|
|
50
|
+
on: :perform,
|
|
51
|
+
max: nil,
|
|
52
|
+
per: nil,
|
|
53
|
+
wait_timeout: 5.seconds,
|
|
54
|
+
retry_jitter: 0.1,
|
|
55
|
+
retry_attempts: DEFAULT_RETRY_ATTEMPTS,
|
|
56
|
+
retry_wait: ->(attempt) { polynomial_backoff(wait_timeout, attempt, retry_jitter) }
|
|
57
|
+
)
|
|
58
|
+
Delayed::Limit.register!(purpose, max: max, per: per) if max || per
|
|
59
|
+
|
|
60
|
+
if defined?(ActiveJob::Base) && self < ActiveJob::Base &&
|
|
61
|
+
rescue_handlers.none? { |klass, _| klass == Delayed::Limit::LimitExceededError.name }
|
|
62
|
+
retry_on(Delayed::Limit::LimitExceededError, attempts: retry_attempts, wait: retry_wait)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
prepend(Module.new do
|
|
66
|
+
Array(on).each do |method_name|
|
|
67
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
68
|
+
Delayed::Limit.within_limit(purpose, wait_timeout: wait_timeout) do
|
|
69
|
+
super(*args, **kwargs, &block)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def polynomial_backoff(min_wait, attempt, jitter)
|
|
79
|
+
[min_wait, (attempt**4)].max * (1 + rand(-jitter..jitter))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/delayed/version.rb
CHANGED
data/lib/delayed.rb
CHANGED
|
@@ -16,12 +16,14 @@ require 'delayed/backend/job_preparer'
|
|
|
16
16
|
require 'delayed/helpers/migration'
|
|
17
17
|
require 'delayed/worker'
|
|
18
18
|
require 'delayed/job_wrapper'
|
|
19
|
+
require 'delayed/limitable'
|
|
19
20
|
|
|
20
21
|
if defined?(Rails::Engine)
|
|
21
22
|
require 'delayed/engine'
|
|
22
23
|
else
|
|
23
24
|
require 'active_record'
|
|
24
25
|
require_relative '../app/models/delayed/job'
|
|
26
|
+
require_relative '../app/models/delayed/limit'
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
ActiveSupport.on_load(:active_job) do
|
|
@@ -29,6 +31,7 @@ ActiveSupport.on_load(:active_job) do
|
|
|
29
31
|
ActiveJob::QueueAdapters::DelayedAdapter = Class.new(Delayed::ActiveJobAdapter)
|
|
30
32
|
|
|
31
33
|
include Delayed::ActiveJobAdapter::EnqueuingPatch
|
|
34
|
+
include Delayed::Limitable
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
ActiveSupport.on_load(:action_mailer) do
|
|
@@ -113,10 +113,18 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
|
113
113
|
Delayed::Job.last.tap do |dj|
|
|
114
114
|
dj.update!(handler: dj.handler.gsub('JobClass', 'MissingJobClass'))
|
|
115
115
|
expect { dj.payload_object }.not_to raise_error
|
|
116
|
-
|
|
116
|
+
if ActiveJob.gem_version >= Gem::Version.new('8.1')
|
|
117
|
+
expect { dj.payload_object.perform_now }.to raise_error(ActiveJob::UnknownJobClassError)
|
|
118
|
+
else
|
|
119
|
+
expect { dj.payload_object.job_id }.to raise_error(NameError, 'uninitialized constant MissingJobClass')
|
|
120
|
+
end
|
|
117
121
|
end
|
|
118
122
|
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
|
119
|
-
|
|
123
|
+
if ActiveJob.gem_version >= Gem::Version.new('8.1')
|
|
124
|
+
expect(Delayed::Job.last.last_error).to match(/`MissingJobClass` doesn't exist/)
|
|
125
|
+
else
|
|
126
|
+
expect(Delayed::Job.last.last_error).to match(/uninitialized constant MissingJobClass/)
|
|
127
|
+
end
|
|
120
128
|
end
|
|
121
129
|
|
|
122
130
|
it 'deserializes even if an underlying argument gid is not defined' do
|
|
@@ -153,6 +161,12 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
|
153
161
|
expect(enqueued_delayed_jobs.last.priority).to eq(20)
|
|
154
162
|
end
|
|
155
163
|
|
|
164
|
+
it 'ignores a nil priority, applying the default instead' do
|
|
165
|
+
JobClass.set(priority: nil).perform_later
|
|
166
|
+
|
|
167
|
+
expect(enqueued_delayed_jobs.last.priority).to eq(10)
|
|
168
|
+
end
|
|
169
|
+
|
|
156
170
|
it 'raises an error when run_at is used' do
|
|
157
171
|
expect { JobClass.set(run_at: arbitrary_time).perform_later }
|
|
158
172
|
.to raise_error(/`:run_at` is not supported./)
|
|
@@ -196,6 +210,39 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
|
196
210
|
end
|
|
197
211
|
end
|
|
198
212
|
|
|
213
|
+
context 'when using the ActiveJob test adapter' do
|
|
214
|
+
let(:queue_adapter) { :test }
|
|
215
|
+
|
|
216
|
+
it 'raises an error when run_at is used' do
|
|
217
|
+
expect { JobClass.set(run_at: arbitrary_time).perform_later }
|
|
218
|
+
.to raise_error(/`:run_at` is not supported./)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'supports priority as a Delayed::Priority' do
|
|
222
|
+
JobClass.set(priority: Delayed::Priority.eventual).perform_later
|
|
223
|
+
|
|
224
|
+
expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => 20)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'supports priority as a symbol' do
|
|
228
|
+
JobClass.set(priority: :eventual).perform_later
|
|
229
|
+
|
|
230
|
+
expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => 20)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'ignores a nil priority, applying the default instead' do
|
|
234
|
+
JobClass.set(priority: nil).perform_later
|
|
235
|
+
|
|
236
|
+
expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => nil)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'captures arbitrary provider attributes without interfering with enqueue' do
|
|
240
|
+
JobClass.set(foo: 'bar').perform_later
|
|
241
|
+
|
|
242
|
+
expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, queue: 'default')
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
199
246
|
context 'when the ActiveJob performable defines a max_attempts' do
|
|
200
247
|
let(:job_class) do
|
|
201
248
|
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
|
@@ -437,6 +484,263 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
|
437
484
|
end
|
|
438
485
|
end
|
|
439
486
|
|
|
487
|
+
describe 'ActiveJob .retry_on' do
|
|
488
|
+
let(:retry_job_class) do
|
|
489
|
+
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
|
490
|
+
retry_on(RetryTestError)
|
|
491
|
+
retry_on(RetryTestErrorWithSpecificPriority, priority: 123)
|
|
492
|
+
|
|
493
|
+
queue_with_priority 567
|
|
494
|
+
|
|
495
|
+
def perform(error_class_name)
|
|
496
|
+
raise error_class_name.constantize
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
before do
|
|
502
|
+
allow(Delayed::Job).to receive(:enqueue_job).and_call_original
|
|
503
|
+
allow(Delayed::Job).to receive(:enqueue_all).and_call_original
|
|
504
|
+
|
|
505
|
+
stub_const('RetryTestError', Class.new(StandardError))
|
|
506
|
+
stub_const('RetryTestErrorWithSpecificPriority', Class.new(StandardError))
|
|
507
|
+
stub_const('MyRetryJob', retry_job_class)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
context 'when retry_on does not specify a priority' do
|
|
511
|
+
it 're-enqueues a new delayed job with the same priority' do
|
|
512
|
+
MyRetryJob.perform_later('RetryTestError')
|
|
513
|
+
original = Delayed::Job.last
|
|
514
|
+
|
|
515
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
516
|
+
|
|
517
|
+
retried = Delayed::Job.last
|
|
518
|
+
expect(retried.id).not_to eq(original.id)
|
|
519
|
+
expect(retried.priority).to eq(567)
|
|
520
|
+
|
|
521
|
+
expect(retried.payload_object.job_data['priority']).to be_an(Integer)
|
|
522
|
+
expect(retried.payload_object.job_data['priority']).to eq(567)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
it 'reuses a priority and queue set for the specific job run' do
|
|
526
|
+
MyRetryJob.set(priority: 789, queue: 'fake_queue').perform_later('RetryTestError')
|
|
527
|
+
|
|
528
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
529
|
+
|
|
530
|
+
retried = Delayed::Job.last
|
|
531
|
+
expect(retried.priority).to eq(789)
|
|
532
|
+
expect(retried.queue).to eq('fake_queue')
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 're-enqueues with the current priority of the job row, in case it was updated after enqueue' do
|
|
536
|
+
MyRetryJob.perform_later('RetryTestError')
|
|
537
|
+
Delayed::Job.last.update!(priority: 5)
|
|
538
|
+
|
|
539
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
540
|
+
|
|
541
|
+
expect(Delayed::Job.last.priority).to eq(5)
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
context 'when retry_on specifies a priority' do
|
|
546
|
+
it 're-enqueues with the specified priority' do
|
|
547
|
+
MyRetryJob.perform_later('RetryTestErrorWithSpecificPriority')
|
|
548
|
+
|
|
549
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
550
|
+
|
|
551
|
+
expect(Delayed::Job.last.priority).to eq(123)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
it 'records the error that triggered the retry on the re-enqueued job' do
|
|
556
|
+
MyRetryJob.perform_later('RetryTestError')
|
|
557
|
+
|
|
558
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
559
|
+
|
|
560
|
+
expect(Delayed::Job.last.last_error).to start_with('RetryTestError')
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it 'preserves the original creation time on the re-enqueued job' do
|
|
564
|
+
Timecop.freeze(arbitrary_time) do
|
|
565
|
+
MyRetryJob.perform_later('RetryTestError')
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
Timecop.freeze(arbitrary_time + 2.hours) do
|
|
569
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
retried = Delayed::Job.last
|
|
573
|
+
expect(retried.created_at).to eq(arbitrary_time)
|
|
574
|
+
expect(retried.run_at).to be_within(1.second).of(arbitrary_time + 2.hours + 3.seconds)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
context 'when attempts are exhausted' do
|
|
578
|
+
it 'marks the ActiveJob as terminated and fails the job immediately, halting the worker retry backstop' do
|
|
579
|
+
job = MyRetryJob.new('RetryTestError')
|
|
580
|
+
job.exception_executions = { '[RetryTestError]' => 4 } # retry_on defaults to 5 attempts
|
|
581
|
+
job.enqueue
|
|
582
|
+
original = Delayed::Job.last
|
|
583
|
+
|
|
584
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
|
585
|
+
|
|
586
|
+
expect(Delayed::Job.count).to eq(1)
|
|
587
|
+
Delayed::Job.last.tap do |dj|
|
|
588
|
+
expect(dj.id).to eq(original.id)
|
|
589
|
+
expect(dj.attempts).to eq(1)
|
|
590
|
+
expect(dj.failed_at).to be_present
|
|
591
|
+
expect(dj.last_error).to match(/RetryTestError/)
|
|
592
|
+
expect(dj.payload_object.job_data['terminated_at']).to be_present
|
|
593
|
+
expect(dj.payload_object.job_data['exception_executions']).to eq('[RetryTestError]' => 5)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
it 'resets execution attempts if (and only if) the row attempts is set back to 0' do
|
|
598
|
+
MyRetryJob.new('RetryTestError').tap do |job|
|
|
599
|
+
job.executions = 4
|
|
600
|
+
job.exception_executions = { '[RetryTestError]' => 4 }
|
|
601
|
+
job.enqueue
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
|
605
|
+
|
|
606
|
+
Delayed::Job.last.tap do |dj|
|
|
607
|
+
expect(dj.failed_at).to be_present
|
|
608
|
+
expect(dj.payload_object.job_data['terminated_at']).to be_present
|
|
609
|
+
expect(dj.payload_object.job_data['executions']).to eq(4)
|
|
610
|
+
expect(dj.payload_object.job_data['exception_executions']).to eq('[RetryTestError]' => 5)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# without setting attempts back to 0:
|
|
614
|
+
Delayed::Job.last.update!(failed_at: nil, locked_at: nil, locked_by: nil)
|
|
615
|
+
|
|
616
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
|
617
|
+
|
|
618
|
+
retried = Delayed::Job.last
|
|
619
|
+
expect(retried.failed_at).to be_present
|
|
620
|
+
expect(retried.attempts).to eq(2)
|
|
621
|
+
expect(retried.payload_object.job_data['terminated_at']).to be_present
|
|
622
|
+
expect(retried.payload_object.job_data['executions']).to eq(4)
|
|
623
|
+
expect(retried.payload_object.job_data['exception_executions']).to eq('[RetryTestError]' => 6)
|
|
624
|
+
|
|
625
|
+
# also setting attempts back to 0:
|
|
626
|
+
Delayed::Job.last.update!(failed_at: nil, attempts: 0, locked_at: nil, locked_by: nil)
|
|
627
|
+
|
|
628
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
629
|
+
|
|
630
|
+
retried = Delayed::Job.last
|
|
631
|
+
expect(retried.failed_at).to be_nil
|
|
632
|
+
expect(retried.attempts).to eq(0)
|
|
633
|
+
expect(retried.payload_object.job_data['terminated_at']).to be_nil
|
|
634
|
+
expect(retried.payload_object.job_data['executions']).to eq(1)
|
|
635
|
+
expect(retried.payload_object.job_data['exception_executions']).to eq('[RetryTestError]' => 1)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
context 'when an error is not covered by any retry_on declaration' do
|
|
640
|
+
before do
|
|
641
|
+
stub_const('UncoveredTestError', Class.new(StandardError))
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it 'marks the ActiveJob as terminated but leaves the job to be retried by the worker itself' do
|
|
645
|
+
MyRetryJob.perform_later('UncoveredTestError')
|
|
646
|
+
|
|
647
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
|
648
|
+
|
|
649
|
+
expect(Delayed::Job.count).to eq(1)
|
|
650
|
+
Delayed::Job.last.tap do |dj|
|
|
651
|
+
expect(dj.attempts).to eq(1)
|
|
652
|
+
expect(dj.failed_at).to be_nil
|
|
653
|
+
expect(dj.last_error).to match(/UncoveredTestError/)
|
|
654
|
+
expect(dj.payload_object.job_data['terminated_at']).to be_present
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
if ActiveJob.gem_version.release >= Gem::Version.new('7.1')
|
|
660
|
+
context 'when attempts is :unlimited' do
|
|
661
|
+
let(:retry_job_class) do
|
|
662
|
+
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
|
663
|
+
retry_on(RetryTestError, attempts: :unlimited)
|
|
664
|
+
|
|
665
|
+
queue_with_priority 567
|
|
666
|
+
|
|
667
|
+
def perform(error_class_name)
|
|
668
|
+
raise error_class_name.constantize
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it 're-enqueues the job even when executions far exceed the default attempt limit' do
|
|
674
|
+
job = MyRetryJob.new('RetryTestError')
|
|
675
|
+
job.exception_executions = { '[RetryTestError]' => 10_000 }
|
|
676
|
+
job.enqueue
|
|
677
|
+
|
|
678
|
+
expect(Delayed::Worker.new.work_off).to eq([1, 0])
|
|
679
|
+
|
|
680
|
+
retried = Delayed::Job.last
|
|
681
|
+
expect(retried.payload_object.job_data['exception_executions']).to eq('[RetryTestError]' => 10_001)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
context 'when using the ActiveJob test adapter' do
|
|
687
|
+
let(:queue_adapter) { :test }
|
|
688
|
+
|
|
689
|
+
it 're-enqueues with the priority specified by retry_on' do
|
|
690
|
+
ActiveJob::Base.execute(MyRetryJob.new('RetryTestErrorWithSpecificPriority').serialize)
|
|
691
|
+
|
|
692
|
+
expect(MyRetryJob.queue_adapter.enqueued_jobs.first).to include(job: MyRetryJob, 'priority' => 123)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it 're-enqueues with the original priority when retry_on does not specify one' do
|
|
696
|
+
ActiveJob::Base.execute(MyRetryJob.new('RetryTestError').serialize)
|
|
697
|
+
|
|
698
|
+
expect(MyRetryJob.queue_adapter.enqueued_jobs.first).to include(job: MyRetryJob, 'priority' => 567)
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
describe 'legacy job hooks' do
|
|
704
|
+
let(:job_class) do
|
|
705
|
+
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
|
706
|
+
cattr_accessor(:messages) { [] }
|
|
707
|
+
|
|
708
|
+
def perform
|
|
709
|
+
self.class.messages << 'perform'
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def before(_delayed_job)
|
|
713
|
+
self.class.messages << 'before'
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
it 'invokes hooks defined on the job class, with a deprecation warning' do
|
|
719
|
+
JobClass.perform_later
|
|
720
|
+
delayed_job = enqueued_delayed_jobs.last
|
|
721
|
+
|
|
722
|
+
expect { delayed_job.invoke_job }
|
|
723
|
+
.to output(/\[DEPRECATION\] Job hook methods .* are deprecated\. Use ActiveJob callbacks instead\./).to_stderr
|
|
724
|
+
|
|
725
|
+
expect(JobClass.messages).to eq(%w(before perform))
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
context 'when the job class does not define any hook methods' do
|
|
729
|
+
let(:job_class) do
|
|
730
|
+
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
|
731
|
+
def perform; end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
it 'does not emit a deprecation warning' do
|
|
736
|
+
JobClass.perform_later
|
|
737
|
+
delayed_job = enqueued_delayed_jobs.last
|
|
738
|
+
|
|
739
|
+
expect { delayed_job.invoke_job }.not_to output.to_stderr
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
440
744
|
describe '.enqueue_all' do # rubocop:disable Metrics/BlockLength
|
|
441
745
|
let(:adapter) { ActiveJob::Base.queue_adapter }
|
|
442
746
|
|
data/spec/delayed/job_spec.rb
CHANGED
|
@@ -199,6 +199,29 @@ describe Delayed::Job do
|
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
context 'when payload is an ActiveJob wrapper built from previously-serialized job data' do
|
|
203
|
+
let(:arbitrary_time) { Time.parse('2021-01-05 03:34:33 UTC') }
|
|
204
|
+
|
|
205
|
+
it "preserves the wrapped job's enqueued_at as created_at" do
|
|
206
|
+
job_data = Timecop.freeze(arbitrary_time) { ActiveJobJob.new.serialize }
|
|
207
|
+
|
|
208
|
+
job = described_class.enqueue(Delayed::JobWrapper.new(job_data))
|
|
209
|
+
|
|
210
|
+
expect(job.reload.created_at).to eq(arbitrary_time)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if ActiveJob.gem_version.release >= Gem::Version.new('7.1')
|
|
214
|
+
it "preserves the wrapped job's scheduled_at as run_at" do
|
|
215
|
+
active_job = ActiveJobJob.new
|
|
216
|
+
active_job.scheduled_at = arbitrary_time + 1.hour
|
|
217
|
+
|
|
218
|
+
job = described_class.enqueue(Delayed::JobWrapper.new(active_job.serialize))
|
|
219
|
+
|
|
220
|
+
expect(job.run_at).to eq(arbitrary_time + 1.hour)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
202
225
|
context 'when payload is a bare ActiveJob::Base instance' do
|
|
203
226
|
it 'raises' do
|
|
204
227
|
expect { described_class.enqueue(payload_object: ActiveJobJob.new) }.to raise_error(RuntimeError, /Delayed::Job enqueue methods do not accept ActiveJobs/)
|
|
@@ -290,6 +313,26 @@ describe Delayed::Job do
|
|
|
290
313
|
expect { described_class.enqueue_all(jobs) }.to raise_error(RuntimeError, /Delayed::Job enqueue methods do not accept ActiveJobs/)
|
|
291
314
|
end
|
|
292
315
|
end
|
|
316
|
+
|
|
317
|
+
it 'stamps created_at and updated_at' do
|
|
318
|
+
now = described_class.db_time_now
|
|
319
|
+
described_class.enqueue_all([build_job])
|
|
320
|
+
|
|
321
|
+
described_class.last.tap do |job|
|
|
322
|
+
expect(job.created_at).to be_within(1).of(now)
|
|
323
|
+
expect(job.updated_at).to be_within(1).of(now)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'preserves a pre-assigned created_at while still stamping updated_at' do
|
|
328
|
+
original_time = described_class.db_time_now - 3.hours
|
|
329
|
+
described_class.enqueue_all([build_job(SimpleJob.new, created_at: original_time)])
|
|
330
|
+
|
|
331
|
+
described_class.last.tap do |job|
|
|
332
|
+
expect(job.created_at).to be_within(1).of(original_time)
|
|
333
|
+
expect(job.updated_at).to be_within(1).of(described_class.db_time_now)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
293
336
|
end
|
|
294
337
|
|
|
295
338
|
describe '#hook' do
|
|
@@ -306,11 +349,28 @@ describe Delayed::Job do
|
|
|
306
349
|
job = described_class.new(payload_object: JobWithEnqueueHook.new)
|
|
307
350
|
expect(job.payload_object).not_to receive(:enqueue)
|
|
308
351
|
expect { job.hook(:enqueue) }
|
|
309
|
-
.to raise_error(
|
|
352
|
+
.to raise_error(ArgumentError, 'Unknown hook: :enqueue')
|
|
310
353
|
end
|
|
311
354
|
end
|
|
312
355
|
end
|
|
313
356
|
|
|
357
|
+
describe '#invoke_job' do
|
|
358
|
+
it "does not clobber a priority attribute on the object receiving a delayed message" do
|
|
359
|
+
stub_const('PrioritizedThing', Class.new do
|
|
360
|
+
attr_accessor :priority
|
|
361
|
+
|
|
362
|
+
def check_in; end
|
|
363
|
+
end)
|
|
364
|
+
thing = PrioritizedThing.new
|
|
365
|
+
thing.priority = :domain_specific_value
|
|
366
|
+
|
|
367
|
+
job = thing.delay(priority: 3).check_in
|
|
368
|
+
job.invoke_job
|
|
369
|
+
|
|
370
|
+
expect(thing.priority).to eq(:domain_specific_value)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
314
374
|
describe 'callbacks' do
|
|
315
375
|
before(:each) do
|
|
316
376
|
CallbackJob.messages = []
|
|
@@ -1023,7 +1083,7 @@ describe Delayed::Job do
|
|
|
1023
1083
|
worker.work_off
|
|
1024
1084
|
@job.reload
|
|
1025
1085
|
expect(@job.last_error).to match(/did not work/)
|
|
1026
|
-
expect(@job.last_error).to match(/sample_jobs.rb:\d+:in
|
|
1086
|
+
expect(@job.last_error).to match(/sample_jobs.rb:\d+:in (`|'ErrorJob#)perform'/)
|
|
1027
1087
|
expect(@job.attempts).to eq(1)
|
|
1028
1088
|
expect(@job.run_at).to be > described_class.db_time_now - 10.minutes
|
|
1029
1089
|
expect(@job.run_at).to be < described_class.db_time_now + 10.minutes
|