inst-jobs 1.0.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d65c567b52e2baf26955d266c9fededc001deab65d871e6e60a42a905d552e8
4
- data.tar.gz: b4d487088aba583188c90650a42a20726f84230e346c321b4b94ddc5c25fe36e
3
+ metadata.gz: 67a4882eeb37fac31a1fd646e0a5a414a3292d7379c243d02b0ede8f9f5f5d89
4
+ data.tar.gz: 5e92f514ea3a561f8e55541a5a7a8b5802909d3ddaf1ea9ec217d2c3c0a6eff9
5
5
  SHA512:
6
- metadata.gz: 9ade674610c5a1a4a04e4a6078b45c38358e7c7af79a417d5e6941d968e1565bc5e217405ad1ca427b373ee0d89bdbc28772f8ed52047394e8760fc09a79b60a
7
- data.tar.gz: 216706e128397e84ba87d495e4b09d30eb58f35ab4821e8ef6f484d5f6226a959cab04a8bfdfa22916f19dad53e7ee01bbe1128bd61fb402aad29885ac81839f
6
+ metadata.gz: d8183e8becaf288a52dec2757b16632fe57c0c05d2f98c3edd9bb13cf732744498c309b0edf6598006fd0914214ae6cf098b965ad64887763524c23616d2ea74
7
+ data.tar.gz: 81650de01fdd02fbb4fcf81b112dc5cfefd4291a439ac8c5aab5faf424c5780b57845c3fe8f69992ed419638289ef0b4f5cab0e6a830d34ba04af160c21139e3
@@ -178,6 +178,10 @@ module Delayed
178
178
  expires_at && (self.class.db_time_now >= expires_at)
179
179
  end
180
180
 
181
+ def inferred_max_attempts
182
+ self.max_attempts || Delayed::Settings.max_attempts
183
+ end
184
+
181
185
  # Reschedule the job in the future (when a job fails).
182
186
  # Uses an exponential scale depending on the number of failed attempts.
183
187
  def reschedule(error = nil, time = nil)
@@ -190,7 +194,7 @@ module Delayed
190
194
 
191
195
  self.attempts += 1 unless return_code == :unlock
192
196
 
193
- if self.attempts >= (self.max_attempts || Delayed::Settings.max_attempts)
197
+ if self.attempts >= self.inferred_max_attempts
194
198
  permanent_failure error || "max attempts reached"
195
199
  elsif expired?
196
200
  permanent_failure error || "job has expired"
@@ -12,6 +12,7 @@ module Delayed
12
12
  :loop => [:worker],
13
13
  :perform => [:worker, :job],
14
14
  :pop => [:worker],
15
+ :retry => [:worker, :job, :exception],
15
16
  :work_queue_pop => [:work_queue, :worker_config],
16
17
  :check_for_work => [:work_queue],
17
18
  }
@@ -13,7 +13,7 @@ module Delayed
13
13
  self.permanent_fail_cb = on_permanent_failure
14
14
  self.sender = sender
15
15
  begin
16
- YAML.dump(sender)
16
+ YAML.load(YAML.dump(sender))
17
17
  rescue
18
18
  # if for some reason you can't dump the sender, just drop it
19
19
  self.sender = nil
@@ -73,7 +73,8 @@ module Delayed
73
73
 
74
74
  def full_name
75
75
  obj_name = object.is_a?(ActiveRecord::Base) ? "#{object.class}.find(#{object.id}).#{method}" : display_name
76
- kwargs_str = kwargs.map { |(k, v)| ", #{k}: #{deep_de_ar_ize(v)}"}.join("")
76
+ kgs = kwargs || {}
77
+ kwargs_str = kgs.map { |(k, v)| ", #{k}: #{deep_de_ar_ize(v)}"}.join("")
77
78
  "#{obj_name}(#{args.map { |a| deep_de_ar_ize(a) }.join(', ')}#{kwargs_str})"
78
79
  end
79
80
  end
@@ -49,10 +49,20 @@ class Periodic
49
49
  end
50
50
 
51
51
  def enqueue
