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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea752ae23dd7aee646bcd0be946a7e414c328c4323dc4a2f34d0ee2106c36364
4
- data.tar.gz: 6b2509d426b6e7dd25340fde6a3ff4ccfea88fbf15d53d91a1724629ff21c170
3
+ metadata.gz: 9e2ec868563930c9d66a6db2eb638559b20497de5a819d543694c8fa4814519e
4
+ data.tar.gz: b48f6b5951f5c7309ea8db1b693451d4501c6f87852b843e5b1c49a5113cd4ed
5
5
  SHA512:
6
- metadata.gz: 015dd62d37946fd70411776f89a6d96850b76a1f3d54a280d38ff82d816b0ab0532b673309ace4882575b443a9ce1244e8a053a82d25413d2217213d3c72284f
7
- data.tar.gz: 5dcd12088fb0bbe76abe2559a35aa53a6b90155fe22dd992033b31f12f63b0d572ec8ba2b87d2ad418837dc25ebaeecd1a558108e8cf74f08081c11435ddcafb
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
@@ -140,6 +140,7 @@ module Delayed
140
140
  attr_reader :value
141
141
 
142
142
  delegate :to_i, to: :value
143
+ delegate :to_f, to: :value
143
144
  delegate :to_s, to: :name
144
145
 
145
146
  def initialize(value)
@@ -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
 
@@ -145,13 +148,10 @@ module Delayed
145
148
  end
146
149
  true # did work
147
150
  rescue DeserializationError => e
148
- job_say job, "FAILED permanently with #{e.class.name}: #{e.message}", 'error'
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
- self.class.lifecycle.run_callbacks(:error, self, job) { handle_failed_job(job, e) }
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 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
@@ -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
@@ -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,23 +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
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.before(:thread) { performances.increment }
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
- Delayed::Job.delete_all
263
+ it 'runs thread and perform callbacks' do
235
264
  Delayed::Job.enqueue SimpleJob.new
236
- worker = described_class.new
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
- worker.work_off
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
- expect(performances.value).to eq(1)
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.7.2
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-02 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