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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c754ed71778d5b3f08641e133be947fae204a56dcee71aef08a7780a7d6afa5
4
- data.tar.gz: 10949318e78e8e9d56b92ee7f2d14bfd647da20ff70578d894e4d1e85441d2fe
3
+ metadata.gz: d674b7da21caf04eb87ff9823ed549c93a901219669316090d088f0699564e59
4
+ data.tar.gz: 021456d34f12eff8cc988db866018d701fc77ffdbb57e9fb308fc1bd25a91ecb
5
5
  SHA512:
6
- metadata.gz: 1a014e191bc0d6ccd083340b6d5c4bca0b7e03439aca459f79a5a704d93d16c42dd9e4855ca5985f7b1e4fb1fe62eead160cb6cb356c73b00ac297dc30112d8a
7
- data.tar.gz: 53997c66017cf6db561104054379cb9e7e934fcc56c54d552b7629ff29eeb58de37c8674088661ba683e7e4d8b19234f26021819753ac06a6c738dd435c3f169
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
- if enqueue_args.delete(:synchronous)
53
- return self
54
- end
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
- if kwargs.empty?
28
- object.send(method, *args)
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
- object.send(method, *args, **kwargs)
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
- 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("")
58
78
  "#{obj_name}(#{args.map { |a| deep_de_ar_ize(a) }.join(', ')}#{kwargs_str})"
59
79
  end
60
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.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -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(*args)
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
- 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
@@ -19,6 +19,8 @@ require 'active_support/core_ext/module/attribute_accessors'
19
19
  require 'active_record'
20
20
  require 'after_transaction_commit'
21
21
 
22
+ require 'delayed/core_ext/kernel'
23
+
22
24
  require 'delayed/settings'
23
25
  require 'delayed/yaml_extensions'
24
26
 
@@ -7,10 +7,6 @@ describe 'Delayed::Backed::ActiveRecord::Job' do
7
7
  Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
8
8
  end
9
9
 
10
- after :all do
11
- Delayed.send(:remove_const, :Job)
12
- end
13
-
14
10
  before do
15
11
  Delayed::Testing.clear_all!
16
12
  end
@@ -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