52
- Delayed::Job.enqueue(self, **@job_args.merge(:max_attempts => 1,
53
- :run_at => @cron.next_time(Delayed::Periodic.now).utc.to_time,
54
- :singleton => tag,
55
- on_conflict: :patient))
52
+ Delayed::Job.enqueue(self, **enqueue_args)
53
+ end
54
+
55
+ def enqueue_args
56
+ inferred_args = {
57
+ max_attempts: 1,
58
+ run_at: @cron.next_time(Delayed::Periodic.now).utc.to_time,
59
+ singleton: (@job_args[:singleton] == false ? nil : tag),
60
+ # yes, checking for whether it is actually the boolean literal false,
61
+ # which means the consuming code really does not want this job to be
62
+ # a singleton at all.
63
+ on_conflict: :patient
64
+ }
65
+ @job_args.merge(inferred_args)
56
66
  end
57
67
 
58
68
  def perform
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "1.0.2"
4
+ VERSION = "2.1.1"
5
5
  end
@@ -3,6 +3,17 @@
3
3
  module Delayed
4
4
 
5
5
  class TimeoutError < RuntimeError; end
6
+ class RetriableError < RuntimeError
7
+ # this error is a special case. You _should_ raise
8
+ # it from inside the rescue block for another error,
9
+ # because it indicates: "something made this job fail
10
+ # but we're pretty sure it's transient and it's safe to try again".
11
+ # the workflow is still the same (retry will happen unless
12
+ # retries are exhausted), but it won't call the :error
13
+ # callback unless it can't retry anymore. It WILL call the
14
+ # separate ":retry" callback, which is ONLY activated
15
+ # for this kind of error.
16
+ end
6
17
 
7
18
  require 'tmpdir'
8
19
  require 'set'
@@ -198,32 +209,38 @@ class Worker
198
209
  end
199
210
 
200
211
  def perform(job)
201
- count = 1
202
- raise Delayed::Backend::JobExpired, "job expired at #{job.expires_at}" if job.expired?
203
- self.class.lifecycle.run_callbacks(:perform, self, job) do
204
- set_process_name("run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}")
205
- logger.info("Processing #{log_job(job, :long)}")
206
- runtime = Benchmark.realtime do
207
- if job.batch?
208
- # each job in the batch will have perform called on it, so we don't
209
- # need a timeout around this
210
- count = perform_batch(job)
211
- else
212
- job.invoke_job
212
+ begin
213
+ count = 1
214
+ raise Delayed::Backend::JobExpired, "job expired at #{job.expires_at}" if job.expired?
215
+ self.class.lifecycle.run_callbacks(:perform, self, job) do
216
+ set_process_name("run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}")
217
+ logger.info("Processing #{log_job(job, :long)}")
218
+ runtime = Benchmark.realtime do
219
+ if job.batch?
220
+ # each job in the batch will have perform called on it, so we don't
221
+ # need a timeout around this
222
+ count = perform_batch(job)
223
+ else
224
+ job.invoke_job
225
+ end
226
+ job.destroy
213
227
  end
214
- job.destroy
228
+ logger.info("Completed #{log_job(job)} #{"%.0fms" % (runtime * 1000)}")
229
+ end
230
+ rescue ::Delayed::RetriableError => re
231
+ can_retry = job.attempts + 1 < job.inferred_max_attempts
232
+ callback_type = can_retry ? :retry : :error
233
+ self.class.lifecycle.run_callbacks(callback_type, self, job, re) do
234
+ handle_failed_job(job, re)
235
+ end
236
+ rescue SystemExit => se
237
+ # There wasn't really a failure here so no callbacks and whatnot needed,
238
+ # still reschedule the job though.
239
+ job.reschedule(se)
240
+ rescue Exception => e
241
+ self.class.lifecycle.run_callbacks(:error, self, job, e) do
242
+ handle_failed_job(job, e)
215
243
  end
216
- logger.info("Completed #{log_job(job)} #{"%.0fms" % (runtime * 1000)}")
217
- end
218
- count
219
- rescue SystemExit => se
220
- # There wasn't really a failure here so no callbacks and whatnot needed,
221
- # still reschedule the job though.
222
- job.reschedule(se)
223
- count
224
- rescue Exception => e
225
- self.class.lifecycle.run_callbacks(:error, self, job, e) do
226
- handle_failed_job(job, e)
227
244
  end
