delayed 0.7.2 → 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/lib/delayed/job_wrapper.rb +18 -0
- data/lib/delayed/lifecycle.rb +1 -1
- data/lib/delayed/plugins/connection.rb +2 -2
- data/lib/delayed/priority.rb +1 -0
- data/lib/delayed/version.rb +5 -0
- data/lib/delayed/worker.rb +27 -16
- data/spec/delayed/active_job_adapter_spec.rb +14 -1
- data/spec/delayed/priority_spec.rb +7 -0
- data/spec/helper.rb +9 -4
- data/spec/sample_jobs.rb +1 -1
- data/spec/worker_spec.rb +120 -9
- metadata +4 -3
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/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
@@ -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/worker.rb
CHANGED
@@ -101,12 +101,15 @@ module Delayed
|
|
101
101
|
pool = Concurrent::FixedThreadPool.new(jobs.length)
|
102
102
|
jobs.each do |job|
|
103
103
|
pool.post do
|
104
|
-
self.class.lifecycle.run_callbacks(:thread, self
|
104
|
+
self.class.lifecycle.run_callbacks(:thread, self) do
|
105
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)
|
106
110
|
end
|
107
111
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
108
|
-
|
109
|
-
job.error = e
|
112
|
+
say "Job thread crashed with #{e.class.name}: #{e.message}", 'error'
|
110
113
|
end
|
111
114
|
end
|
112
115
|
|
@@ -145,13 +148,10 @@ module Delayed
|
|
145
148
|
end
|
146
149
|
true # did work
|
147
150
|
rescue DeserializationError => e
|
148
|
-
|
149
|
-
|
150
|
-
job.error = e
|
151
|
-
failed(job)
|
151
|
+
handle_unrecoverable_error(job, e)
|
152
152
|
false # work failed
|
153
153
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
154
|
-
|
154
|
+
handle_erroring_job(job, e)
|
155
155
|
false # work failed
|
156
156
|
end
|
157
157
|
|
@@ -172,12 +172,12 @@ module Delayed
|
|
172
172
|
def failed(job)
|
173
173
|
self.class.lifecycle.run_callbacks(:failure, self, job) do
|
174
174
|
job.hook(:failure)
|
175
|
-
rescue StandardError => e
|
176
|
-
say "Error when running failure callback: #{e}", 'error'
|
177
|
-
say e.backtrace.join("\n"), 'error'
|
178
|
-
ensure
|
179
|
-
job.destroy_failed_jobs? ? job.destroy : job.fail!
|
180
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!
|
181
181
|
end
|
182
182
|
|
183
183
|
def job_say(job, text, level = Delayed.default_log_level)
|
@@ -204,10 +204,21 @@ module Delayed
|
|
204
204
|
" (queue=#{queue})" if queue
|
205
205
|
end
|
206
206
|
|
207
|
-
def
|
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)
|
215
|
+
end
|
216
|
+
|
217
|
+
def handle_unrecoverable_error(job, error)
|
218
|
+
job_say job, "FAILED permanently with #{error.class.name}: #{error.message}", 'error'
|
219
|
+
|
208
220
|
job.error = error
|
209
|
-
|
210
|
-
reschedule(job)
|
221
|
+
failed(job)
|
211
222
|
end
|
212
223
|
|
213
224
|
# The backend adapter may return either a list or a single job
|
@@ -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
|
@@ -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/helper.rb
CHANGED
@@ -10,6 +10,12 @@ 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
|
+
|
13
19
|
if ActiveSupport.gem_version >= Gem::Version.new('7.1')
|
14
20
|
frameworks = [ActiveModel, ActiveRecord, ActionMailer, ActiveJob, ActiveSupport]
|
15
21
|
frameworks.each { |framework| framework.deprecator.behavior = :raise }
|
@@ -36,6 +42,7 @@ db_adapter ||= "sqlite3"
|
|
36
42
|
config = YAML.load(ERB.new(File.read("spec/database.yml")).result)
|
37
43
|
ActiveRecord::Base.establish_connection config[db_adapter]
|
38
44
|
ActiveRecord::Base.logger = Delayed.logger
|
45
|
+
ActiveJob::Base.logger = Delayed.logger
|
39
46
|
ActiveRecord::Migration.verbose = false
|
40
47
|
|
41
48
|
# MySQL 5.7 no longer supports null default values for the primary key
|
@@ -126,10 +133,7 @@ RSpec.configure do |config|
|
|
126
133
|
read_ahead_was = Delayed::Worker.read_ahead
|
127
134
|
sleep_delay_was = Delayed::Worker.sleep_delay
|
128
135
|
min_reserve_interval_was = Delayed::Worker.min_reserve_interval
|
129
|
-
|
130
|
-
if Gem.loaded_specs['delayed'].version >= Gem::Version.new('1.0') && min_reserve_interval_was.zero?
|
131
|
-
raise "Min reserve interval should be nonzero in v1.0 release"
|
132
|
-
end
|
136
|
+
plugins_was = Delayed.plugins.dup
|
133
137
|
|
134
138
|
Delayed::Worker.sleep_delay = TEST_SLEEP_DELAY
|
135
139
|
Delayed::Worker.min_reserve_interval = TEST_MIN_RESERVE_INTERVAL
|
@@ -151,6 +155,7 @@ RSpec.configure do |config|
|
|
151
155
|
Delayed::Worker.read_ahead = read_ahead_was
|
152
156
|
Delayed::Worker.sleep_delay = sleep_delay_was
|
153
157
|
Delayed::Worker.min_reserve_interval = min_reserve_interval_was
|
158
|
+
Delayed.plugins = plugins_was
|
154
159
|
|
155
160
|
Delayed::Job.delete_all
|
156
161
|
end
|
data/spec/sample_jobs.rb
CHANGED
data/spec/worker_spec.rb
CHANGED
@@ -221,23 +221,134 @@ describe Delayed::Worker do
|
|
221
221
|
end
|
222
222
|
end
|
223
223
|
|
224
|
-
describe '
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
+
|
228
235
|
callbacks do |lifecycle|
|
229
|
-
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
|
230
255
|
end
|
231
256
|
end
|
257
|
+
end
|
258
|
+
|
259
|
+
before do
|
232
260
|
Delayed.plugins << plugin
|
261
|
+
end
|
233
262
|
|
234
|
-
|
263
|
+
it 'runs thread and perform callbacks' do
|
235
264
|
Delayed::Job.enqueue SimpleJob.new
|
236
|
-
|
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
|
237
295
|
|
238
|
-
|
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
|
301
|
+
|
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
|
306
|
+
|
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
|
239
337
|
|
240
|
-
|
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
|
241
352
|
end
|
242
353
|
end
|
243
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
|
@@ -18,7 +18,7 @@ authors:
|
|
18
18
|
- Tobias Lütke
|
19
19
|
bindir: bin
|
20
20
|
cert_chain: []
|
21
|
-
date:
|
21
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|
24
24
|
name: activerecord
|
@@ -86,6 +86,7 @@ files:
|
|
86
86
|
- lib/delayed/serialization/active_record.rb
|
87
87
|
- lib/delayed/syck_ext.rb
|
88
88
|
- lib/delayed/tasks.rb
|
89
|
+
- lib/delayed/version.rb
|
89
90
|
- lib/delayed/worker.rb
|
90
91
|
- lib/delayed/yaml_ext.rb
|
91
92
|
- lib/delayed_job.rb
|
@@ -137,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
138
|
- !ruby/object:Gem::Version
|
138
139
|
version: '0'
|
139
140
|
requirements: []
|
140
|
-
rubygems_version: 3.6.
|
141
|
+
rubygems_version: 3.6.8
|
141
142
|
specification_version: 4
|
142
143
|
summary: a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process
|
143
144
|
millions of background jobs per day
|