delayed 3.0.1 → 4.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 +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/job_wrapper.rb +46 -3
- data/lib/delayed/limitable.rb +83 -0
- data/lib/delayed/priority.rb +28 -20
- 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/priority_spec.rb +30 -1
- data/spec/helper.rb +7 -2
- metadata +24 -6
data/lib/delayed/priority.rb
CHANGED
|
@@ -70,10 +70,12 @@ module Delayed
|
|
|
70
70
|
def names=(names)
|
|
71
71
|
raise "must include a name for priority >= 0" if names && !names.value?(0)
|
|
72
72
|
|
|
73
|
+
remove_named_priority_methods
|
|
73
74
|
@ranges = nil
|
|
74
75
|
@alerts = nil
|
|
75
76
|
@names_to_priority = nil
|
|
76
77
|
@names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
|
|
78
|
+
define_named_priority_methods
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def alerts=(alerts)
|
|
@@ -124,17 +126,35 @@ module Delayed
|
|
|
124
126
|
low + ((high - low).to_d / 2).ceil
|
|
125
127
|
end
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
# Defines a class method (e.g. `Priority.eventual`) and an instance predicate (e.g.
|
|
130
|
+
# `priority.eventual?`) per name, as real, introspectable methods (visible to `methods`,
|
|
131
|
+
# RDoc, and Sorbet's tapioca, unlike `method_missing` dispatch). Pre-existing methods
|
|
132
|
+
# (e.g. a name of `superclass`) are never overridden.
|
|
133
|
+
def define_named_priority_methods
|
|
134
|
+
names.each_key do |name|
|
|
135
|
+
predicate = :"#{name}?"
|
|
136
|
+
define_singleton_method(name) { names_to_priority.fetch(name) } unless method_defined_on?(singleton_class, name)
|
|
137
|
+
define_method(predicate) { name.to_s == to_s } unless method_defined_on?(self, predicate)
|
|
138
|
+
end
|
|
129
139
|
end
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
# Removes the methods previously defined for the current `names` (called before `names` is
|
|
142
|
+
# reassigned). The non-inherited (`false`) checks mean we only remove methods we defined
|
|
143
|
+
# ourselves, leaving any pre-existing method a colliding name never overrode (e.g.
|
|
144
|
+
# `Class#superclass`, `Numeric#zero?`) in place.
|
|
145
|
+
def remove_named_priority_methods
|
|
146
|
+
names.each_key do |name|
|
|
147
|
+
predicate = :"#{name}?"
|
|
148
|
+
singleton_class.send(:remove_method, name) if singleton_class.method_defined?(name, false)
|
|
149
|
+
remove_method(predicate) if method_defined?(predicate, false)
|
|
136
150
|
end
|
|
137
151
|
end
|
|
152
|
+
|
|
153
|
+
# Whether `mod` already responds to `name` (public, protected, private, or inherited), in
|
|
154
|
+
# which case a same-named priority must not override it.
|
|
155
|
+
def method_defined_on?(mod, name)
|
|
156
|
+
mod.method_defined?(name) || mod.private_method_defined?(name)
|
|
157
|
+
end
|
|
138
158
|
end
|
|
139
159
|
|
|
140
160
|
attr_reader :value
|
|
@@ -188,18 +208,6 @@ module Delayed
|
|
|
188
208
|
to_i.to_d
|
|
189
209
|
end
|
|
190
210
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
194
|
-
(method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)) || super
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def method_missing(method_name, *args)
|
|
198
|
-
if method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)
|
|
199
|
-
method_name.to_s[0..-2] == to_s
|
|
200
|
-
else
|
|
201
|
-
super
|
|
202
|
-
end
|
|
203
|
-
end
|
|
211
|
+
send(:define_named_priority_methods)
|
|
204
212
|
end
|
|
205
213
|
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
|