228
245
  count
229
246
  end
@@ -22,31 +22,46 @@ module Delayed
22
22
 
23
23
  def reschedule_abandoned_jobs
24
24
  return if Settings.worker_health_check_type == :none
25
+ Delayed::Job.transaction do
26
+ # this job is a special case, and is not a singleton
27
+ # because if it gets wiped out suddenly during execution
28
+ # it can't go clean up it's abandoned self. Therefore,
29
+ # we try to get an advisory lock when it runs. If we succeed,
30
+ # no other job is trying to do this right now (and if we abandon the
31
+ # job, the transaction will end, releasing the advisory lock).
32
+ result = attempt_advisory_lock
33
+ return unless result
34
+ checker = Worker::HealthCheck.build(
35
+ type: Settings.worker_health_check_type,
36
+ config: Settings.worker_health_check_config,
37
+ worker_name: 'cleanup-crew'
38
+ )
39
+ live_workers = checker.live_workers
25
40
 
26
- checker = Worker::HealthCheck.build(
27
- type: Settings.worker_health_check_type,
28
- config: Settings.worker_health_check_config,
29
- worker_name: 'cleanup-crew'
30
- )
31
- live_workers = checker.live_workers
32
-
33
- Delayed::Job.running_jobs.each do |job|
34
- # prefetched jobs have their own way of automatically unlocking themselves
35
- next if job.locked_by.start_with?("prefetch:")
36
- unless live_workers.include?(job.locked_by)
37
- begin
38
- Delayed::Job.transaction do
39
- # double check that the job is still there. locked_by will immediately be reset
40
- # to nil in this transaction by Job#reschedule
41
- next unless Delayed::Job.where(id: job, locked_by: job.locked_by).update_all(locked_by: "abandoned job cleanup") == 1
42
- job.reschedule
41
+ Delayed::Job.running_jobs.each do |job|
42
+ # prefetched jobs have their own way of automatically unlocking themselves
43
+ next if job.locked_by.start_with?("prefetch:")
44
+ unless live_workers.include?(job.locked_by)
45
+ begin
46
+ Delayed::Job.transaction do
47
+ # double check that the job is still there. locked_by will immediately be reset
48
+ # to nil in this transaction by Job#reschedule
49
+ next unless Delayed::Job.where(id: job, locked_by: job.locked_by).update_all(locked_by: "abandoned job cleanup") == 1
50
+ job.reschedule
51
+ end
52
+ rescue
53
+ ::Rails.logger.error "Failure rescheduling abandoned job #{job.id} #{$!.inspect}"
43
54
  end
44
- rescue
45
- ::Rails.logger.error "Failure rescheduling abandoned job #{job.id} #{$!.inspect}"
46
55
  end
47
56
  end
48
57
  end
49
58
  end
59
+
60
+ def attempt_advisory_lock
61
+ lock_name = "Delayed::Worker::HealthCheck#reschedule_abandoned_jobs"
62
+ output = ActiveRecord::Base.connection.execute("SELECT pg_try_advisory_xact_lock(half_md5_as_bigint('#{lock_name}'));")
63
+ output.getvalue(0, 0)
64
+ end
50
65
  end
51
66
 
52
67
  attr_accessor :config, :worker_name
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Delayed::Periodic do
6
+ around(:each) do |block|
7
+ # make sure we can use ".cron" and
8
+ # such safely without leaking global state
9
+ prev_sched = Delayed::Periodic.scheduled
10
+ prev_ovr = Delayed::Periodic.overrides
11
+ Delayed::Periodic.scheduled = {}
12
+ Delayed::Periodic.overrides = {}
13
+ block.call
14
+ ensure
15
+ Delayed::Periodic.scheduled = prev_sched
16
+ Delayed::Periodic.overrides = prev_ovr
17
+ end
18
+
19
+ describe ".cron" do
20
+ let(:job_name){ 'just a test'}
21
+ it "provides a tag by default for periodic jobs" do
22
+ Delayed::Periodic.cron job_name, '*/10 * * * *' do
23
+ # no-op
24
+ end
25
+ instance = Delayed::Periodic.scheduled[job_name]
26
+ expect(instance).to_not be_nil
27
+ expect(instance.enqueue_args[:singleton]).to eq("periodic: just a test")
28
+ end
29
+
30
+ it "uses no singleton if told to skip" do
31
+ Delayed::Periodic.cron job_name, '*/10 * * * *', {singleton: false} do
32
+ # no-op
33
+ end
34
+ instance = Delayed::Periodic.scheduled[job_name]
35
+ expect(instance).to_not be_nil
36
+ expect(instance.enqueue_args[:singleton]).to be_nil
37
+ end
38
+ end
39
+ end
@@ -107,6 +107,15 @@ RSpec.describe Delayed::Worker::HealthCheck do
107
107
  @dead_job.reload
