delayed 0.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +560 -0
- data/Rakefile +35 -0
- data/lib/delayed.rb +72 -0
- data/lib/delayed/active_job_adapter.rb +65 -0
- data/lib/delayed/backend/base.rb +166 -0
- data/lib/delayed/backend/job_preparer.rb +43 -0
- data/lib/delayed/exceptions.rb +14 -0
- data/lib/delayed/job.rb +250 -0
- data/lib/delayed/lifecycle.rb +85 -0
- data/lib/delayed/message_sending.rb +65 -0
- data/lib/delayed/monitor.rb +134 -0
- data/lib/delayed/performable_mailer.rb +22 -0
- data/lib/delayed/performable_method.rb +47 -0
- data/lib/delayed/plugin.rb +15 -0
- data/lib/delayed/plugins/connection.rb +13 -0
- data/lib/delayed/plugins/instrumentation.rb +39 -0
- data/lib/delayed/priority.rb +164 -0
- data/lib/delayed/psych_ext.rb +135 -0
- data/lib/delayed/railtie.rb +7 -0
- data/lib/delayed/runnable.rb +46 -0
- data/lib/delayed/serialization/active_record.rb +18 -0
- data/lib/delayed/syck_ext.rb +42 -0
- data/lib/delayed/tasks.rb +40 -0
- data/lib/delayed/worker.rb +233 -0
- data/lib/delayed/yaml_ext.rb +10 -0
- data/lib/delayed_job.rb +1 -0
- data/lib/delayed_job_active_record.rb +1 -0
- data/lib/generators/delayed/generator.rb +7 -0
- data/lib/generators/delayed/migration_generator.rb +28 -0
- data/lib/generators/delayed/next_migration_version.rb +14 -0
- data/lib/generators/delayed/templates/migration.rb +22 -0
- data/spec/autoloaded/clazz.rb +6 -0
- data/spec/autoloaded/instance_clazz.rb +5 -0
- data/spec/autoloaded/instance_struct.rb +6 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/database.yml +25 -0
- data/spec/delayed/active_job_adapter_spec.rb +267 -0
- data/spec/delayed/job_spec.rb +953 -0
- data/spec/delayed/monitor_spec.rb +276 -0
- data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
- data/spec/delayed/priority_spec.rb +154 -0
- data/spec/delayed/serialization/active_record_spec.rb +15 -0
- data/spec/delayed/tasks_spec.rb +116 -0
- data/spec/helper.rb +196 -0
- data/spec/lifecycle_spec.rb +77 -0
- data/spec/message_sending_spec.rb +149 -0
- data/spec/performable_mailer_spec.rb +68 -0
- data/spec/performable_method_spec.rb +123 -0
- data/spec/psych_ext_spec.rb +94 -0
- data/spec/sample_jobs.rb +117 -0
- data/spec/worker_spec.rb +235 -0
- data/spec/yaml_ext_spec.rb +48 -0
- metadata +326 -0
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/gem_helper'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
ADAPTERS = %w(mysql2 postgresql sqlite3).freeze
|
7
|
+
|
8
|
+
ADAPTERS.each do |adapter|
|
9
|
+
desc "Run RSpec code examples for #{adapter} adapter"
|
10
|
+
RSpec::Core::RakeTask.new(adapter => "#{adapter}:adapter")
|
11
|
+
|
12
|
+
namespace adapter do
|
13
|
+
task :adapter do
|
14
|
+
ENV['ADAPTER'] = adapter
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task :adapter do
|
20
|
+
ENV['ADAPTER'] = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rubocop/rake_task'
|
24
|
+
RuboCop::RakeTask.new
|
25
|
+
|
26
|
+
if ENV['APPRAISAL_INITIALIZED'] || ENV['CI']
|
27
|
+
tasks = ADAPTERS + [:adapter]
|
28
|
+
tasks += [:rubocop] unless ENV['CI']
|
29
|
+
|
30
|
+
task default: tasks
|
31
|
+
else
|
32
|
+
require 'appraisal'
|
33
|
+
Appraisal::Task.new
|
34
|
+
task default: :appraisal
|
35
|
+
end
|
data/lib/delayed.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/numeric/time'
|
3
|
+
require 'delayed/exceptions'
|
4
|
+
require 'delayed/message_sending'
|
5
|
+
require 'delayed/performable_method'
|
6
|
+
require 'delayed/yaml_ext'
|
7
|
+
require 'delayed/lifecycle'
|
8
|
+
require 'delayed/runnable'
|
9
|
+
require 'delayed/priority'
|
10
|
+
require 'delayed/monitor'
|
11
|
+
require 'delayed/plugin'
|
12
|
+
require 'delayed/plugins/connection'
|
13
|
+
require 'delayed/plugins/instrumentation'
|
14
|
+
require 'delayed/backend/base'
|
15
|
+
require 'delayed/backend/job_preparer'
|
16
|
+
require 'delayed/worker'
|
17
|
+
require 'delayed/railtie' if defined?(Rails::Railtie)
|
18
|
+
|
19
|
+
ActiveSupport.on_load(:active_record) do
|
20
|
+
require 'delayed/serialization/active_record'
|
21
|
+
require 'delayed/job'
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveSupport.on_load(:active_job) do
|
25
|
+
require 'delayed/active_job_adapter'
|
26
|
+
ActiveJob::QueueAdapters::DelayedAdapter = Class.new(Delayed::ActiveJobAdapter)
|
27
|
+
|
28
|
+
include Delayed::ActiveJobAdapter::EnqueuingPatch
|
29
|
+
end
|
30
|
+
|
31
|
+
ActiveSupport.on_load(:action_mailer) do
|
32
|
+
require 'delayed/performable_mailer'
|
33
|
+
ActionMailer::Base.extend(Delayed::DelayMail)
|
34
|
+
ActionMailer::Parameterized::Mailer.include(Delayed::DelayMail) if defined?(ActionMailer::Parameterized::Mailer)
|
35
|
+
end
|
36
|
+
|
37
|
+
module Delayed
|
38
|
+
autoload :PerformableMailer, 'delayed/performable_mailer'
|
39
|
+
|
40
|
+
mattr_accessor(:default_log_level) { 'info'.freeze }
|
41
|
+
mattr_accessor(:plugins) do
|
42
|
+
[
|
43
|
+
Delayed::Plugins::Instrumentation,
|
44
|
+
Delayed::Plugins::Connection,
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.lifecycle
|
49
|
+
setup_lifecycle unless @lifecycle
|
50
|
+
@lifecycle
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.setup_lifecycle
|
54
|
+
@lifecycle = Delayed::Lifecycle.new
|
55
|
+
plugins.each { |klass| klass.new }
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.logger
|
59
|
+
@logger ||= Rails.logger
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.logger=(value)
|
63
|
+
@logger = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.say(message, level = default_log_level)
|
67
|
+
logger&.send(level, message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Object.include Delayed::MessageSending
|
72
|
+
Module.include Delayed::MessageSendingClassMethods
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Delayed
|
2
|
+
class ActiveJobAdapter
|
3
|
+
def enqueue(job)
|
4
|
+
_enqueue(job)
|
5
|
+
end
|
6
|
+
|
7
|
+
def enqueue_at(job, timestamp)
|
8
|
+
_enqueue(job, run_at: Time.at(timestamp)) # rubocop:disable Rails/TimeZone
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def _enqueue(job, opts = {})
|
14
|
+
opts.merge!({ queue: job.queue_name, priority: job.priority }.compact)
|
15
|
+
.merge!(job.provider_attributes || {})
|
16
|
+
|
17
|
+
Delayed::Job.enqueue(JobWrapper.new(job.serialize), opts).tap do |dj|
|
18
|
+
job.provider_job_id = dj.id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module EnqueuingPatch
|
23
|
+
def self.included(klass)
|
24
|
+
klass.prepend PrependedMethods
|
25
|
+
klass.attr_accessor :provider_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
module PrependedMethods
|
29
|
+
def enqueue(opts = {})
|
30
|
+
raise "`:run_at` is not supported. Use `:wait_until` instead." if opts.key?(:run_at)
|
31
|
+
|
32
|
+
self.provider_attributes = opts.except(:wait, :wait_until, :queue, :priority)
|
33
|
+
opts[:priority] = Delayed::Priority.new(opts[:priority]) if opts.key?(:priority)
|
34
|
+
super(opts)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class JobWrapper # rubocop:disable Betterment/ActiveJobPerformable
|
41
|
+
attr_accessor :job_data
|
42
|
+
|
43
|
+
delegate_missing_to :job
|
44
|
+
|
45
|
+
def initialize(job_data)
|
46
|
+
@job_data = job_data
|
47
|
+
end
|
48
|
+
|
49
|
+
def display_name
|
50
|
+
job_data['job_class']
|
51
|
+
end
|
52
|
+
|
53
|
+
def perform
|
54
|
+
ActiveJob::Callbacks.run_callbacks(:execute) do
|
55
|
+
job.perform_now
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def job
|
62
|
+
@job ||= ActiveJob::Base.deserialize(job_data) if job_data
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Backend
|
3
|
+
module Base
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Add a job to the queue
|
10
|
+
def enqueue(*args)
|
11
|
+
job_options = Delayed::Backend::JobPreparer.new(*args).prepare
|
12
|
+
enqueue_job(job_options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def enqueue_job(options)
|
16
|
+
new(options).tap do |job|
|
17
|
+
Delayed.lifecycle.run_callbacks(:enqueue, job) do
|
18
|
+
job.hook(:enqueue)
|
19
|
+
Delayed::Worker.delay_job?(job) ? job.save : job.invoke_job
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def reserve(worker, max_run_time = Worker.max_run_time)
|
25
|
+
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
26
|
+
# this leads to a more even distribution of jobs across the worker processes
|
27
|
+
claims = 0
|
28
|
+
find_available(worker.name, worker.read_ahead, max_run_time).select do |job|
|
29
|
+
next if claims >= worker.max_claims
|
30
|
+
|
31
|
+
job.lock_exclusively!(max_run_time, worker.name).tap do |result|
|
32
|
+
claims += 1 if result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Allow the backend to attempt recovery from reserve errors
|
38
|
+
def recover_from(_error); end
|
39
|
+
|
40
|
+
def work_off(num = 100)
|
41
|
+
warn '[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead.'
|
42
|
+
Delayed::Worker.new.work_off(num)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :error
|
47
|
+
|
48
|
+
def error=(error)
|
49
|
+
@error = error
|
50
|
+
self.last_error = "#{error.message}\n#{error.backtrace.join("\n")}" if respond_to?(:last_error=)
|
51
|
+
end
|
52
|
+
|
53
|
+
def failed?
|
54
|
+
!!failed_at
|
55
|
+
end
|
56
|
+
alias failed failed?
|
57
|
+
|
58
|
+
ParseObjectFromYaml = %r{!ruby/\w+:([^\s]+)}.freeze # rubocop:disable Naming/ConstantName
|
59
|
+
|
60
|
+
def name # rubocop:disable Metrics/AbcSize
|
61
|
+
@name ||= payload_object.job_data['job_class'] if payload_object.respond_to?(:job_data)
|
62
|
+
@name ||= payload_object.display_name if payload_object.respond_to?(:display_name)
|
63
|
+
@name ||= payload_object.class.name
|
64
|
+
rescue DeserializationError
|
65
|
+
ParseObjectFromYaml.match(handler)[1]
|
66
|
+
end
|
67
|
+
|
68
|
+
def priority
|
69
|
+
Priority.new(super)
|
70
|
+
end
|
71
|
+
|
72
|
+
def priority=(value)
|
73
|
+
super(Priority.new(value))
|
74
|
+
end
|
75
|
+
|
76
|
+
def payload_object=(object)
|
77
|
+
@payload_object = object
|
78
|
+
self.handler = YAML.dump_dj(object)
|
79
|
+
end
|
80
|
+
|
81
|
+
def payload_object
|
82
|
+
@payload_object ||= YAML.load_dj(handler)
|
83
|
+
rescue TypeError, LoadError, NameError, ArgumentError, SyntaxError, Psych::SyntaxError => e
|
84
|
+
raise DeserializationError, "Job failed to load: #{e.message}. Handler: #{handler.inspect}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def invoke_job
|
88
|
+
Delayed::Worker.lifecycle.run_callbacks(:invoke_job, self) do
|
89
|
+
hook :before
|
90
|
+
payload_object.perform
|
91
|
+
hook :success
|
92
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
93
|
+
hook :error, e
|
94
|
+
raise e
|
95
|
+
ensure
|
96
|
+
hook :after
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Unlock this job (note: not saved to DB)
|
101
|
+
def unlock
|
102
|
+
self.locked_at = nil
|
103
|
+
self.locked_by = nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def hook(name, *args)
|
107
|
+
if payload_object.respond_to?(name)
|
108
|
+
if payload_object.is_a?(Delayed::JobWrapper)
|
109
|
+
return if name == :enqueue # this callback is not supported due to method naming conflicts.
|
110
|
+
|
111
|
+
warn '[DEPRECATION] Job hook methods (`before`, `after`, `success`, etc) are deprecated. Use ActiveJob callbacks instead.'
|
112
|
+
end
|
113
|
+
|
114
|
+
method = payload_object.method(name)
|
115
|
+
method.arity.zero? ? method.call : method.call(self, *args)
|
116
|
+
end
|
117
|
+
rescue DeserializationError
|
118
|
+
end
|
119
|
+
|
120
|
+
def reschedule_at
|
121
|
+
if payload_object.respond_to?(:reschedule_at)
|
122
|
+
payload_object.reschedule_at(self.class.db_time_now, attempts)
|
123
|
+
else
|
124
|
+
self.class.db_time_now + (attempts**4) + 5
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def max_attempts
|
129
|
+
payload_object.max_attempts if payload_object.respond_to?(:max_attempts)
|
130
|
+
end
|
131
|
+
|
132
|
+
def max_run_time
|
133
|
+
return unless payload_object.respond_to?(:max_run_time)
|
134
|
+
return unless (run_time = payload_object.max_run_time)
|
135
|
+
|
136
|
+
if run_time > Delayed::Worker.max_run_time
|
137
|
+
Delayed::Worker.max_run_time
|
138
|
+
else
|
139
|
+
run_time
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def destroy_failed_jobs?
|
144
|
+
payload_object.respond_to?(:destroy_failed_jobs?) ? payload_object.destroy_failed_jobs? : Delayed::Worker.destroy_failed_jobs
|
145
|
+
rescue DeserializationError
|
146
|
+
Delayed::Worker.destroy_failed_jobs
|
147
|
+
end
|
148
|
+
|
149
|
+
def fail!
|
150
|
+
self.failed_at = self.class.db_time_now
|
151
|
+
save!
|
152
|
+
end
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
def set_default_run_at
|
157
|
+
self.run_at ||= self.class.db_time_now
|
158
|
+
end
|
159
|
+
|
160
|
+
# Call during reload operation to clear out internal state
|
161
|
+
def reset
|
162
|
+
@payload_object = nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Backend
|
3
|
+
class JobPreparer
|
4
|
+
attr_reader :options, :args
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
@options = args.extract_options!.dup
|
8
|
+
@args = args
|
9
|
+
end
|
10
|
+
|
11
|
+
def prepare
|
12
|
+
set_payload
|
13
|
+
set_queue_name
|
14
|
+
set_priority
|
15
|
+
handle_deprecation
|
16
|
+
options
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def set_payload
|
22
|
+
options[:payload_object] ||= args.shift
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_queue_name
|
26
|
+
options[:queue] ||= options[:payload_object].queue_name if options[:payload_object].respond_to?(:queue_name)
|
27
|
+
options[:queue] ||= Delayed::Worker.default_queue_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_priority
|
31
|
+
options[:priority] ||= options[:payload_object].priority if options[:payload_object].respond_to?(:priority)
|
32
|
+
options[:priority] ||= Delayed::Worker.default_priority
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_deprecation
|
36
|
+
unless options[:payload_object].respond_to?(:perform)
|
37
|
+
raise ArgumentError,
|
38
|
+
'Cannot enqueue items which do not respond to perform'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
class WorkerTimeout < RuntimeError
|
5
|
+
def message
|
6
|
+
seconds = Delayed::Worker.max_run_time.to_i
|
7
|
+
"#{super} (Delayed::Worker.max_run_time is only #{seconds} second#{seconds == 1 ? '' : 's'})"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class FatalBackendError < RuntimeError; end
|
12
|
+
|
13
|
+
class DeserializationError < StandardError; end
|
14
|
+
end
|
data/lib/delayed/job.rb
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Job < ::ActiveRecord::Base
|
3
|
+
include Delayed::Backend::Base
|
4
|
+
|
5
|
+
scope :by_priority, lambda { order("priority ASC, run_at ASC") }
|
6
|
+
scope :min_priority, lambda { |priority| where("priority >= ?", priority) if priority }
|
7
|
+
scope :max_priority, lambda { |priority| where("priority <= ?", priority) if priority }
|
8
|
+
scope :for_queues, lambda { |queues| where(queue: queues) if queues.any? }
|
9
|
+
|
10
|
+
scope :locked, -> { where.not(locked_at: nil) }
|
11
|
+
scope :erroring, -> { where.not(last_error: nil) }
|
12
|
+
scope :failed, -> { where.not(failed_at: nil) }
|
13
|
+
scope :not_locked, -> { where(locked_at: nil) }
|
14
|
+
scope :not_failed, -> { where(failed_at: nil) }
|
15
|
+
scope :workable, ->(timestamp) { not_locked.not_failed.where("run_at <= ?", timestamp) }
|
16
|
+
scope :working, -> { locked.not_failed }
|
17
|
+
|
18
|
+
before_save :set_default_run_at
|
19
|
+
|
20
|
+
REENQUEUE_BUFFER = 30.seconds
|
21
|
+
|
22
|
+
def self.set_delayed_job_table_name
|
23
|
+
delayed_job_table_name = "#{::ActiveRecord::Base.table_name_prefix}delayed_jobs"
|
24
|
+
self.table_name = delayed_job_table_name
|
25
|
+
end
|
26
|
+
|
27
|
+
set_delayed_job_table_name
|
28
|
+
|
29
|
+
def self.ready_to_run(worker_name, max_run_time)
|
30
|
+
where(
|
31
|
+
"((run_at <= ? AND (locked_at IS NULL OR locked_at < ?)) OR locked_by = ?) AND failed_at IS NULL",
|
32
|
+
db_time_now,
|
33
|
+
db_time_now - (max_run_time + REENQUEUE_BUFFER),
|
34
|
+
worker_name,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# When a worker is exiting, make sure we don't have any locked jobs.
|
39
|
+
def self.clear_locks!(worker_name)
|
40
|
+
where(locked_by: worker_name).update_all(locked_by: nil, locked_at: nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.reserve(worker, max_run_time = Worker.max_run_time)
|
44
|
+
ready_scope =
|
45
|
+
ready_to_run(worker.name, max_run_time)
|
46
|
+
.min_priority(worker.min_priority)
|
47
|
+
.max_priority(worker.max_priority)
|
48
|
+
.for_queues(worker.queues)
|
49
|
+
.by_priority
|
50
|
+
|
51
|
+
ActiveSupport::Notifications.instrument('delayed.worker.reserve_jobs', worker_tags(worker)) do
|
52
|
+
reserve_with_scope(ready_scope, worker, db_time_now)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.reserve_with_scope(ready_scope, worker, now)
|
57
|
+
case connection.adapter_name
|
58
|
+
when "PostgreSQL", "PostGIS"
|
59
|
+
reserve_with_scope_using_optimized_postgres(ready_scope, worker, now)
|
60
|
+
when "MySQL", "Mysql2"
|
61
|
+
reserve_with_scope_using_optimized_mysql(ready_scope, worker, now)
|
62
|
+
when "MSSQL", "Teradata"
|
63
|
+
reserve_with_scope_using_optimized_mssql(ready_scope, worker, now)
|
64
|
+
# Fallback for unknown / other DBMS
|
65
|
+
else
|
66
|
+
reserve_with_scope_using_default_sql(ready_scope, worker, now)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.reserve_with_scope_using_default_sql(ready_scope, worker, now)
|
71
|
+
# This is our old fashion, tried and true, but possibly slower lookup
|
72
|
+
# Instead of reading the entire job record for our detect loop, we select only the id,
|
73
|
+
# and only read the full job record after we've successfully locked the job.
|
74
|
+
# This can have a noticable impact on large read_ahead configurations and large payload jobs.
|
75
|
+
attrs = { locked_at: now, locked_by: worker.name }
|
76
|
+
|
77
|
+
jobs = []
|
78
|
+
ready_scope.limit(worker.read_ahead).select(:id).each do |job|
|
79
|
+
break if jobs.count >= worker.max_claims
|
80
|
+
next unless ready_scope.where(id: job.id).update_all(attrs) == 1
|
81
|
+
|
82
|
+
jobs << job.reload
|
83
|
+
end
|
84
|
+
|
85
|
+
jobs
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.reserve_with_scope_using_optimized_postgres(ready_scope, worker, now) # rubocop:disable Metrics/AbcSize
|
89
|
+
# Custom SQL required for PostgreSQL because postgres does not support UPDATE...LIMIT
|
90
|
+
# This locks the single record 'FOR UPDATE' in the subquery
|
91
|
+
# http://www.postgresql.org/docs/9.0/static/sql-select.html#SQL-FOR-UPDATE-SHARE
|
92
|
+
# Note: active_record would attempt to generate UPDATE...LIMIT like
|
93
|
+
# SQL for Postgres if we use a .limit() filter, but it would not
|
94
|
+
# use 'FOR UPDATE' and we would have many locking conflicts
|
95
|
+
table = connection.quote_table_name(table_name)
|
96
|
+
|
97
|
+
# Rather than relying on a primary key, we use "WHERE ctid =", resulting in a fast 'Tid Scan'.
|
98
|
+
if worker.max_claims > 1
|
99
|
+
subquery = ready_scope.limit(worker.max_claims).lock("FOR UPDATE SKIP LOCKED").select("ctid").to_sql
|
100
|
+
sql = "UPDATE #{table} SET locked_at = ?, locked_by = ? WHERE ctid = ANY (ARRAY (#{subquery})) RETURNING *"
|
101
|
+
else
|
102
|
+
subquery = ready_scope.limit(1).lock("FOR UPDATE SKIP LOCKED").select("ctid").to_sql
|
103
|
+
sql = "UPDATE #{table} SET locked_at = ?, locked_by = ? WHERE ctid = (#{subquery}) RETURNING *"
|
104
|
+
end
|
105
|
+
|
106
|
+
find_by_sql([sql, now, worker.name]).sort_by(&:priority)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.reserve_with_scope_using_optimized_mysql(ready_scope, worker, now)
|
110
|
+
# Removing the millisecond precision from now(time object)
|
111
|
+
# MySQL 5.6.4 onwards millisecond precision exists, but the
|
112
|
+
# datetime object created doesn't have precision, so discarded
|
113
|
+
# while updating. But during the where clause, for mysql(>=5.6.4),
|
114
|
+
# it queries with precision as well. So removing the precision
|
115
|
+
now = now.change(usec: 0)
|
116
|
+
# Despite MySQL's support of UPDATE...LIMIT, it has an optimizer bug
|
117
|
+
# that results in filesorts rather than index scans, which is very
|
118
|
+
# expensive with a large number of jobs in the table:
|
119
|
+
# http://bugs.mysql.com/bug.php?id=74049
|
120
|
+
# The PostgreSQL and MSSQL reserve strategies, while valid syntax in
|
121
|
+
# MySQL, result in deadlocks so we use a SELECT then UPDATE strategy
|
122
|
+
# that is more likely to false-negative when attempting to reserve
|
123
|
+
# jobs in parallel but doesn't rely on subselects or transactions.
|
124
|
+
|
125
|
+
# Also, we are fetching multiple candidate_jobs at a time to try to
|
126
|
+
# avoid the situation where multiple workers try to grab the same
|
127
|
+
# job at the same time. That previously had caused poor performance
|
128
|
+
# since ready_scope.where(id: job.id) would return nothing even
|
129
|
+
# though there was a large number of jobs on the queue.
|
130
|
+
attrs = { locked_at: now, locked_by: worker.name }
|
131
|
+
|
132
|
+
jobs = []
|
133
|
+
ready_scope.limit(worker.read_ahead).each do |job|
|
134
|
+
break if jobs.count >= worker.max_claims
|
135
|
+
next unless ready_scope.where(id: job.id).update_all(attrs) == 1
|
136
|
+
|
137
|
+
job.assign_attributes(attrs)
|
138
|
+
job.send(:changes_applied)
|
139
|
+
jobs << job
|
140
|
+
end
|
141
|
+
|
142
|
+
jobs
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.reserve_with_scope_using_optimized_mssql(ready_scope, worker, now)
|
146
|
+
# The MSSQL driver doesn't generate a limit clause when update_all
|
147
|
+
# is called directly
|
148
|
+
subsubquery_sql = ready_scope.limit(1).to_sql
|
149
|
+
# select("id") doesn't generate a subquery, so force a subquery
|
150
|
+
subquery_sql = "SELECT id FROM (#{subsubquery_sql}) AS x"
|
151
|
+
quoted_table_name = connection.quote_table_name(table_name)
|
152
|
+
sql = "UPDATE #{quoted_table_name} SET locked_at = ?, locked_by = ? WHERE id IN (#{subquery_sql})"
|
153
|
+
count = connection.execute(sanitize_sql([sql, now, worker.name]))
|
154
|
+
return [] if count.zero?
|
155
|
+
|
156
|
+
# MSSQL JDBC doesn't support OUTPUT INSERTED.* for returning a result set, so query locked row
|
157
|
+
where(locked_at: now, locked_by: worker.name, failed_at: nil)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Get the current time (GMT or local depending on DB)
|
161
|
+
# Note: This does not ping the DB to get the time, so all your clients
|
162
|
+
# must have syncronized clocks.
|
163
|
+
def self.db_time_now
|
164
|
+
if Time.zone
|
165
|
+
Time.zone.now
|
166
|
+
elsif ::ActiveRecord::Base.default_timezone == :utc
|
167
|
+
Time.now.utc
|
168
|
+
else
|
169
|
+
Time.current
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.worker_tags(worker)
|
174
|
+
{
|
175
|
+
min_priority: worker.min_priority,
|
176
|
+
max_priority: worker.max_priority,
|
177
|
+
max_claims: worker.max_claims,
|
178
|
+
read_ahead: worker.read_ahead,
|
179
|
+
queues: worker.queues,
|
180
|
+
table: table_name,
|
181
|
+
database: database_name,
|
182
|
+
database_adapter: database_adapter_name,
|
183
|
+
worker: worker,
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.database_name
|
188
|
+
connection_config[:database]
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.database_adapter_name
|
192
|
+
connection_config[:adapter]
|
193
|
+
end
|
194
|
+
|
195
|
+
if ActiveRecord.gem_version >= Gem::Version.new('6.1')
|
196
|
+
def self.connection_config
|
197
|
+
connection_db_config.configuration_hash
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def reload(*args)
|
202
|
+
reset
|
203
|
+
super
|
204
|
+
end
|
205
|
+
|
206
|
+
def alert_age
|
207
|
+
if payload_object.respond_to?(:alert_age)
|
208
|
+
payload_object.alert_age
|
209
|
+
else
|
210
|
+
priority.alert_age
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def alert_run_time
|
215
|
+
if payload_object.respond_to?(:alert_run_time)
|
216
|
+
payload_object.alert_run_time
|
217
|
+
else
|
218
|
+
priority.alert_run_time
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def alert_attempts
|
223
|
+
if payload_object.respond_to?(:alert_attempts)
|
224
|
+
payload_object.alert_attempts
|
225
|
+
else
|
226
|
+
priority.alert_attempts
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def age
|
231
|
+
[(locked_at || self.class.db_time_now) - run_at, 0].max
|
232
|
+
end
|
233
|
+
|
234
|
+
def run_time
|
235
|
+
self.class.db_time_now - locked_at if locked_at
|
236
|
+
end
|
237
|
+
|
238
|
+
def age_alert?
|
239
|
+
alert_age&.<= age
|
240
|
+
end
|
241
|
+
|
242
|
+
def run_time_alert?
|
243
|
+
alert_run_time&.<= run_time if run_time # locked_at may be `nil` if `delay_jobs` is false
|
244
|
+
end
|
245
|
+
|
246
|
+
def attempts_alert?
|
247
|
+
alert_attempts&.<= attempts
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|