delayed 0.5.5 → 1.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 +4 -0
- data/lib/delayed/active_job_adapter.rb +6 -1
- data/lib/delayed/job_wrapper.rb +18 -0
- data/lib/delayed/lifecycle.rb +1 -1
- data/lib/delayed/message_sending.rb +4 -3
- data/lib/delayed/plugins/connection.rb +2 -2
- data/lib/delayed/priority.rb +1 -0
- data/lib/delayed/runnable.rb +1 -1
- data/lib/delayed/version.rb +5 -0
- data/lib/delayed/worker.rb +40 -23
- data/lib/delayed.rb +1 -1
- data/spec/delayed/active_job_adapter_spec.rb +48 -7
- data/spec/delayed/job_spec.rb +4 -4
- data/spec/delayed/priority_spec.rb +7 -0
- data/spec/delayed/tasks_spec.rb +2 -2
- data/spec/helper.rb +26 -0
- data/spec/sample_jobs.rb +1 -1
- data/spec/worker_spec.rb +177 -58
- metadata +4 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e2ec868563930c9d66a6db2eb638559b20497de5a819d543694c8fa4814519e
|
4
|
+
data.tar.gz: b48f6b5951f5c7309ea8db1b693451d4501c6f87852b843e5b1c49a5113cd4ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17289bfb6ea1de0202570e44351e023a9024395bd2df12121b848e9813fc6eebd57c71abf709c61f7625697fddb097dc22d8bc1c2e0b714996b9e0cbdac7ceee
|
7
|
+
data.tar.gz: 76b59feb415bf66841659c05aeb421a69d59579161004da33b11de4937fe7e30c232da6bab17008c12489cf65186d8c316b371f5c41deb710e5534796b95ba20
|
data/README.md
CHANGED
@@ -432,6 +432,10 @@ Delayed::Worker.read_ahead = 5
|
|
432
432
|
|
433
433
|
# If a worker finds no jobs, it will sleep this number of seconds in between attempts:
|
434
434
|
Delayed::Worker.sleep_delay = 5
|
435
|
+
|
436
|
+
# Until version 1.0, the worker will not sleep at all between attemps if it finds jobs.
|
437
|
+
# This can be configured by setting the minimum reserve interval:
|
438
|
+
Delayed::Worker.min_reserve_interval = 0.5 # seconds
|
435
439
|
```
|
436
440
|
|
437
441
|
If a job fails, it will be rerun up to 25 times (with an exponential back-off). Jobs will also
|
@@ -17,7 +17,7 @@ module Delayed
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def _enqueue(job, opts = {})
|
20
|
-
if
|
20
|
+
if enqueue_after_transaction_commit_enabled?(job)
|
21
21
|
raise UnsafeEnqueueError, "The ':delayed' ActiveJob adapter is not compatible with enqueue_after_transaction_commit"
|
22
22
|
end
|
23
23
|
|
@@ -29,6 +29,11 @@ module Delayed
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
def enqueue_after_transaction_commit_enabled?(job)
|
33
|
+
job.class.respond_to?(:enqueue_after_transaction_commit) &&
|
34
|
+
[true, :always].include?(job.class.enqueue_after_transaction_commit)
|
35
|
+
end
|
36
|
+
|
32
37
|
module EnqueuingPatch
|
33
38
|
def self.included(klass)
|
34
39
|
klass.prepend PrependedMethods
|
data/lib/delayed/job_wrapper.rb
CHANGED
@@ -20,6 +20,24 @@ module Delayed
|
|
20
20
|
job_data['job_class']
|
21
21
|
end
|
22
22
|
|
23
|
+
# If job failed to deserialize, we can't respond to delegated methods.
|
24
|
+
# Returning false here prevents instance method checks from blocking job cleanup.
|
25
|
+
# There is a (currently) unreleased Rails PR that changes the exception class in this case:
|
26
|
+
# https://github.com/rails/rails/pull/53770
|
27
|
+
if defined?(ActiveJob::UnknownJobClassError)
|
28
|
+
def respond_to?(*, **)
|
29
|
+
super
|
30
|
+
rescue ActiveJob::UnknownJobClassError
|
31
|
+
false
|
32
|
+
end
|
33
|
+
else
|
34
|
+
def respond_to?(*, **)
|
35
|
+
super
|
36
|
+
rescue NameError
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
23
41
|
def perform
|
24
42
|
ActiveJob::Callbacks.run_callbacks(:execute) do
|
25
43
|
job.perform_now
|
data/lib/delayed/lifecycle.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
require 'active_support/proxy_object'
|
2
|
-
|
3
1
|
module Delayed
|
4
|
-
class DelayProxy <
|
2
|
+
class DelayProxy < BasicObject
|
3
|
+
undef_method :==
|
4
|
+
undef_method :equal?
|
5
|
+
|
5
6
|
def initialize(payload_class, target, options)
|
6
7
|
@payload_class = payload_class
|
7
8
|
@target = target
|
@@ -2,9 +2,9 @@ module Delayed
|
|
2
2
|
module Plugins
|
3
3
|
class Connection < Plugin
|
4
4
|
callbacks do |lifecycle|
|
5
|
-
lifecycle.around(:thread) do |worker,
|
5
|
+
lifecycle.around(:thread) do |worker, &block|
|
6
6
|
Job.connection_pool.with_connection do
|
7
|
-
block.call(worker
|
7
|
+
block.call(worker)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
data/lib/delayed/priority.rb
CHANGED
data/lib/delayed/runnable.rb
CHANGED
data/lib/delayed/worker.rb
CHANGED
@@ -12,6 +12,7 @@ module Delayed
|
|
12
12
|
include Runnable
|
13
13
|
|
14
14
|
cattr_accessor :sleep_delay, instance_writer: false, default: 5
|
15
|
+
cattr_accessor :min_reserve_interval, instance_writer: false, default: 0
|
15
16
|
cattr_accessor :max_attempts, instance_writer: false, default: 25
|
16
17
|
cattr_accessor :max_claims, instance_writer: false, default: 5
|
17
18
|
cattr_accessor :max_run_time, instance_writer: false, default: 20.minutes
|
@@ -92,6 +93,7 @@ module Delayed
|
|
92
93
|
total = 0
|
93
94
|
|
94
95
|
while total < num
|
96
|
+
start = clock_time
|
95
97
|
jobs = reserve_jobs
|
96
98
|
break if jobs.empty?
|
97
99
|
|
@@ -99,7 +101,15 @@ module Delayed
|
|
99
101
|
pool = Concurrent::FixedThreadPool.new(jobs.length)
|
100
102
|
jobs.each do |job|
|
101
103
|
pool.post do
|
102
|
-
|
104
|
+
self.class.lifecycle.run_callbacks(:thread, self) do
|
105
|
+
success.increment if perform(job)
|
106
|
+
rescue DeserializationError => e
|
107
|
+
handle_unrecoverable_error(job, e)
|
108
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
109
|
+
handle_erroring_job(job, e)
|
110
|
+
end
|
111
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
112
|
+
say "Job thread crashed with #{e.class.name}: #{e.message}", 'error'
|
103
113
|
end
|
104
114
|
end
|
105
115
|
|
@@ -107,17 +117,16 @@ module Delayed
|
|
107
117
|
pool.wait_for_termination
|
108
118
|
|
109
119
|
break if stop? # leave if we're exiting
|
120
|
+
|
121
|
+
elapsed = clock_time - start
|
122
|
+
interruptable_sleep(self.class.min_reserve_interval - elapsed)
|
110
123
|
end
|
111
124
|
|
112
125
|
[success.value, total - success.value]
|
113
126
|
end
|
114
127
|
|
115
|
-
def
|
116
|
-
self.class.lifecycle.run_callbacks(:
|
117
|
-
end
|
118
|
-
|
119
|
-
def run(job)
|
120
|
-
run_thread_callbacks(job) do
|
128
|
+
def perform(job)
|
129
|
+
self.class.lifecycle.run_callbacks(:perform, self, job) do
|
121
130
|
metadata = {
|
122
131
|
status: 'RUNNING',
|
123
132
|
name: job.name,
|
@@ -139,13 +148,10 @@ module Delayed
|
|
139
148
|
end
|
140
149
|
true # did work
|
141
150
|
rescue DeserializationError => e
|
142
|
-
|
143
|
-
|
144
|
-
job.error = e
|
145
|
-
failed(job)
|
151
|
+
handle_unrecoverable_error(job, e)
|
146
152
|
false # work failed
|
147
153
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
148
|
-
|
154
|
+
handle_erroring_job(job, e)
|
149
155
|
false # work failed
|
150
156
|
end
|
151
157
|
|
@@ -166,12 +172,12 @@ module Delayed
|
|
166
172
|
def failed(job)
|
167
173
|
self.class.lifecycle.run_callbacks(:failure, self, job) do
|
168
174
|
job.hook(:failure)
|
169
|
-
rescue StandardError => e
|
170
|
-
say "Error when running failure callback: #{e}", 'error'
|
171
|
-
say e.backtrace.join("\n"), 'error'
|
172
|
-
ensure
|
173
|
-
job.destroy_failed_jobs? ? job.destroy : job.fail!
|
174
175
|
end
|
176
|
+
rescue StandardError => e
|
177
|
+
say "Error when running failure callback: #{e}", 'error'
|
178
|
+
say e.backtrace.join("\n"), 'error'
|
179
|
+
ensure
|
180
|
+
job.destroy_failed_jobs? ? job.destroy : job.fail!
|
175
181
|
end
|
176
182
|
|
177
183
|
def job_say(job, text, level = Delayed.default_log_level)
|
@@ -198,14 +204,21 @@ module Delayed
|
|
198
204
|
" (queue=#{queue})" if queue
|
199
205
|
end
|
200
206
|
|
201
|
-
def
|
202
|
-
|
203
|
-
|
204
|
-
|
207
|
+
def handle_erroring_job(job, error)
|
208
|
+
self.class.lifecycle.run_callbacks(:error, self, job) do
|
209
|
+
job.error = error
|
210
|
+
job_say job, "FAILED (#{job.attempts} prior attempts) with #{error.class.name}: #{error.message}", 'error'
|
211
|
+
reschedule(job)
|
212
|
+
end
|
213
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
214
|
+
handle_unrecoverable_error(job, e)
|
205
215
|
end
|
206
216
|
|
207
|
-
def
|
208
|
-
|
217
|
+
def handle_unrecoverable_error(job, error)
|
218
|
+
job_say job, "FAILED permanently with #{error.class.name}: #{error.message}", 'error'
|
219
|
+
|
220
|
+
job.error = error
|
221
|
+
failed(job)
|
209
222
|
end
|
210
223
|
|
211
224
|
# The backend adapter may return either a list or a single job
|
@@ -227,5 +240,9 @@ module Delayed
|
|
227
240
|
def reload!
|
228
241
|
Rails.application.reloader.reload! if defined?(Rails.application.reloader) && Rails.application.reloader.check!
|
229
242
|
end
|
243
|
+
|
244
|
+
def clock_time
|
245
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
246
|
+
end
|
230
247
|
end
|
231
248
|
end
|
data/lib/delayed.rb
CHANGED
@@ -49,11 +49,11 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
49
49
|
/ priority: ?\n/,
|
50
50
|
" arguments: []\n",
|
51
51
|
" executions: 0\n",
|
52
|
-
(" exception_executions: {}\n" if ActiveJob
|
52
|
+
(" exception_executions: {}\n" if ActiveJob.gem_version >= Gem::Version.new('6.0')),
|
53
53
|
" locale: en\n",
|
54
|
-
(/ timezone: ?\n/ if ActiveJob
|
55
|
-
(/ enqueued_at: '2023-01-20T18:52:29(\.\d+)?Z'\n/ if ActiveJob
|
56
|
-
(/ scheduled_at: ?\n/ if ActiveJob
|
54
|
+
(/ timezone: ?\n/ if ActiveJob.gem_version >= Gem::Version.new('6.0')),
|
55
|
+
(/ enqueued_at: '2023-01-20T18:52:29(\.\d+)?Z'\n/ if ActiveJob.gem_version >= Gem::Version.new('6.0')),
|
56
|
+
(/ scheduled_at: ?\n/ if ActiveJob.gem_version >= Gem::Version.new('7.1')),
|
57
57
|
].compact
|
58
58
|
end
|
59
59
|
end
|
@@ -62,10 +62,23 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
62
62
|
JobClass.perform_later
|
63
63
|
|
64
64
|
Delayed::Job.last.tap do |dj|
|
65
|
-
dj.handler
|
65
|
+
dj.update!(handler: dj.handler.gsub('JobClass', 'MissingJobClass'))
|
66
66
|
expect { dj.payload_object }.not_to raise_error
|
67
67
|
expect { dj.payload_object.job_id }.to raise_error(NameError, 'uninitialized constant MissingJobClass')
|
68
68
|
end
|
69
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
70
|
+
expect(Delayed::Job.last.last_error).to match(/uninitialized constant MissingJobClass/)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'deserializes even if an underlying argument gid is not defined' do
|
74
|
+
ActiveJobJob.perform_later(story: Story.create!)
|
75
|
+
Delayed::Job.last.tap do |dj|
|
76
|
+
dj.update!(handler: dj.handler.gsub('Story', 'MissingArgumentClass'))
|
77
|
+
expect { dj.payload_object }.not_to raise_error
|
78
|
+
expect { dj.payload_object.perform_now }.to raise_error(ActiveJob::DeserializationError)
|
79
|
+
end
|
80
|
+
expect(Delayed::Worker.new.work_off).to eq([0, 1])
|
81
|
+
expect(Delayed::Job.last.last_error).to match(/Error while trying to deserialize arguments/)
|
69
82
|
end
|
70
83
|
|
71
84
|
describe '.set' do
|
@@ -295,7 +308,9 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
295
308
|
end
|
296
309
|
|
297
310
|
it 'raises an exception on enqueue' do
|
298
|
-
|
311
|
+
ActiveJob.deprecator.silence do
|
312
|
+
expect { JobClass.perform_later }.to raise_error(Delayed::ActiveJobAdapter::UnsafeEnqueueError)
|
313
|
+
end
|
299
314
|
end
|
300
315
|
end
|
301
316
|
|
@@ -306,7 +321,33 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
306
321
|
end
|
307
322
|
|
308
323
|
it 'does not raises an exception on enqueue' do
|
309
|
-
|
324
|
+
ActiveJob.deprecator.silence do
|
325
|
+
expect { JobClass.perform_later }.not_to raise_error
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
if ActiveJob.gem_version.release >= Gem::Version.new('8.0')
|
332
|
+
context 'when the given job sets enqueue_after_transaction_commit to true' do
|
333
|
+
before do
|
334
|
+
JobClass.include ActiveJob::EnqueueAfterTransactionCommit # normally run in an ActiveJob railtie
|
335
|
+
JobClass.enqueue_after_transaction_commit = true
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'raises an exception on enqueue' do
|
339
|
+
expect { JobClass.perform_later }.to raise_error(Delayed::ActiveJobAdapter::UnsafeEnqueueError)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
context 'when the given job sets enqueue_after_transaction_commit to false' do
|
344
|
+
before do
|
345
|
+
JobClass.include ActiveJob::EnqueueAfterTransactionCommit # normally run in an ActiveJob railtie
|
346
|
+
JobClass.enqueue_after_transaction_commit = false
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'does not raises an exception on enqueue' do
|
350
|
+
expect { JobClass.perform_later }.not_to raise_error
|
310
351
|
end
|
311
352
|
end
|
312
353
|
end
|
data/spec/delayed/job_spec.rb
CHANGED
@@ -533,7 +533,7 @@ describe Delayed::Job do
|
|
533
533
|
it 'fails after Worker.max_run_time' do
|
534
534
|
Delayed::Worker.max_run_time = 1.second
|
535
535
|
job = described_class.create payload_object: LongRunningJob.new
|
536
|
-
worker.
|
536
|
+
worker.perform(job)
|
537
537
|
expect(job.error).not_to be_nil
|
538
538
|
expect(job.reload.last_error).to match(/expired/)
|
539
539
|
expect(job.reload.last_error).to match(/Delayed::Worker\.max_run_time is only 1 second/)
|
@@ -558,7 +558,7 @@ describe Delayed::Job do
|
|
558
558
|
|
559
559
|
it 'records last_error when destroy_failed_jobs = false, max_attempts = 1' do
|
560
560
|
Delayed::Worker.max_attempts = 1
|
561
|
-
worker.
|
561
|
+
worker.perform(@job)
|
562
562
|
@job.reload
|
563
563
|
expect(@job.error).not_to be_nil
|
564
564
|
expect(@job.last_error).to match(/did not work/)
|
@@ -580,7 +580,7 @@ describe Delayed::Job do
|
|
580
580
|
|
581
581
|
it 're-schedules jobs with handler provided time if present' do
|
582
582
|
job = described_class.enqueue(CustomRescheduleJob.new(99.minutes))
|
583
|
-
worker.
|
583
|
+
worker.perform(job)
|
584
584
|
job.reload
|
585
585
|
|
586
586
|
expect((described_class.db_time_now + 99.minutes - job.run_at).abs).to be < 1
|
@@ -590,7 +590,7 @@ describe Delayed::Job do
|
|
590
590
|
error_with_nil_message = StandardError.new
|
591
591
|
expect(error_with_nil_message).to receive(:message).twice.and_return(nil)
|
592
592
|
expect(@job).to receive(:invoke_job).and_raise error_with_nil_message
|
593
|
-
expect { worker.
|
593
|
+
expect { worker.perform(@job) }.not_to raise_error
|
594
594
|
end
|
595
595
|
end
|
596
596
|
|
@@ -207,6 +207,13 @@ RSpec.describe Delayed::Priority do
|
|
207
207
|
expect(described_class.new(101)).to eq described_class.new(101) # rubocop:disable RSpec/IdenticalEqualityAssertion
|
208
208
|
end
|
209
209
|
|
210
|
+
it 'supports explicit casting' do
|
211
|
+
expect(described_class.new(0).to_i).to eq 0
|
212
|
+
expect(described_class.new(3).to_f).to eq 3.0
|
213
|
+
expect(described_class.new(10).to_s).to eq 'user_visible'
|
214
|
+
expect(described_class.new(:eventual).to_d).to eq '20'.to_d
|
215
|
+
end
|
216
|
+
|
210
217
|
it 'suports coercion' do
|
211
218
|
expect(described_class.new(0)).to eq 0
|
212
219
|
expect(described_class.new(8)).to be > 5
|
data/spec/delayed/tasks_spec.rb
CHANGED
@@ -64,7 +64,7 @@ describe 'rake' do
|
|
64
64
|
.to change { Delayed::Worker.min_priority }.from(nil).to(6)
|
65
65
|
.and change { Delayed::Worker.max_priority }.from(nil).to(8)
|
66
66
|
.and change { Delayed::Worker.queues }.from([]).to(%w(foo bar))
|
67
|
-
.and change { Delayed::Worker.sleep_delay }.from(
|
67
|
+
.and change { Delayed::Worker.sleep_delay }.from(TEST_SLEEP_DELAY).to(1)
|
68
68
|
.and change { Delayed::Worker.read_ahead }.from(5).to(3)
|
69
69
|
.and change { Delayed::Worker.max_claims }.from(5).to(3)
|
70
70
|
end
|
@@ -96,7 +96,7 @@ describe 'rake' do
|
|
96
96
|
.to change { Delayed::Worker.min_priority }.from(nil).to(6)
|
97
97
|
.and change { Delayed::Worker.max_priority }.from(nil).to(8)
|
98
98
|
.and change { Delayed::Worker.queues }.from([]).to(%w(foo))
|
99
|
-
.and change { Delayed::Worker.sleep_delay }.from(
|
99
|
+
.and change { Delayed::Worker.sleep_delay }.from(TEST_SLEEP_DELAY).to(1)
|
100
100
|
.and change { Delayed::Worker.read_ahead }.from(5).to(3)
|
101
101
|
.and change { Delayed::Worker.max_claims }.from(5).to(3)
|
102
102
|
end
|
data/spec/helper.rb
CHANGED
@@ -10,6 +10,19 @@ require 'sample_jobs'
|
|
10
10
|
|
11
11
|
require 'rake'
|
12
12
|
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
require 'global_id/identification'
|
15
|
+
include GlobalID::Identification
|
16
|
+
GlobalID.app = 'test'
|
17
|
+
end
|
18
|
+
|
19
|
+
if ActiveSupport.gem_version >= Gem::Version.new('7.1')
|
20
|
+
frameworks = [ActiveModel, ActiveRecord, ActionMailer, ActiveJob, ActiveSupport]
|
21
|
+
frameworks.each { |framework| framework.deprecator.behavior = :raise }
|
22
|
+
else
|
23
|
+
ActiveSupport::Deprecation.behavior = :raise
|
24
|
+
end
|
25
|
+
|
13
26
|
if ENV['DEBUG_LOGS']
|
14
27
|
Delayed.logger = Logger.new($stdout)
|
15
28
|
else
|
@@ -29,6 +42,7 @@ db_adapter ||= "sqlite3"
|
|
29
42
|
config = YAML.load(ERB.new(File.read("spec/database.yml")).result)
|
30
43
|
ActiveRecord::Base.establish_connection config[db_adapter]
|
31
44
|
ActiveRecord::Base.logger = Delayed.logger
|
45
|
+
ActiveJob::Base.logger = Delayed.logger
|
32
46
|
ActiveRecord::Migration.verbose = false
|
33
47
|
|
34
48
|
# MySQL 5.7 no longer supports null default values for the primary key
|
@@ -97,6 +111,11 @@ class SingletonClass
|
|
97
111
|
include Singleton
|
98
112
|
end
|
99
113
|
|
114
|
+
# Negative values are treated as sleep(0),
|
115
|
+
# so we can use different values to test the sleep behavior:
|
116
|
+
TEST_MIN_RESERVE_INTERVAL = -10
|
117
|
+
TEST_SLEEP_DELAY = -100
|
118
|
+
|
100
119
|
RSpec.configure do |config|
|
101
120
|
config.around(:each) do |example|
|
102
121
|
aj_priority_was = ActiveJob::Base.priority
|
@@ -113,6 +132,11 @@ RSpec.configure do |config|
|
|
113
132
|
queues_was = Delayed::Worker.queues
|
114
133
|
read_ahead_was = Delayed::Worker.read_ahead
|
115
134
|
sleep_delay_was = Delayed::Worker.sleep_delay
|
135
|
+
min_reserve_interval_was = Delayed::Worker.min_reserve_interval
|
136
|
+
plugins_was = Delayed.plugins.dup
|
137
|
+
|
138
|
+
Delayed::Worker.sleep_delay = TEST_SLEEP_DELAY
|
139
|
+
Delayed::Worker.min_reserve_interval = TEST_MIN_RESERVE_INTERVAL
|
116
140
|
|
117
141
|
example.run
|
118
142
|
ensure
|
@@ -130,6 +154,8 @@ RSpec.configure do |config|
|
|
130
154
|
Delayed::Worker.queues = queues_was
|
131
155
|
Delayed::Worker.read_ahead = read_ahead_was
|
132
156
|
Delayed::Worker.sleep_delay = sleep_delay_was
|
157
|
+
Delayed::Worker.min_reserve_interval = min_reserve_interval_was
|
158
|
+
Delayed.plugins = plugins_was
|
133
159
|
|
134
160
|
Delayed::Job.delete_all
|
135
161
|
end
|
data/spec/sample_jobs.rb
CHANGED
data/spec/worker_spec.rb
CHANGED
@@ -1,10 +1,6 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
describe Delayed::Worker do
|
4
|
-
before do
|
5
|
-
described_class.sleep_delay = 0
|
6
|
-
end
|
7
|
-
|
8
4
|
describe 'start' do
|
9
5
|
it 'runs the :execute lifecycle hook' do
|
10
6
|
performances = []
|
@@ -32,62 +28,74 @@ describe Delayed::Worker do
|
|
32
28
|
allow(subject).to receive(:interruptable_sleep).and_call_original
|
33
29
|
end
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
around do |example|
|
32
|
+
max_claims_was = described_class.max_claims
|
33
|
+
described_class.max_claims = max_claims
|
34
|
+
example.run
|
35
|
+
ensure
|
36
|
+
described_class.max_claims = max_claims_was
|
37
|
+
end
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
expect(Delayed.logger).not_to have_received(:info)
|
43
|
-
expect(subject).to have_received(:interruptable_sleep)
|
44
|
-
end
|
39
|
+
before do
|
40
|
+
allow(Delayed::Job).to receive(:reserve).and_return((0...jobs_returned).map { job }, [])
|
45
41
|
end
|
46
42
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
let(:max_claims) { 1 }
|
44
|
+
let(:jobs_returned) { 1 }
|
45
|
+
let(:job) do
|
46
|
+
instance_double(
|
47
|
+
Delayed::Job,
|
48
|
+
id: 123,
|
49
|
+
max_run_time: 10,
|
50
|
+
name: 'MyJob',
|
51
|
+
run_at: Delayed::Job.db_time_now,
|
52
|
+
created_at: Delayed::Job.db_time_now,
|
53
|
+
priority: Delayed::Priority.interactive,
|
54
|
+
queue: 'testqueue',
|
55
|
+
attempts: 0,
|
56
|
+
invoke_job: true,
|
57
|
+
destroy: true,
|
58
|
+
)
|
59
|
+
end
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
it 'logs the count and sleeps only within the loop' do
|
62
|
+
subject.run!
|
63
|
+
expect(Delayed.logger).to have_received(:info).with(/1 jobs processed/)
|
64
|
+
expect(subject).to have_received(:interruptable_sleep).once.with(a_value_within(1).of(TEST_MIN_RESERVE_INTERVAL))
|
65
|
+
expect(subject).not_to have_received(:interruptable_sleep).with(TEST_SLEEP_DELAY)
|
66
|
+
end
|
59
67
|
|
60
|
-
|
61
|
-
let(:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
run_at: Delayed::Job.db_time_now,
|
68
|
-
created_at: Delayed::Job.db_time_now,
|
69
|
-
priority: Delayed::Priority.interactive,
|
70
|
-
queue: 'testqueue',
|
71
|
-
attempts: 0,
|
72
|
-
invoke_job: true,
|
73
|
-
destroy: true,
|
74
|
-
)
|
68
|
+
context 'when no jobs are returned' do
|
69
|
+
let(:jobs_returned) { 0 }
|
70
|
+
|
71
|
+
it 'does not log and then sleeps only outside of the loop' do
|
72
|
+
subject.run!
|
73
|
+
expect(Delayed.logger).not_to have_received(:info)
|
74
|
+
expect(subject).to have_received(:interruptable_sleep).with(TEST_SLEEP_DELAY)
|
75
75
|
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when max_claims is 3 and 3 jobs are returned' do
|
79
|
+
let(:max_claims) { 3 }
|
80
|
+
let(:jobs_returned) { 3 }
|
76
81
|
|
77
|
-
it 'logs the count and
|
82
|
+
it 'logs the count and sleeps only in the loop' do
|
78
83
|
subject.run!
|
79
|
-
expect(Delayed.logger).to have_received(:info).with(/
|
80
|
-
expect(subject).
|
84
|
+
expect(Delayed.logger).to have_received(:info).with(/3 jobs processed/)
|
85
|
+
expect(subject).to have_received(:interruptable_sleep).once.with(a_value_within(1).of(TEST_MIN_RESERVE_INTERVAL))
|
86
|
+
expect(subject).not_to have_received(:interruptable_sleep).with(TEST_SLEEP_DELAY)
|
81
87
|
end
|
88
|
+
end
|
82
89
|
|
83
|
-
|
84
|
-
|
90
|
+
context 'when max_claims is 3 and 2 jobs are returned' do
|
91
|
+
let(:max_claims) { 3 }
|
92
|
+
let(:jobs_returned) { 2 }
|
85
93
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
94
|
+
it 'logs the count and sleeps both in the loop and outside of the loop' do
|
95
|
+
subject.run!
|
96
|
+
expect(Delayed.logger).to have_received(:info).with(/2 jobs processed/)
|
97
|
+
expect(subject).to have_received(:interruptable_sleep).once.with(a_value_within(1).of(TEST_MIN_RESERVE_INTERVAL))
|
98
|
+
expect(subject).to have_received(:interruptable_sleep).once.with(TEST_SLEEP_DELAY)
|
91
99
|
end
|
92
100
|
end
|
93
101
|
end
|
@@ -213,23 +221,134 @@ describe Delayed::Worker do
|
|
213
221
|
end
|
214
222
|
end
|
215
223
|
|
216
|
-
describe '
|
217
|
-
|
218
|
-
|
219
|
-
|
224
|
+
describe 'lifecycle callbacks' do
|
225
|
+
let(:plugin) do
|
226
|
+
Class.new(Delayed::Plugin) do
|
227
|
+
class << self
|
228
|
+
attr_accessor :last_error, :raise_on
|
229
|
+
|
230
|
+
def events
|
231
|
+
@events ||= []
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
220
235
|
callbacks do |lifecycle|
|
221
|
-
lifecycle.
|
236
|
+
lifecycle.around(:thread) do |_, &blk|
|
237
|
+
events << :thread_start
|
238
|
+
blk.call
|
239
|
+
raise "oh no" if raise_on == :thread
|
240
|
+
|
241
|
+
events << :thread_end
|
242
|
+
end
|
243
|
+
|
244
|
+
%i(perform error failure).each do |event|
|
245
|
+
lifecycle.around(event) do |_, job, &blk|
|
246
|
+
events << :"#{event}_start"
|
247
|
+
raise "oh no" if raise_on == event
|
248
|
+
|
249
|
+
blk.call.tap do
|
250
|
+
self.last_error = job.last_error if event == :error
|
251
|
+
events << :"#{event}_end"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
222
255
|
end
|
223
256
|
end
|
257
|
+
end
|
258
|
+
|
259
|
+
before do
|
224
260
|
Delayed.plugins << plugin
|
261
|
+
end
|
225
262
|
|
226
|
-
|
263
|
+
it 'runs thread and perform callbacks' do
|
227
264
|
Delayed::Job.enqueue SimpleJob.new
|
228
|
-
|
265
|
+
described_class.new.work_off
|
266
|
+
|
267
|
+
expect(plugin.events).to eq %i(thread_start perform_start perform_end thread_end)
|
268
|
+
expect(plugin.last_error).to eq(nil)
|
269
|
+
expect(Delayed::Job.count).to eq 0
|
270
|
+
end
|
271
|
+
|
272
|
+
context 'when thread callback raises an error' do
|
273
|
+
before do
|
274
|
+
plugin.raise_on = :thread
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'logs that the thread crashed' do
|
278
|
+
Delayed::Job.enqueue SimpleJob.new
|
279
|
+
described_class.new.work_off
|
280
|
+
|
281
|
+
expect(plugin.events).to eq %i(thread_start perform_start perform_end)
|
282
|
+
expect(plugin.last_error).to eq(nil)
|
283
|
+
expect(Delayed::Job.count).to eq 0
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
context 'when the perform callback raises an error' do
|
288
|
+
before do
|
289
|
+
plugin.raise_on = :perform
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'runs expected perform and error callbacks' do
|
293
|
+
Delayed::Job.enqueue SimpleJob.new
|
294
|
+
described_class.new.work_off
|
295
|
+
|
296
|
+
expect(plugin.events).to eq %i(thread_start perform_start error_start error_end thread_end)
|
297
|
+
expect(plugin.last_error).to match(/oh no/) # assert that cleanup happened before `:perform_end`
|
298
|
+
expect(Delayed::Job.count).to eq 1
|
299
|
+
end
|
300
|
+
end
|
229
301
|
|
230
|
-
|
302
|
+
context 'when the perform method raises an error' do
|
303
|
+
it 'runs error callbacks' do
|
304
|
+
Delayed::Job.enqueue ErrorJob.new
|
305
|
+
described_class.new.work_off
|
231
306
|
|
232
|
-
|
307
|
+
expect(plugin.events).to eq %i(thread_start perform_start error_start error_end thread_end)
|
308
|
+
expect(plugin.last_error).to match(/did not work/) # assert that cleanup happened before `:perform_end`
|
309
|
+
expect(Delayed::Job.count).to eq 1
|
310
|
+
end
|
311
|
+
|
312
|
+
context 'when error callback raises an error' do
|
313
|
+
before do
|
314
|
+
plugin.raise_on = :error
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'runs thread and perform callbacks' do
|
318
|
+
Delayed::Job.enqueue SimpleJob.new
|
319
|
+
described_class.new.work_off
|
320
|
+
|
321
|
+
expect(plugin.events).to eq %i(thread_start perform_start perform_end thread_end)
|
322
|
+
expect(plugin.last_error).to eq(nil)
|
323
|
+
expect(Delayed::Job.count).to eq 0
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
context 'when max attempts is exceeded' do
|
329
|
+
it 'runs failure callbacks' do
|
330
|
+
Delayed::Job.enqueue FailureJob.new
|
331
|
+
described_class.new.work_off
|
332
|
+
|
333
|
+
expect(plugin.events).to eq %i(thread_start perform_start error_start failure_start failure_end error_end thread_end)
|
334
|
+
expect(plugin.last_error).to match(/did not work/) # assert that cleanup happened before `:perform_end`
|
335
|
+
expect(Delayed::Job.count).to eq 1
|
336
|
+
end
|
337
|
+
|
338
|
+
context 'when failure callback raises an error' do
|
339
|
+
before do
|
340
|
+
plugin.raise_on = :failure
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'runs thread and perform callbacks' do
|
344
|
+
Delayed::Job.enqueue SimpleJob.new
|
345
|
+
described_class.new.work_off
|
346
|
+
|
347
|
+
expect(plugin.events).to eq %i(thread_start perform_start perform_end thread_end)
|
348
|
+
expect(plugin.last_error).to eq(nil)
|
349
|
+
expect(Delayed::Job.count).to eq 0
|
350
|
+
end
|
351
|
+
end
|
233
352
|
end
|
234
353
|
end
|
235
354
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Griffith
|
@@ -16,10 +16,9 @@ authors:
|
|
16
16
|
- Matt Griffin
|
17
17
|
- Steve Richert
|
18
18
|
- Tobias Lütke
|
19
|
-
autorequire:
|
20
19
|
bindir: bin
|
21
20
|
cert_chain: []
|
22
|
-
date:
|
21
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
23
22
|
dependencies:
|
24
23
|
- !ruby/object:Gem::Dependency
|
25
24
|
name: activerecord
|
@@ -87,6 +86,7 @@ files:
|
|
87
86
|
- lib/delayed/serialization/active_record.rb
|
88
87
|
- lib/delayed/syck_ext.rb
|
89
88
|
- lib/delayed/tasks.rb
|
89
|
+
- lib/delayed/version.rb
|
90
90
|
- lib/delayed/worker.rb
|
91
91
|
- lib/delayed/yaml_ext.rb
|
92
92
|
- lib/delayed_job.rb
|
@@ -124,7 +124,6 @@ metadata:
|
|
124
124
|
bug_tracker_uri: https://github.com/betterment/delayed/issues
|
125
125
|
source_code_uri: https://github.com/betterment/delayed
|
126
126
|
rubygems_mfa_required: 'true'
|
127
|
-
post_install_message:
|
128
127
|
rdoc_options: []
|
129
128
|
require_paths:
|
130
129
|
- lib
|
@@ -139,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
138
|
- !ruby/object:Gem::Version
|
140
139
|
version: '0'
|
141
140
|
requirements: []
|
142
|
-
rubygems_version: 3.
|
143
|
-
signing_key:
|
141
|
+
rubygems_version: 3.6.8
|
144
142
|
specification_version: 4
|
145
143
|
summary: a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process
|
146
144
|
millions of background jobs per day
|