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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae36c9e652d4fb6ce28f8897b0e7b03b7777ce9907a74973924111b735bca3ae
4
- data.tar.gz: 0d4c27d0bf1f7e256fa85914086a0711c6406b4be5e88f211eccbd921c001718
3
+ metadata.gz: 9e2ec868563930c9d66a6db2eb638559b20497de5a819d543694c8fa4814519e
4
+ data.tar.gz: b48f6b5951f5c7309ea8db1b693451d4501c6f87852b843e5b1c49a5113cd4ed
5
5
  SHA512:
6
- metadata.gz: cbae79e450983c1dd968b5f9959b4497758c32cc2c21a45ecea5b41dc1d7829fac2699d0a8cea635369d276eaec5b07ce8d7fa292674578ff62690823bc163fc
7
- data.tar.gz: df37746eee87f40792499e02421f82a2fc4afa4317e168bdb440f3bed1ba737c761469092098f99e2aa3fb163f50981e6b19a83a4859e91871ac8f046363f630
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 job.class.respond_to?(:enqueue_after_transaction_commit) && job.class.enqueue_after_transaction_commit == :always
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
@@ -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
 
@@ -1,7 +1,8 @@
1
- require 'active_support/proxy_object'
2
-
3
1
  module Delayed
4
- class DelayProxy < ActiveSupport::ProxyObject
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, 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)
@@ -21,7 +21,7 @@ module Delayed
21
21
  def on_exit!; end
22
22
 
23
23
  def interruptable_sleep(seconds)
24
- pipe[0].wait_readable(seconds)
24
+ pipe[0].wait_readable(seconds) if seconds.positive?
25
25
  end
26
26
 
27
27
  def stop
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delayed
4
+ VERSION = '1.0.0'
5
+ end
@@ -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
- success.increment if run_job(job)
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 run_thread_callbacks(job, &block)
116
- self.class.lifecycle.run_callbacks(:thread, self, job, &block)
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
- job_say job, "FAILED permanently with #{e.class.name}: #{e.message}", 'error'
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
- self.class.lifecycle.run_callbacks(:error, self, job) { handle_failed_job(job, e) }
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 handle_failed_job(job, error)
202
- job.error = error
203
- job_say job, "FAILED (#{job.attempts} prior attempts) with #{error.class.name}: #{error.message}", 'error'
204
- reschedule(job)
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 run_job(job)
208
- self.class.lifecycle.run_callbacks(:perform, self, job) { run(job) }
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
@@ -42,8 +42,8 @@ module Delayed
42
42
  mattr_accessor(:default_log_level) { 'info'.freeze }
43
43
  mattr_accessor(:plugins) do
44
44
  [
45
- Delayed::Plugins::Instrumentation,
46
45
  Delayed::Plugins::Connection,
46
+ Delayed::Plugins::Instrumentation,
47
47
  ]
48
48
  end
49
49
 
@@ -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::VERSION::MAJOR >= 6),
52
+ (" exception_executions: {}\n" if ActiveJob.gem_version >= Gem::Version.new('6.0')),
53
53
  " locale: en\n",
54
- (/ timezone: ?\n/ if ActiveJob::VERSION::MAJOR >= 6),
55
- (/ enqueued_at: '2023-01-20T18:52:29(\.\d+)?Z'\n/ if ActiveJob::VERSION::MAJOR >= 6),
56
- (/ scheduled_at: ?\n/ if ActiveJob::VERSION::MAJOR >= 7 && ActiveJob::VERSION::MINOR >= 1),
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 = 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
@@ -295,7 +308,9 @@ RSpec.describe Delayed::ActiveJobAdapter do
295
308
  end
296
309
 
297
310
  it 'raises an exception on enqueue' do
298
- expect { JobClass.perform_later }.to raise_error(Delayed::ActiveJobAdapter::UnsafeEnqueueError)
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
- expect { JobClass.perform_later }.not_to raise_error(Delayed::ActiveJobAdapter::UnsafeEnqueueError)
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
@@ -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.run(job)
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.run(@job)
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.run(job)
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.run(@job) }.not_to raise_error
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
@@ -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(5).to(1)
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(5).to(1)
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
@@ -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
@@ -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
- context 'when there are no jobs' do
36
- before do
37
- allow(Delayed::Job).to receive(:reserve).and_return([])
38
- end
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
- it 'does not log and then sleeps' do
41
- subject.run!
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
- context 'when there is a job worked off' do
48
- around do |example|
49
- max_claims_was = described_class.max_claims
50
- described_class.max_claims = max_claims
51
- example.run
52
- ensure
53
- described_class.max_claims = max_claims_was
54
- end
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
- before do
57
- allow(Delayed::Job).to receive(:reserve).and_return([job], [])
58
- end
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
- let(:max_claims) { 1 }
61
- let(:job) do
62
- instance_double(
63
- Delayed::Job,
64
- id: 123,
65
- max_run_time: 10,
66
- name: 'MyJob',
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 does not sleep' do
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(/1 jobs processed/)
80
- expect(subject).not_to have_received(:interruptable_sleep)
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
- context 'when max_claims is 2' do
84
- let(:max_claims) { 2 }
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
- it 'logs the count and sleeps' do
87
- subject.run!
88
- expect(Delayed.logger).to have_received(:info).with(/1 jobs processed/)
89
- expect(subject).to have_received(:interruptable_sleep)
90
- end
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 'thread callback' do
217
- it 'wraps code after thread is checked out' do
218
- performances = Concurrent::AtomicFixnum.new(0)
219
- 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
+
220
235
  callbacks do |lifecycle|
221
- 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
222
255
  end
223
256
  end
257
+ end
258
+
259
+ before do
224
260
  Delayed.plugins << plugin
261
+ end
225
262
 
226
- Delayed::Job.delete_all
263
+ it 'runs thread and perform callbacks' do
227
264
  Delayed::Job.enqueue SimpleJob.new
228
- 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
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
- worker.work_off
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
- expect(performances.value).to eq(1)
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.5.5
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: 2024-08-13 00:00:00.000000000 Z
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.3.26
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