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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +560 -0
  4. data/Rakefile +35 -0
  5. data/lib/delayed.rb +72 -0
  6. data/lib/delayed/active_job_adapter.rb +65 -0
  7. data/lib/delayed/backend/base.rb +166 -0
  8. data/lib/delayed/backend/job_preparer.rb +43 -0
  9. data/lib/delayed/exceptions.rb +14 -0
  10. data/lib/delayed/job.rb +250 -0
  11. data/lib/delayed/lifecycle.rb +85 -0
  12. data/lib/delayed/message_sending.rb +65 -0
  13. data/lib/delayed/monitor.rb +134 -0
  14. data/lib/delayed/performable_mailer.rb +22 -0
  15. data/lib/delayed/performable_method.rb +47 -0
  16. data/lib/delayed/plugin.rb +15 -0
  17. data/lib/delayed/plugins/connection.rb +13 -0
  18. data/lib/delayed/plugins/instrumentation.rb +39 -0
  19. data/lib/delayed/priority.rb +164 -0
  20. data/lib/delayed/psych_ext.rb +135 -0
  21. data/lib/delayed/railtie.rb +7 -0
  22. data/lib/delayed/runnable.rb +46 -0
  23. data/lib/delayed/serialization/active_record.rb +18 -0
  24. data/lib/delayed/syck_ext.rb +42 -0
  25. data/lib/delayed/tasks.rb +40 -0
  26. data/lib/delayed/worker.rb +233 -0
  27. data/lib/delayed/yaml_ext.rb +10 -0
  28. data/lib/delayed_job.rb +1 -0
  29. data/lib/delayed_job_active_record.rb +1 -0
  30. data/lib/generators/delayed/generator.rb +7 -0
  31. data/lib/generators/delayed/migration_generator.rb +28 -0
  32. data/lib/generators/delayed/next_migration_version.rb +14 -0
  33. data/lib/generators/delayed/templates/migration.rb +22 -0
  34. data/spec/autoloaded/clazz.rb +6 -0
  35. data/spec/autoloaded/instance_clazz.rb +5 -0
  36. data/spec/autoloaded/instance_struct.rb +6 -0
  37. data/spec/autoloaded/struct.rb +7 -0
  38. data/spec/database.yml +25 -0
  39. data/spec/delayed/active_job_adapter_spec.rb +267 -0
  40. data/spec/delayed/job_spec.rb +953 -0
  41. data/spec/delayed/monitor_spec.rb +276 -0
  42. data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
  43. data/spec/delayed/priority_spec.rb +154 -0
  44. data/spec/delayed/serialization/active_record_spec.rb +15 -0
  45. data/spec/delayed/tasks_spec.rb +116 -0
  46. data/spec/helper.rb +196 -0
  47. data/spec/lifecycle_spec.rb +77 -0
  48. data/spec/message_sending_spec.rb +149 -0
  49. data/spec/performable_mailer_spec.rb +68 -0
  50. data/spec/performable_method_spec.rb +123 -0
  51. data/spec/psych_ext_spec.rb +94 -0
  52. data/spec/sample_jobs.rb +117 -0
  53. data/spec/worker_spec.rb +235 -0
  54. data/spec/yaml_ext_spec.rb +48 -0
  55. 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
@@ -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