delayed 0.8.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6268c514bb90e5279b8265c4ce21eea8262ac4d539ff161c7f9eab12f15f17ba
4
- data.tar.gz: eb0c1f81475163766a680eab603740a4632956fb415e4158430a67bb3998c381
3
+ metadata.gz: 9e2ec868563930c9d66a6db2eb638559b20497de5a819d543694c8fa4814519e
4
+ data.tar.gz: b48f6b5951f5c7309ea8db1b693451d4501c6f87852b843e5b1c49a5113cd4ed
5
5
  SHA512:
6
- metadata.gz: 73686b95d83b336dcf268f4524fb8b512e64f61b58773b0884e8f1a4c30abc89532aaa0df4e1395d69417d2062ae7e948157c7cd5b3b7cb80982c68dbf798936
7
- data.tar.gz: 9f3b7437e5772e0fe44b3f168cd32b78073b6c49b2fc208b713d946d4b48f14549646363856e3d62a7525059a7d841fbe0191a520af3dc06a71e3d258679b68e
6
+ metadata.gz: 17289bfb6ea1de0202570e44351e023a9024395bd2df12121b848e9813fc6eebd57c71abf709c61f7625697fddb097dc22d8bc1c2e0b714996b9e0cbdac7ceee
7
+ data.tar.gz: 76b59feb415bf66841659c05aeb421a69d59579161004da33b11de4937fe7e30c232da6bab17008c12489cf65186d8c316b371f5c41deb710e5534796b95ba20
@@ -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
@@ -8,7 +8,7 @@ module Delayed
8
8
  perform: %i(worker job),
9
9
  error: %i(worker job),
10
10
  failure: %i(worker job),
11
- thread: %i(worker job),
11
+ thread: [:worker],
12
12
  invoke_job: [:job],
13
13
  }.freeze
14
14
 
@@ -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, job, &block|
5
+ lifecycle.around(:thread) do |worker, &block|
6
6
  Job.connection_pool.with_connection do
7
- block.call(worker, job)
7
+ block.call(worker)
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delayed
4
+ VERSION = '1.0.0'
5
+ end
@@ -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, job) do
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
- job_say job, "Job thread crashed with #{e.class.name}: #{e.message}", 'error'
109
- job.error = e
112
+ say "Job thread crashed with #{e.class.name}: #{e.message}", 'error'
110
113
  end
111
114
  end
112
115
 
@@ -142,17 +145,14 @@ module Delayed
142
145
  job.destroy
143
146
  end
144
147
  job_say job, format('COMPLETED after %.4f seconds', run_time)
145
- true # did work
146
- rescue DeserializationError => e
147
- job_say job, "FAILED permanently with #{e.class.name}: #{e.message}", 'error'
148
-
149
- job.error = e
150
- failed(job)
151
- false # work failed
152
- rescue Exception => e # rubocop:disable Lint/RescueException
153
- self.class.lifecycle.run_callbacks(:error, self, job) { handle_failed_job(job, e) }
154
- false # work failed
155
148
  end
149
+ true # did work
150
+ rescue DeserializationError => e
151
+ handle_unrecoverable_error(job, e)
152
+ false # work failed
153
+ rescue Exception => e # rubocop:disable Lint/RescueException
154
+ handle_erroring_job(job, e)
155
+ false # work failed
156
156
  end
157
157
 
158
158
  # Reschedule the job in the future (when a job fails).
@@ -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 handle_failed_job(job, error)
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
- job_say job, "FAILED (#{job.attempts} prior attempts) with #{error.class.name}: #{error.message}", 'error'
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 = dj.handler.gsub('JobClass', 'MissingJobClass')
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
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
@@ -113,5 +113,5 @@ class EnqueueJobMod < SimpleJob
113
113
  end
114
114
 
115
115
  class ActiveJobJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
116
- def perform; end
116
+ def perform(*args, **kwargs); end
117
117
  end
data/spec/worker_spec.rb CHANGED
@@ -221,53 +221,134 @@ describe Delayed::Worker do
221
221
  end
222
222
  end
223
223
 
224
- describe 'thread callback' do
225
- it 'wraps code after thread is checked out' do
226
- performances = Concurrent::AtomicFixnum.new(0)
227
- plugin = Class.new(Delayed::Plugin) do
228
- callbacks do |lifecycle|
229
- lifecycle.before(:thread) { performances.increment }
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
230
233
  end
231
- end
232
- Delayed.plugins << plugin
233
234
 
234
- Delayed::Job.delete_all
235
- Delayed::Job.enqueue SimpleJob.new
236
- worker = described_class.new
237
-
238
- worker.work_off
239
-
240
- expect(performances.value).to eq(1)
241
- end
242
-
243
- it 'wraps perform and cleanup, even when perform raises' do
244
- events = []
245
- last_error = nil
246
-
247
- plugin = Class.new(Delayed::Plugin) do
248
235
  callbacks do |lifecycle|
249
236
  lifecycle.around(:thread) do |_, &blk|
250
237
  events << :thread_start
251
238
  blk.call
239
+ raise "oh no" if raise_on == :thread
240
+
252
241
  events << :thread_end
253
242
  end
254
- lifecycle.around(:perform) do |_, job, &blk|
255
- events << :perform_start
256
- blk.call.tap do
257
- last_error = job.last_error
258
- events << :perform_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
259
253
  end
260
254
  end
261
255
  end
262
256
  end
257
+ end
263
258
 
259
+ before do
264
260
  Delayed.plugins << plugin
261
+ end
265
262
 
266
- Delayed::Job.enqueue ErrorJob.new
263
+ it 'runs thread and perform callbacks' do
264
+ Delayed::Job.enqueue SimpleJob.new
267
265
  described_class.new.work_off
268
266
 
269
- expect(events).to eq %i(thread_start perform_start perform_end thread_end)
270
- expect(last_error).to match(/did not work/) # assert that cleanup happened before `:perform_end`
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
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
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
271
352
  end
272
353
  end
273
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.8.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: 2025-04-03 00:00:00.000000000 Z
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.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