inst-jobs 1.0.1 → 2.1.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/backend/base.rb +5 -1
- data/lib/delayed/lifecycle.rb +1 -0
- data/lib/delayed/message_sending.rb +36 -27
- data/lib/delayed/performable_method.rb +20 -9
- data/lib/delayed/periodic.rb +14 -4
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/worker.rb +17 -0
- data/lib/delayed/worker/health_check.rb +34 -19
- data/spec/delayed/message_sending_spec.rb +53 -3
- data/spec/delayed/periodic_spec.rb +39 -0
- data/spec/delayed/worker/health_check_spec.rb +9 -0
- data/spec/delayed/worker_spec.rb +13 -0
- data/spec/shared/delayed_method.rb +2 -2
- data/spec/shared/performable_method.rb +6 -0
- data/spec/spec_helper.rb +9 -0
- metadata +10 -18
- data/spec/gemfiles/42.gemfile.lock +0 -192
- data/spec/gemfiles/50.gemfile.lock +0 -197
- data/spec/gemfiles/51.gemfile.lock +0 -198
- data/spec/gemfiles/52.gemfile.lock +0 -206
- data/spec/gemfiles/60.gemfile.lock +0 -224
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b789a390da84dd32ede51b6d8655853e3f20efa55144fe19cf0605d9e5083e62
|
4
|
+
data.tar.gz: bd263366443698e93572f83a6f0e2b3d747c9e72acd851fbc0aa3f920c413e02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b902bbdb3d4d676b984a22c2df5d4382960219de7a9a3d639df4fd6f958816ac77a3e2e00e881d7afbc240fd6043eef21f5362885301ccf1c9fb1ae790a21e2f
|
7
|
+
data.tar.gz: 3dbb3fe818c20be76f6f07f61893014bc881a2f82a7c44139e62642230ff49a2acd0d3e2ba746b4bcdf1834fed0b5cb1ebcbb6ea3431b9fe46ffc29d6956200b
|
data/lib/delayed/backend/base.rb
CHANGED
@@ -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 >=
|
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"
|
data/lib/delayed/lifecycle.rb
CHANGED
@@ -7,32 +7,45 @@ end
|
|
7
7
|
module Delayed
|
8
8
|
module MessageSending
|
9
9
|
class DelayProxy < BasicObject
|
10
|
-
def initialize(object, synchronous: false,
|
10
|
+
def initialize(object, synchronous: false, sender: nil, **enqueue_args)
|
11
11
|
@object = object
|
12
12
|
@enqueue_args = enqueue_args
|
13
13
|
@synchronous = synchronous
|
14
|
-
@
|
14
|
+
@sender = sender
|
15
15
|
end
|
16
16
|
|
17
17
|
def method_missing(method, *args, **kwargs)
|
18
|
+
# method doesn't exist? must be method_missing; assume private access
|
19
|
+
@sender = nil if !@sender.nil? &&
|
20
|
+
!@object.methods.include?(method) &&
|
21
|
+
!@object.protected_methods.include?(method) &&
|
22
|
+
!@object.private_methods.include?(method)
|
23
|
+
|
24
|
+
sender_is_object = @sender == @object
|
25
|
+
sender_is_class = @sender.is_a?(@object.class)
|
26
|
+
|
27
|
+
# even if the call is async, if the call is _going_ to generate an error, we make it synchronous
|
28
|
+
# so that the error is generated immediately, instead of waiting for it to fail in a job,
|
29
|
+
# which might go unnoticed
|
30
|
+
if !@sender.nil? && !@synchronous
|
31
|
+
@synchronous = true if !sender_is_object && @object.private_methods.include?(method)
|
32
|
+
@synchronous = true if !sender_is_class && @object.protected_methods.include?(method)
|
33
|
+
end
|
34
|
+
|
18
35
|
if @synchronous
|
19
|
-
if @
|
20
|
-
if kwargs.empty?
|
21
|
-
return @object.public_send(method, *args)
|
22
|
-
else
|
23
|
-
return @object.public_send(method, *args, **kwargs)
|
24
|
-
end
|
25
|
-
else
|
36
|
+
if @sender.nil? || sender_is_object || sender_is_class && @object.protected_methods.include?(method)
|
26
37
|
if kwargs.empty?
|
27
38
|
return @object.send(method, *args)
|
28
39
|
else
|
29
40
|
return @object.send(method, *args, **kwargs)
|
30
41
|
end
|
31
42
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
43
|
+
|
44
|
+
if kwargs.empty?
|
45
|
+
return @object.public_send(method, *args)
|
46
|
+
else
|
47
|
+
return @object.public_send(method, *args, **kwargs)
|
48
|
+
end
|
36
49
|
end
|
37
50
|
|
38
51
|
ignore_transaction = @enqueue_args.delete(:ignore_transaction)
|
@@ -50,7 +63,8 @@ module Delayed
|
|
50
63
|
::Delayed::Job.enqueue(::Delayed::PerformableMethod.new(@object, method,
|
51
64
|
args: args, kwargs: kwargs,
|
52
65
|
on_failure: on_failure,
|
53
|
-
on_permanent_failure: on_permanent_failure
|
66
|
+
on_permanent_failure: on_permanent_failure,
|
67
|
+
sender: @sender),
|
54
68
|
**@enqueue_args)
|
55
69
|
end
|
56
70
|
return nil
|
@@ -61,14 +75,15 @@ module Delayed
|
|
61
75
|
args: args,
|
62
76
|
kwargs: kwargs,
|
63
77
|
on_failure: on_failure,
|
64
|
-
on_permanent_failure: on_permanent_failure
|
78
|
+
on_permanent_failure: on_permanent_failure,
|
79
|
+
sender: @sender),
|
65
80
|
**@enqueue_args)
|
66
81
|
result = nil unless ignore_transaction
|
67
82
|
result
|
68
83
|
end
|
69
84
|
end
|
70
85
|
|
71
|
-
def delay(
|
86
|
+
def delay(sender: nil, **enqueue_args)
|
72
87
|
# support procs/methods as enqueue arguments
|
73
88
|
enqueue_args.each do |k,v|
|
74
89
|
if v.respond_to?(:call)
|
@@ -76,21 +91,15 @@ module Delayed
|
|
76
91
|
end
|
77
92
|
end
|
78
93
|
|
79
|
-
|
94
|
+
sender ||= __calculate_sender_for_delay
|
80
95
|
|
81
|
-
DelayProxy.new(self,
|
96
|
+
DelayProxy.new(self, sender: sender, **enqueue_args)
|
82
97
|
end
|
83
98
|
|
84
|
-
def
|
99
|
+
def __calculate_sender_for_delay
|
85
100
|
# enforce public send in dev and test, but not prod (since it uses
|
86
101
|
# debug APIs, it's expensive)
|
87
|
-
|
88
|
-
sender = self.sender(1)
|
89
|
-
# if the caller isn't self, use public_send; i.e. enforce method visibility
|
90
|
-
sender != self
|
91
|
-
else
|
92
|
-
false
|
93
|
-
end
|
102
|
+
return sender(1) if ::Rails.env.test? || ::Rails.env.development?
|
94
103
|
end
|
95
104
|
|
96
105
|
module ClassMethods
|
@@ -114,7 +123,7 @@ module Delayed
|
|
114
123
|
if synchronous
|
115
124
|
super(*args, **kwargs)
|
116
125
|
else
|
117
|
-
delay(**enqueue_args).method_missing(method_name, *args, synchronous: true, **kwargs)
|
126
|
+
delay(sender: __calculate_sender_for_delay, **enqueue_args).method_missing(method_name, *args, synchronous: true, **kwargs)
|
118
127
|
end
|
119
128
|
end)
|
120
129
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Delayed
|
4
|
-
class PerformableMethod < Struct.new(:object, :method, :args, :kwargs, :fail_cb, :permanent_fail_cb, :
|
5
|
-
def initialize(object, method, args: [], kwargs: {}, on_failure: nil, on_permanent_failure: nil,
|
4
|
+
class PerformableMethod < Struct.new(:object, :method, :args, :kwargs, :fail_cb, :permanent_fail_cb, :sender)
|
5
|
+
def initialize(object, method, args: [], kwargs: {}, on_failure: nil, on_permanent_failure: nil, sender: nil)
|
6
6
|
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method, true)
|
7
7
|
|
8
8
|
self.object = object
|
@@ -11,7 +11,13 @@ module Delayed
|
|
11
11
|
self.method = method.to_sym
|
12
12
|
self.fail_cb = on_failure
|
13
13
|
self.permanent_fail_cb = on_permanent_failure
|
14
|
-
self.
|
14
|
+
self.sender = sender
|
15
|
+
begin
|
16
|
+
YAML.load(YAML.dump(sender))
|
17
|
+
rescue
|
18
|
+
# if for some reason you can't dump the sender, just drop it
|
19
|
+
self.sender = nil
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
def display_name
|
@@ -25,17 +31,21 @@ module Delayed
|
|
25
31
|
|
26
32
|
def perform
|
27
33
|
kwargs = self.kwargs || {}
|
28
|
-
|
34
|
+
|
35
|
+
sender_is_object = sender == object
|
36
|
+
sender_is_class = sender.is_a?(object.class)
|
37
|
+
|
38
|
+
if sender.nil? || sender_is_object || sender_is_class && object.protected_methods.include?(method)
|
29
39
|
if kwargs.empty?
|
30
|
-
object.
|
40
|
+
object.send(method, *args)
|
31
41
|
else
|
32
|
-
object.
|
42
|
+
object.send(method, *args, **kwargs)
|
33
43
|
end
|
34
44
|
else
|
35
45
|
if kwargs.empty?
|
36
|
-
object.
|
46
|
+
object.public_send(method, *args)
|
37
47
|
else
|
38
|
-
object.
|
48
|
+
object.public_send(method, *args, **kwargs)
|
39
49
|
end
|
40
50
|
end
|
41
51
|
end
|
@@ -63,7 +73,8 @@ module Delayed
|
|
63
73
|
|
64
74
|
def full_name
|
65
75
|
obj_name = object.is_a?(ActiveRecord::Base) ? "#{object.class}.find(#{object.id}).#{method}" : display_name
|
66
|
-
|
76
|
+
kgs = kwargs || {}
|
77
|
+
kwargs_str = kgs.map { |(k, v)| ", #{k}: #{deep_de_ar_ize(v)}"}.join("")
|
67
78
|
"#{obj_name}(#{args.map { |a| deep_de_ar_ize(a) }.join(', ')}#{kwargs_str})"
|
68
79
|
end
|
69
80
|
end
|
data/lib/delayed/periodic.rb
CHANGED
@@ -49,10 +49,20 @@ class Periodic
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def enqueue
|
52
|
-
Delayed::Job.enqueue(self,
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
data/lib/delayed/version.rb
CHANGED
data/lib/delayed/worker.rb
CHANGED
@@ -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'
|
@@ -216,6 +227,12 @@ class Worker
|
|
216
227
|
logger.info("Completed #{log_job(job)} #{"%.0fms" % (runtime * 1000)}")
|
217
228
|
end
|
218
229
|
count
|
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
|
219
236
|
rescue SystemExit => se
|
220
237
|
# There wasn't really a failure here so no callbacks and whatnot needed,
|
221
238
|
# still reschedule the job though.
|
@@ -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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
@@ -8,21 +8,38 @@ RSpec.describe Delayed::MessageSending do
|
|
8
8
|
allow(::Rails.env).to receive(:test?).and_return(true)
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
before (:all) do
|
12
|
+
class SpecClass
|
13
13
|
def call_private(**enqueue_args)
|
14
14
|
delay(**enqueue_args).private_method
|
15
15
|
end
|
16
16
|
|
17
|
+
def call_protected(**enqueue_args)
|
18
|
+
other = self.class.new
|
19
|
+
other.delay(**enqueue_args).protected_method
|
20
|
+
end
|
21
|
+
|
17
22
|
private
|
18
23
|
|
19
24
|
def private_method
|
20
25
|
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def protected_method
|
30
|
+
end
|
21
31
|
end
|
22
32
|
end
|
23
33
|
|
34
|
+
after(:all) do
|
35
|
+
Object.send(:remove_const, :SpecClass)
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:klass) { SpecClass }
|
39
|
+
|
24
40
|
it "allows an object to send a private message to itself" do
|
25
|
-
klass.new.call_private
|
41
|
+
job = klass.new.call_private(ignore_transaction: true)
|
42
|
+
job.invoke_job
|
26
43
|
end
|
27
44
|
|
28
45
|
it "allows an object to send a private message to itself synchronouosly" do
|
@@ -48,4 +65,37 @@ RSpec.describe Delayed::MessageSending do
|
|
48
65
|
allow(::Rails.env).to receive(:development?).and_return(false)
|
49
66
|
klass.new.delay(synchronous: true).private_method
|
50
67
|
end
|
68
|
+
|
69
|
+
it "allows an object to send a protected message to itself" do
|
70
|
+
job = klass.new.call_protected(ignore_transaction: true)
|
71
|
+
job.invoke_job
|
72
|
+
end
|
73
|
+
|
74
|
+
it "allows an object to send a protected message to itself synchronouosly" do
|
75
|
+
klass.new.call_protected(synchronous: true)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "warns about directly sending a protected message asynchronously" do
|
79
|
+
expect { klass.new.delay.protected_method }.to raise_error(NoMethodError)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "warns about directly sending a protected message synchronusly" do
|
83
|
+
expect { klass.new.delay(synchronous: true).protected_method }.to raise_error(NoMethodError)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "doesn't explode if you can't dump the sender" do
|
87
|
+
klass = Class.new do
|
88
|
+
def delay_something
|
89
|
+
Kernel.delay.sleep(1)
|
90
|
+
end
|
91
|
+
|
92
|
+
def encode_with(encoder)
|
93
|
+
raise "yaml encoding failed"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
obj = klass.new
|
98
|
+
expect { YAML.dump(obj) }.to raise_error("yaml encoding failed")
|
99
|
+
expect { obj.delay_something }.not_to raise_error
|
100
|
+
end
|
51
101
|
end
|
@@ -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
|