inst-jobs 1.0.0 → 2.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 +4 -4
- data/lib/delayed/core_ext/kernel.rb +9 -0
- data/lib/delayed/message_sending.rb +56 -9
- data/lib/delayed/performable_method.rb +26 -6
- data/lib/delayed/periodic.rb +14 -4
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/worker/consul_health_check.rb +1 -1
- data/lib/delayed/worker/health_check.rb +34 -19
- data/lib/delayed_job.rb +2 -0
- data/spec/active_record_job_spec.rb +0 -4
- data/spec/delayed/message_sending_spec.rb +101 -0
- data/spec/delayed/periodic_spec.rb +39 -0
- data/spec/delayed/server_spec.rb +0 -4
- data/spec/delayed/work_queue/in_process_spec.rb +0 -4
- data/spec/delayed/work_queue/parent_process/client_spec.rb +0 -4
- data/spec/delayed/work_queue/parent_process/server_spec.rb +0 -1
- data/spec/delayed/work_queue/parent_process_spec.rb +0 -1
- data/spec/delayed/worker/consul_health_check_spec.rb +1 -1
- data/spec/delayed/worker/health_check_spec.rb +9 -0
- data/spec/redis_job_spec.rb +0 -4
- data/spec/shared/delayed_method.rb +2 -2
- data/spec/shared/performable_method.rb +6 -0
- data/spec/spec_helper.rb +9 -0
- metadata +27 -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 -222
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d674b7da21caf04eb87ff9823ed549c93a901219669316090d088f0699564e59
|
4
|
+
data.tar.gz: 021456d34f12eff8cc988db866018d701fc77ffdbb57e9fb308fc1bd25a91ecb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad78cfdd9026db24b714c532c8ee837a875e443afc375909f0c130e3cfbf87d1f872344f982d931838bfa6649a2f1edc59430f6444a2baee08f8afb568015cfc
|
7
|
+
data.tar.gz: e2b127477f0687958178505628b9544aa5c49e7aa1d0ceef32892250aa26aeb1c77f12bcacd6682e17c2bc379f987b154a0f982e029852432c39f7b3a5335df8
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Kernel
|
2
|
+
def sender(i = 0)
|
3
|
+
frame_self = nil
|
4
|
+
# 3. one for the block, one for this method, one for the method calling this
|
5
|
+
# method, and _then_ we get to the self for who sent the message we want
|
6
|
+
RubyVM::DebugInspector.open { |dc| frame_self = dc.frame_self(3 + i) }
|
7
|
+
frame_self
|
8
|
+
end
|
9
|
+
end
|
@@ -1,14 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
if ::Rails.env.test? || ::Rails.env.development?
|
4
|
+
require 'debug_inspector'
|
5
|
+
end
|
6
|
+
|
3
7
|
module Delayed
|
4
8
|
module MessageSending
|
5
9
|
class DelayProxy < BasicObject
|
6
|
-
def initialize(object, enqueue_args)
|
10
|
+
def initialize(object, synchronous: false, sender: nil, **enqueue_args)
|
7
11
|
@object = object
|
8
12
|
@enqueue_args = enqueue_args
|
13
|
+
@synchronous = synchronous
|
14
|
+
@sender = sender
|
9
15
|
end
|
10
16
|
|
11
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
|
+
|
35
|
+
if @synchronous
|
36
|
+
if @sender.nil? || sender_is_object || sender_is_class && @object.protected_methods.include?(method)
|
37
|
+
if kwargs.empty?
|
38
|
+
return @object.send(method, *args)
|
39
|
+
else
|
40
|
+
return @object.send(method, *args, **kwargs)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if kwargs.empty?
|
45
|
+
return @object.public_send(method, *args)
|
46
|
+
else
|
47
|
+
return @object.public_send(method, *args, **kwargs)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
12
51
|
ignore_transaction = @enqueue_args.delete(:ignore_transaction)
|
13
52
|
on_failure = @enqueue_args.delete(:on_failure)
|
14
53
|
on_permanent_failure = @enqueue_args.delete(:on_permanent_failure)
|
@@ -24,7 +63,8 @@ module Delayed
|
|
24
63
|
::Delayed::Job.enqueue(::Delayed::PerformableMethod.new(@object, method,
|
25
64
|
args: args, kwargs: kwargs,
|
26
65
|
on_failure: on_failure,
|
27
|
-
on_permanent_failure: on_permanent_failure
|
66
|
+
on_permanent_failure: on_permanent_failure,
|
67
|
+
sender: @sender),
|
28
68
|
**@enqueue_args)
|
29
69
|
end
|
30
70
|
return nil
|
@@ -35,24 +75,31 @@ module Delayed
|
|
35
75
|
args: args,
|
36
76
|
kwargs: kwargs,
|
37
77
|
on_failure: on_failure,
|
38
|
-
on_permanent_failure: on_permanent_failure
|
78
|
+
on_permanent_failure: on_permanent_failure,
|
79
|
+
sender: @sender),
|
39
80
|
**@enqueue_args)
|
40
81
|
result = nil unless ignore_transaction
|
41
82
|
result
|
42
83
|
end
|
43
84
|
end
|
44
85
|
|
45
|
-
def delay(**enqueue_args)
|
86
|
+
def delay(sender: nil, **enqueue_args)
|
46
87
|
# support procs/methods as enqueue arguments
|
47
88
|
enqueue_args.each do |k,v|
|
48
89
|
if v.respond_to?(:call)
|
49
90
|
enqueue_args[k] = v.call(self)
|
50
91
|
end
|
51
92
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
DelayProxy.new(self, enqueue_args)
|
93
|
+
|
94
|
+
sender ||= __calculate_sender_for_delay
|
95
|
+
|
96
|
+
DelayProxy.new(self, sender: sender, **enqueue_args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def __calculate_sender_for_delay
|
100
|
+
# enforce public send in dev and test, but not prod (since it uses
|
101
|
+
# debug APIs, it's expensive)
|
102
|
+
return sender(1) if ::Rails.env.test? || ::Rails.env.development?
|
56
103
|
end
|
57
104
|
|
58
105
|
module ClassMethods
|
@@ -76,7 +123,7 @@ module Delayed
|
|
76
123
|
if synchronous
|
77
124
|
super(*args, **kwargs)
|
78
125
|
else
|
79
|
-
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)
|
80
127
|
end
|
81
128
|
end)
|
82
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,6 +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.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
|
14
21
|
end
|
15
22
|
|
16
23
|
def display_name
|
@@ -24,10 +31,22 @@ module Delayed
|
|
24
31
|
|
25
32
|
def perform
|
26
33
|
kwargs = self.kwargs || {}
|
27
|
-
|
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)
|
39
|
+
if kwargs.empty?
|
40
|
+
object.send(method, *args)
|
41
|
+
else
|
42
|
+
object.send(method, *args, **kwargs)
|
43
|
+
end
|
29
44
|
else
|
30
|
-
|
45
|
+
if kwargs.empty?
|
46
|
+
object.public_send(method, *args)
|
47
|
+
else
|
48
|
+
object.public_send(method, *args, **kwargs)
|
49
|
+
end
|
31
50
|
end
|
32
51
|
end
|
33
52
|
|
@@ -54,7 +73,8 @@ module Delayed
|
|
54
73
|
|
55
74
|
def full_name
|
56
75
|
obj_name = object.is_a?(ActiveRecord::Base) ? "#{object.class}.find(#{object.id}).#{method}" : display_name
|
57
|
-
|
76
|
+
kgs = kwargs || {}
|
77
|
+
kwargs_str = kgs.map { |(k, v)| ", #{k}: #{deep_de_ar_ize(v)}"}.join("")
|
58
78
|
"#{obj_name}(#{args.map { |a| deep_de_ar_ize(a) }.join(', ')}#{kwargs_str})"
|
59
79
|
end
|
60
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
@@ -13,7 +13,7 @@ module Delayed
|
|
13
13
|
DEFAULT_SERVICE_NAME = 'inst-jobs_worker'.freeze
|
14
14
|
attr_reader :agent_client, :catalog_client
|
15
15
|
|
16
|
-
def initialize(
|
16
|
+
def initialize(*, **)
|
17
17
|
super
|
18
18
|
# Because we don't want the consul client to be a hard dependency we're
|
19
19
|
# only requiring it once it's absolutely needed
|
@@ -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
|
data/lib/delayed_job.rb
CHANGED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'debug_inspector'
|
5
|
+
|
6
|
+
RSpec.describe Delayed::MessageSending do
|
7
|
+
before do
|
8
|
+
allow(::Rails.env).to receive(:test?).and_return(true)
|
9
|
+
end
|
10
|
+
|
11
|
+
before (:all) do
|
12
|
+
class SpecClass
|
13
|
+
def call_private(**enqueue_args)
|
14
|
+
delay(**enqueue_args).private_method
|
15
|
+
end
|
16
|
+
|
17
|
+
def call_protected(**enqueue_args)
|
18
|
+
other = self.class.new
|
19
|
+
other.delay(**enqueue_args).protected_method
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def private_method
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def protected_method
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
after(:all) do
|
35
|
+
Object.send(:remove_const, :SpecClass)
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:klass) { SpecClass }
|
39
|
+
|
40
|
+
it "allows an object to send a private message to itself" do
|
41
|
+
job = klass.new.call_private(ignore_transaction: true)
|
42
|
+
job.invoke_job
|
43
|
+
end
|
44
|
+
|
45
|
+
it "allows an object to send a private message to itself synchronouosly" do
|
46
|
+
klass.new.call_private(synchronous: true)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "warns about directly sending a private message asynchronously" do
|
50
|
+
expect { klass.new.delay.private_method }.to raise_error(NoMethodError)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "warns about directly sending a private message synchronusly" do
|
54
|
+
expect { klass.new.delay(synchronous: true).private_method }.to raise_error(NoMethodError)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "does not warn about directly sending a private message in production" do
|
58
|
+
allow(::Rails.env).to receive(:test?).and_return(false)
|
59
|
+
allow(::Rails.env).to receive(:development?).and_return(false)
|
60
|
+
klass.new.delay.private_method
|
61
|
+
end
|
62
|
+
|
63
|
+
it "does not warn about directly sending a private message synchronously in production" do
|
64
|
+
allow(::Rails.env).to receive(:test?).and_return(false)
|
65
|
+
allow(::Rails.env).to receive(:development?).and_return(false)
|
66
|
+
klass.new.delay(synchronous: true).private_method
|
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
|
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
|