108
108
  expect(@dead_job.locked_by).to eq 'prefetch:some_node'
109
109
  end
110
+
111
+ it "bails immediately if advisory lock already taken" do
112
+ allow(Delayed::Worker::HealthCheck).to receive(:attempt_advisory_lock).and_return(false)
113
+ Delayed::Worker::HealthCheck.reschedule_abandoned_jobs
114
+ @dead_job.reload
115
+ expect(@dead_job.run_at.to_i).to eq(initial_run_at.to_i)
116
+ expect(@dead_job.locked_at).to_not be_nil
117
+ expect(@dead_job.locked_by).to_not be_nil
118
+ end
110
119
  end
111
120
 
112
121
  describe '#initialize' do
@@ -6,6 +6,11 @@ describe Delayed::Worker do
6
6
  let(:worker_config) { {
7
7
  queue: "test", min_priority: 1, max_priority: 2, stuff: "stuff",
8
8
  }.freeze }
9
+ let(:job_attrs) { {
10
+ id: 42, name: "testjob", full_name: "testfullname", :last_error= => nil,
11
+ attempts: 1, reschedule: nil, :expired? => false,
12
+ payload_object: {}, priority: 25
13
+ }.freeze }
9
14
  subject { described_class.new(worker_config.dup) }
10
15
 
11
16
  after { Delayed::Worker.lifecycle.reset! }
@@ -14,9 +19,24 @@ describe Delayed::Worker do
14
19
  it "fires off an error callback when a job raises an exception" do
15
20
  fired = false
16
21
  Delayed::Worker.lifecycle.before(:error) {|worker, exception| fired = true}
17
- job = double(:last_error= => nil, attempts: 1, reschedule: nil)
18
- subject.perform(job)
22
+ job = double(job_attrs)
23
+ output_count = subject.perform(job)
19
24
  expect(fired).to be_truthy
25
+ expect(output_count).to eq(1)
26
+ end
27
+
28
+ it "uses the retry callback for a retriable exception" do
29
+ error_fired = retry_fired = false
30
+ Delayed::Worker.lifecycle.before(:error) {|worker, exception| error_fired = true }
31
+ Delayed::Worker.lifecycle.before(:retry) {|worker, exception| retry_fired = true}
32
+ job = Delayed::Job.new(payload_object: {}, priority: 25, strand: "test_jobs", max_attempts: 3)
33
+ expect(job).to receive(:invoke_job) do
34
+ raise Delayed::RetriableError, "that's all this job does"
35
+ end
36
+ output_count = subject.perform(job)
37
+ expect(error_fired).to be_falsey
38
+ expect(retry_fired).to be_truthy
39
+ expect(output_count).to eq(1)
20
40
  end
21
41
 
22
42
  it "reloads" do
@@ -35,7 +55,7 @@ describe Delayed::Worker do
35
55
  expect(ActionDispatch::Reloader).to receive(:prepare!).once
36
56
  expect(ActionDispatch::Reloader).to receive(:cleanup!).once
37
57
  end
38
- job = double(:last_error= => nil, attempts: 0, reschedule: nil, expired?: false)
58
+ job = double(job_attrs)
39
59
  subject.perform(job)
40
60
  end
41
61
  end
@@ -65,4 +65,10 @@ shared_examples_for 'Delayed::PerformableMethod' do
65
65
  p.send(:on_permanent_failure, 'fail_frd')
66
66
  story.text.should == 'fail_frd'
67
67
  end
68
+
69
+ it "can still generate a name with no kwargs" do
70
+ story = Story.create :text => 'wat'
71
+ p = Delayed::PerformableMethod.new(story, :tell, kwargs: nil)
72
+ expect(p.full_name).to eq("Story.find(#{story.id}).tell()")
73
+ end
68
74
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Luetke
8
8
  - Brian Palmer
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-10-30 00:00:00.000000000 Z
12
+ date: 2020-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -325,7 +325,7 @@ dependencies:
325
325
  - - "~>"
326
326
  - !ruby/object:Gem::Version
327
327
  version: 1.4.0
328
- description:
328
+ description:
329
329
  email:
330
330
  - brianp@instructure.com
331
331
  executables:
@@ -412,6 +412,7 @@ files:
412
412
  - spec/delayed/cli_spec.rb
413
413
  - spec/delayed/daemon_spec.rb
414
414
  - spec/delayed/message_sending_spec.rb
415
+ - spec/delayed/periodic_spec.rb
415
416
  - spec/delayed/server_spec.rb
416
417
  - spec/delayed/settings_spec.rb
417
418
  - spec/delayed/work_queue/in_process_spec.rb
@@ -422,15 +423,10 @@ files:
422
423
  - spec/delayed/worker/health_check_spec.rb
423
424
  - spec/delayed/worker_spec.rb
424
425
  - spec/gemfiles/42.gemfile
425
- - spec/gemfiles/42.gemfile.lock
426
426
  - spec/gemfiles/50.gemfile
427
- - spec/gemfiles/50.gemfile.lock
428
427
  - spec/gemfiles/51.gemfile
429
- - spec/gemfiles/51.gemfile.lock
430
428
  - spec/gemfiles/52.gemfile
431
- - spec/gemfiles/52.gemfile.lock
432
429
  - spec/gemfiles/60.gemfile
433
- - spec/gemfiles/60.gemfile.lock
434
430
  - spec/migrate/20140924140513_add_story_table.rb
435
431
  - spec/redis_job_spec.rb
436
432
  - spec/sample_jobs.rb
@@ -445,7 +441,7 @@ files:
445
441
  homepage: https://github.com/instructure/inst-jobs
446
442
  licenses: []
447
443
  metadata: {}
448
- post_install_message:
444
+ post_install_message:
449
445
  rdoc_options: []
450
446
  require_paths:
451
447
  - lib
@@ -453,30 +449,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
453
449
  requirements:
454
450
  - - ">="
455
451
  - !ruby/object:Gem::Version
456
- version: '2.3'
452
+ version: '2.6'
457
453
  required_rubygems_version: !ruby/object:Gem::Requirement
458
454
  requirements:
459
455
  - - ">="
460
456
  - !ruby/object:Gem::Version
461
457
  version: '0'
462
458
  requirements: []
463
- rubygems_version: 3.1.4
464
- signing_key:
459
+ rubygems_version: 3.0.3
460
+ signing_key:
465
461
  specification_version: 4
466
462
  summary: Instructure-maintained fork of delayed_job
467
463
  test_files:
468
464
  - spec/sample_jobs.rb
469
465
  - spec/spec_helper.rb
470
466
  - spec/redis_job_spec.rb
471
- - spec/gemfiles/51.gemfile.lock
472
- - spec/gemfiles/60.gemfile.lock
473
- - spec/gemfiles/42.gemfile.lock
474
- - spec/gemfiles/50.gemfile.lock
475
467
  - spec/gemfiles/60.gemfile
476
468
  - spec/gemfiles/42.gemfile
477
469
  - spec/gemfiles/52.gemfile
478
470
  - spec/gemfiles/50.gemfile
479
- - spec/gemfiles/52.gemfile.lock
480
471
  - spec/gemfiles/51.gemfile
481
472
  - spec/shared_jobs_specs.rb
482
473
  - spec/shared/performable_method.rb
@@ -490,6 +481,7 @@ test_files:
490
481
  - spec/delayed/cli_spec.rb
491
482
  - spec/delayed/daemon_spec.rb
492
483
  - spec/delayed/worker_spec.rb
484
+ - spec/delayed/periodic_spec.rb
493
485
  - spec/delayed/message_sending_spec.rb
494
486
  - spec/delayed/settings_spec.rb
495
487
  - spec/delayed/work_queue/in_process_spec.rb