good_job 2.1.0 → 2.2.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/CHANGELOG.md +29 -1
- data/README.md +32 -0
- data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +2 -2
- data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +2 -2
- data/engine/app/controllers/good_job/active_jobs_controller.rb +3 -2
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +10 -0
- data/engine/app/controllers/good_job/dashboards_controller.rb +17 -16
- data/engine/app/controllers/good_job/executions_controller.rb +10 -0
- data/engine/app/views/good_job/active_jobs/show.html.erb +3 -1
- data/engine/app/views/good_job/cron_schedules/index.html.erb +28 -0
- data/engine/app/views/good_job/dashboards/index.html.erb +5 -5
- data/engine/app/views/good_job/shared/_executions_table.erb +56 -0
- data/engine/app/views/layouts/good_job/base.html.erb +9 -3
- data/engine/config/routes.rb +2 -1
- data/lib/good_job/active_job_extensions/concurrency.rb +6 -6
- data/lib/good_job/adapter.rb +8 -8
- data/lib/good_job/cron_manager.rb +3 -3
- data/lib/good_job/{current_execution.rb → current_thread.rb} +8 -8
- data/lib/good_job/execution.rb +308 -0
- data/lib/good_job/job.rb +6 -294
- data/lib/good_job/job_performer.rb +2 -2
- data/lib/good_job/log_subscriber.rb +4 -4
- data/lib/good_job/notifier.rb +3 -3
- data/lib/good_job/railtie.rb +2 -2
- data/lib/good_job/scheduler.rb +2 -2
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +2 -2
- metadata +8 -5
- data/engine/app/controllers/good_job/jobs_controller.rb +0 -10
- data/engine/app/views/good_job/shared/_jobs_table.erb +0 -48
@@ -24,7 +24,7 @@ module GoodJob # :nodoc:
|
|
24
24
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
25
25
|
end
|
26
26
|
|
27
|
-
#
|
27
|
+
# Execution configuration to be scheduled
|
28
28
|
# @return [Hash]
|
29
29
|
attr_reader :schedules
|
30
30
|
|
@@ -90,8 +90,8 @@ module GoodJob # :nodoc:
|
|
90
90
|
# Re-schedule the next cron task before executing the current task
|
91
91
|
thr_scheduler.create_task(thr_cron_key)
|
92
92
|
|
93
|
-
|
94
|
-
|
93
|
+
CurrentThread.reset
|
94
|
+
CurrentThread.cron_key = thr_cron_key
|
95
95
|
|
96
96
|
Rails.application.executor.wrap do
|
97
97
|
schedule = thr_scheduler.schedules.fetch(thr_cron_key).with_indifferent_access
|
@@ -4,7 +4,7 @@ require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
|
4
4
|
module GoodJob
|
5
5
|
# Thread-local attributes for passing values from Instrumentation.
|
6
6
|
# (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
|
7
|
-
module
|
7
|
+
module CurrentThread
|
8
8
|
# @!attribute [rw] cron_key
|
9
9
|
# @!scope class
|
10
10
|
# Cron Key
|
@@ -23,24 +23,24 @@ module GoodJob
|
|
23
23
|
# @return [Exception, nil]
|
24
24
|
thread_mattr_accessor :error_on_retry
|
25
25
|
|
26
|
-
# @!attribute [rw]
|
26
|
+
# @!attribute [rw] executions
|
27
27
|
# @!scope class
|
28
|
-
#
|
29
|
-
# @return [GoodJob::
|
30
|
-
thread_mattr_accessor :
|
28
|
+
# Execution
|
29
|
+
# @return [GoodJob::Execution, nil]
|
30
|
+
thread_mattr_accessor :execution
|
31
31
|
|
32
32
|
# Resets attributes
|
33
33
|
# @return [void]
|
34
34
|
def self.reset
|
35
35
|
self.cron_key = nil
|
36
|
-
self.
|
36
|
+
self.execution = nil
|
37
37
|
self.error_on_discard = nil
|
38
38
|
self.error_on_retry = nil
|
39
39
|
end
|
40
40
|
|
41
|
-
# @return [String] UUID of the currently executing GoodJob::
|
41
|
+
# @return [String] UUID of the currently executing GoodJob::Execution
|
42
42
|
def self.active_job_id
|
43
|
-
|
43
|
+
execution&.active_job_id
|
44
44
|
end
|
45
45
|
|
46
46
|
# @return [Integer] Current process ID
|
@@ -0,0 +1,308 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
5
|
+
# @!parse
|
6
|
+
# class Execution < ActiveRecord::Base; end
|
7
|
+
class Execution < Object.const_get(GoodJob.active_record_parent_class)
|
8
|
+
include Lockable
|
9
|
+
|
10
|
+
# Raised if something attempts to execute a previously completed Execution again.
|
11
|
+
PreviouslyPerformedError = Class.new(StandardError)
|
12
|
+
|
13
|
+
# ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
|
14
|
+
DEFAULT_QUEUE_NAME = 'default'
|
15
|
+
# ActiveJob jobs without a +priority+ attribute are given this priority.
|
16
|
+
DEFAULT_PRIORITY = 0
|
17
|
+
|
18
|
+
self.table_name = 'good_jobs'
|
19
|
+
self.advisory_lockable_column = 'active_job_id'
|
20
|
+
|
21
|
+
# Parse a string representing a group of queues into a more readable data
|
22
|
+
# structure.
|
23
|
+
# @param string [String] Queue string
|
24
|
+
# @return [Hash]
|
25
|
+
# How to match a given queue. It can have the following keys and values:
|
26
|
+
# - +{ all: true }+ indicates that all queues match.
|
27
|
+
# - +{ exclude: Array<String> }+ indicates the listed queue names should
|
28
|
+
# not match.
|
29
|
+
# - +{ include: Array<String> }+ indicates the listed queue names should
|
30
|
+
# match.
|
31
|
+
# @example
|
32
|
+
# GoodJob::Execution.queue_parser('-queue1,queue2')
|
33
|
+
# => { exclude: [ 'queue1', 'queue2' ] }
|
34
|
+
def self.queue_parser(string)
|
35
|
+
string = string.presence || '*'
|
36
|
+
|
37
|
+
if string.first == '-'
|
38
|
+
exclude_queues = true
|
39
|
+
string = string[1..-1]
|
40
|
+
end
|
41
|
+
|
42
|
+
queues = string.split(',').map(&:strip)
|
43
|
+
|
44
|
+
if queues.include?('*')
|
45
|
+
{ all: true }
|
46
|
+
elsif exclude_queues
|
47
|
+
{ exclude: queues }
|
48
|
+
else
|
49
|
+
{ include: queues }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get Jobs with given ActiveJob ID
|
54
|
+
# @!method active_job_id
|
55
|
+
# @!scope class
|
56
|
+
# @param active_job_id [String]
|
57
|
+
# ActiveJob ID
|
58
|
+
# @return [ActiveRecord::Relation]
|
59
|
+
scope :active_job_id, ->(active_job_id) { where(active_job_id: active_job_id) }
|
60
|
+
|
61
|
+
# Get Jobs with given class name
|
62
|
+
# @!method job_class
|
63
|
+
# @!scope class
|
64
|
+
# @param string [String]
|
65
|
+
# Execution class name
|
66
|
+
# @return [ActiveRecord::Relation]
|
67
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
68
|
+
|
69
|
+
# Get Jobs that have not yet been completed.
|
70
|
+
# @!method unfinished
|
71
|
+
# @!scope class
|
72
|
+
# @return [ActiveRecord::Relation]
|
73
|
+
scope :unfinished, -> { where(finished_at: nil) }
|
74
|
+
|
75
|
+
# Get Jobs that are not scheduled for a later time than now (i.e. jobs that
|
76
|
+
# are not scheduled or scheduled for earlier than the current time).
|
77
|
+
# @!method only_scheduled
|
78
|
+
# @!scope class
|
79
|
+
# @return [ActiveRecord::Relation]
|
80
|
+
scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
|
81
|
+
|
82
|
+
# Order jobs by priority (highest priority first).
|
83
|
+
# @!method priority_ordered
|
84
|
+
# @!scope class
|
85
|
+
# @return [ActiveRecord::Relation]
|
86
|
+
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
87
|
+
|
88
|
+
# Order jobs by scheduled (unscheduled or soonest first).
|
89
|
+
# @!method schedule_ordered
|
90
|
+
# @!scope class
|
91
|
+
# @return [ActiveRecord::Relation]
|
92
|
+
scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
|
93
|
+
|
94
|
+
# Get Jobs were completed before the given timestamp. If no timestamp is
|
95
|
+
# provided, get all jobs that have been completed. By default, GoodJob
|
96
|
+
# deletes jobs after they are completed and this will find no jobs.
|
97
|
+
# However, if you have changed {GoodJob.preserve_job_records}, this may
|
98
|
+
# find completed Jobs.
|
99
|
+
# @!method finished(timestamp = nil)
|
100
|
+
# @!scope class
|
101
|
+
# @param timestamp (Float)
|
102
|
+
# Get jobs that finished before this time (in epoch time).
|
103
|
+
# @return [ActiveRecord::Relation]
|
104
|
+
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
105
|
+
|
106
|
+
# Get Jobs that started but not finished yet.
|
107
|
+
# @!method running
|
108
|
+
# @!scope class
|
109
|
+
# @return [ActiveRecord::Relation]
|
110
|
+
scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
|
111
|
+
|
112
|
+
# Get Jobs that do not have subsequent retries
|
113
|
+
# @!method running
|
114
|
+
# @!scope class
|
115
|
+
# @return [ActiveRecord::Relation]
|
116
|
+
scope :head, -> { where(retried_good_job_id: nil) }
|
117
|
+
|
118
|
+
# Get Jobs have errored that will not be retried further
|
119
|
+
# @!method running
|
120
|
+
# @!scope class
|
121
|
+
# @return [ActiveRecord::Relation]
|
122
|
+
scope :dead, -> { head.where.not(error: nil) }
|
123
|
+
|
124
|
+
# Get Jobs on queues that match the given queue string.
|
125
|
+
# @!method queue_string(string)
|
126
|
+
# @!scope class
|
127
|
+
# @param string [String]
|
128
|
+
# A string expression describing what queues to select. See
|
129
|
+
# {Execution.queue_parser} or
|
130
|
+
# {file:README.md#optimize-queues-threads-and-processes} for more details
|
131
|
+
# on the format of the string. Note this only handles individual
|
132
|
+
# semicolon-separated segments of that string format.
|
133
|
+
# @return [ActiveRecord::Relation]
|
134
|
+
scope :queue_string, (lambda do |string|
|
135
|
+
parsed = queue_parser(string)
|
136
|
+
|
137
|
+
if parsed[:all]
|
138
|
+
all
|
139
|
+
elsif parsed[:exclude]
|
140
|
+
where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
|
141
|
+
elsif parsed[:include]
|
142
|
+
where(queue_name: parsed[:include])
|
143
|
+
end
|
144
|
+
end)
|
145
|
+
|
146
|
+
# Get Jobs in display order with optional keyset pagination.
|
147
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
148
|
+
# @!scope class
|
149
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
150
|
+
# Display records scheduled after this time for keyset pagination
|
151
|
+
# @param after_id [Numeric, String, nil]
|
152
|
+
# Display records after this ID for keyset pagination
|
153
|
+
# @return [ActiveRecord::Relation]
|
154
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
155
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
156
|
+
if after_scheduled_at.present? && after_id.present?
|
157
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
158
|
+
elsif after_scheduled_at.present?
|
159
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
160
|
+
end
|
161
|
+
query
|
162
|
+
end)
|
163
|
+
|
164
|
+
# Finds the next eligible Execution, acquire an advisory lock related to it, and
|
165
|
+
# executes the job.
|
166
|
+
# @return [ExecutionResult, nil]
|
167
|
+
# If a job was executed, returns an array with the {Execution} record, the
|
168
|
+
# return value for the job's +#perform+ method, and the exception the job
|
169
|
+
# raised, if any (if the job raised, then the second array entry will be
|
170
|
+
# +nil+). If there were no jobs to execute, returns +nil+.
|
171
|
+
def self.perform_with_advisory_lock
|
172
|
+
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |executions|
|
173
|
+
execution = executions.first
|
174
|
+
break if execution.blank?
|
175
|
+
break :unlocked unless execution&.executable?
|
176
|
+
|
177
|
+
begin
|
178
|
+
execution.with_advisory_lock(key: "good_jobs-#{execution.active_job_id}") do
|
179
|
+
execution.perform
|
180
|
+
end
|
181
|
+
rescue RecordAlreadyAdvisoryLockedError => e
|
182
|
+
ExecutionResult.new(value: nil, handled_error: e)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Fetches the scheduled execution time of the next eligible Execution(s).
|
188
|
+
# @param after [DateTime]
|
189
|
+
# @param limit [Integer]
|
190
|
+
# @param now_limit [Integer, nil]
|
191
|
+
# @return [Array<DateTime>]
|
192
|
+
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
193
|
+
query = advisory_unlocked.unfinished.schedule_ordered
|
194
|
+
|
195
|
+
after ||= Time.current
|
196
|
+
after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
|
197
|
+
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
198
|
+
|
199
|
+
if now_limit&.positive?
|
200
|
+
now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
|
201
|
+
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
202
|
+
end
|
203
|
+
|
204
|
+
Array(now_at) + after_at
|
205
|
+
end
|
206
|
+
|
207
|
+
# Places an ActiveJob job on a queue by creating a new {Execution} record.
|
208
|
+
# @param active_job [ActiveJob::Base]
|
209
|
+
# The job to enqueue.
|
210
|
+
# @param scheduled_at [Float]
|
211
|
+
# Epoch timestamp when the job should be executed.
|
212
|
+
# @param create_with_advisory_lock [Boolean]
|
213
|
+
# Whether to establish a lock on the {Execution} record after it is created.
|
214
|
+
# @return [Execution]
|
215
|
+
# The new {Execution} instance representing the queued ActiveJob job.
|
216
|
+
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
217
|
+
ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
|
218
|
+
execution_args = {
|
219
|
+
active_job_id: active_job.job_id,
|
220
|
+
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
221
|
+
priority: active_job.priority || DEFAULT_PRIORITY,
|
222
|
+
serialized_params: active_job.serialize,
|
223
|
+
scheduled_at: scheduled_at,
|
224
|
+
create_with_advisory_lock: create_with_advisory_lock,
|
225
|
+
}
|
226
|
+
|
227
|
+
execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
|
228
|
+
|
229
|
+
if CurrentThread.cron_key
|
230
|
+
execution_args[:cron_key] = CurrentThread.cron_key
|
231
|
+
elsif CurrentThread.active_job_id == active_job.job_id
|
232
|
+
execution_args[:cron_key] = CurrentThread.execution.cron_key
|
233
|
+
end
|
234
|
+
|
235
|
+
execution = GoodJob::Execution.new(**execution_args)
|
236
|
+
|
237
|
+
instrument_payload[:execution] = execution
|
238
|
+
|
239
|
+
execution.save!
|
240
|
+
active_job.provider_job_id = execution.id
|
241
|
+
|
242
|
+
CurrentThread.execution.retried_good_job_id = execution.id if CurrentThread.execution && CurrentThread.execution.active_job_id == active_job.job_id
|
243
|
+
|
244
|
+
execution
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Execute the ActiveJob job this {Execution} represents.
|
249
|
+
# @return [ExecutionResult]
|
250
|
+
# An array of the return value of the job's +#perform+ method and the
|
251
|
+
# exception raised by the job, if any. If the job completed successfully,
|
252
|
+
# the second array entry (the exception) will be +nil+ and vice versa.
|
253
|
+
def perform
|
254
|
+
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
255
|
+
|
256
|
+
self.performed_at = Time.current
|
257
|
+
save! if GoodJob.preserve_job_records
|
258
|
+
|
259
|
+
result = execute
|
260
|
+
|
261
|
+
job_error = result.handled_error || result.unhandled_error
|
262
|
+
self.error = "#{job_error.class}: #{job_error.message}" if job_error
|
263
|
+
|
264
|
+
if result.unhandled_error && GoodJob.retry_on_unhandled_error
|
265
|
+
save!
|
266
|
+
elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
267
|
+
self.finished_at = Time.current
|
268
|
+
save!
|
269
|
+
else
|
270
|
+
destroy!
|
271
|
+
end
|
272
|
+
|
273
|
+
result
|
274
|
+
end
|
275
|
+
|
276
|
+
# Tests whether this job is safe to be executed by this thread.
|
277
|
+
# @return [Boolean]
|
278
|
+
def executable?
|
279
|
+
self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
# @return [ExecutionResult]
|
285
|
+
def execute
|
286
|
+
GoodJob::CurrentThread.reset
|
287
|
+
GoodJob::CurrentThread.execution = self
|
288
|
+
|
289
|
+
job_data = serialized_params.deep_dup
|
290
|
+
job_data["provider_job_id"] = id
|
291
|
+
|
292
|
+
# DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
|
293
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, execution: self, process_id: GoodJob::CurrentThread.process_id, thread_name: GoodJob::CurrentThread.thread_name }) do
|
294
|
+
value = ActiveJob::Base.execute(job_data)
|
295
|
+
|
296
|
+
if value.is_a?(Exception)
|
297
|
+
handled_error = value
|
298
|
+
value = nil
|
299
|
+
end
|
300
|
+
handled_error ||= GoodJob::CurrentThread.error_on_retry || GoodJob::CurrentThread.error_on_discard
|
301
|
+
|
302
|
+
ExecutionResult.new(value: value, handled_error: handled_error)
|
303
|
+
rescue StandardError => e
|
304
|
+
ExecutionResult.new(value: nil, unhandled_error: e)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -1,299 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# Raised if something attempts to execute a previously completed Job again.
|
11
|
-
PreviouslyPerformedError = Class.new(StandardError)
|
12
|
-
|
13
|
-
# ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
|
14
|
-
DEFAULT_QUEUE_NAME = 'default'
|
15
|
-
# ActiveJob jobs without a +priority+ attribute are given this priority.
|
16
|
-
DEFAULT_PRIORITY = 0
|
17
|
-
|
18
|
-
self.table_name = 'good_jobs'
|
19
|
-
self.advisory_lockable_column = 'active_job_id'
|
20
|
-
|
21
|
-
# Parse a string representing a group of queues into a more readable data
|
22
|
-
# structure.
|
23
|
-
# @param string [String] Queue string
|
24
|
-
# @return [Hash]
|
25
|
-
# How to match a given queue. It can have the following keys and values:
|
26
|
-
# - +{ all: true }+ indicates that all queues match.
|
27
|
-
# - +{ exclude: Array<String> }+ indicates the listed queue names should
|
28
|
-
# not match.
|
29
|
-
# - +{ include: Array<String> }+ indicates the listed queue names should
|
30
|
-
# match.
|
31
|
-
# @example
|
32
|
-
# GoodJob::Job.queue_parser('-queue1,queue2')
|
33
|
-
# => { exclude: [ 'queue1', 'queue2' ] }
|
34
|
-
def self.queue_parser(string)
|
35
|
-
string = string.presence || '*'
|
36
|
-
|
37
|
-
if string.first == '-'
|
38
|
-
exclude_queues = true
|
39
|
-
string = string[1..-1]
|
40
|
-
end
|
41
|
-
|
42
|
-
queues = string.split(',').map(&:strip)
|
43
|
-
|
44
|
-
if queues.include?('*')
|
45
|
-
{ all: true }
|
46
|
-
elsif exclude_queues
|
47
|
-
{ exclude: queues }
|
48
|
-
else
|
49
|
-
{ include: queues }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
# Get Jobs with given class name
|
54
|
-
# @!method with_job_class
|
55
|
-
# @!scope class
|
56
|
-
# @param string [String]
|
57
|
-
# Job class name
|
58
|
-
# @return [ActiveRecord::Relation]
|
59
|
-
scope :with_job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
60
|
-
|
61
|
-
# Get Jobs that have not yet been completed.
|
62
|
-
# @!method unfinished
|
63
|
-
# @!scope class
|
64
|
-
# @return [ActiveRecord::Relation]
|
65
|
-
scope :unfinished, -> { where(finished_at: nil) }
|
66
|
-
|
67
|
-
# Get Jobs that are not scheduled for a later time than now (i.e. jobs that
|
68
|
-
# are not scheduled or scheduled for earlier than the current time).
|
69
|
-
# @!method only_scheduled
|
70
|
-
# @!scope class
|
71
|
-
# @return [ActiveRecord::Relation]
|
72
|
-
scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
|
73
|
-
|
74
|
-
# Order jobs by priority (highest priority first).
|
75
|
-
# @!method priority_ordered
|
76
|
-
# @!scope class
|
77
|
-
# @return [ActiveRecord::Relation]
|
78
|
-
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
79
|
-
|
80
|
-
# Order jobs by scheduled (unscheduled or soonest first).
|
81
|
-
# @!method schedule_ordered
|
82
|
-
# @!scope class
|
83
|
-
# @return [ActiveRecord::Relation]
|
84
|
-
scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
|
85
|
-
|
86
|
-
# Get Jobs were completed before the given timestamp. If no timestamp is
|
87
|
-
# provided, get all jobs that have been completed. By default, GoodJob
|
88
|
-
# deletes jobs after they are completed and this will find no jobs.
|
89
|
-
# However, if you have changed {GoodJob.preserve_job_records}, this may
|
90
|
-
# find completed Jobs.
|
91
|
-
# @!method finished(timestamp = nil)
|
92
|
-
# @!scope class
|
93
|
-
# @param timestamp (Float)
|
94
|
-
# Get jobs that finished before this time (in epoch time).
|
95
|
-
# @return [ActiveRecord::Relation]
|
96
|
-
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
97
|
-
|
98
|
-
# Get Jobs that started but not finished yet.
|
99
|
-
# @!method running
|
100
|
-
# @!scope class
|
101
|
-
# @return [ActiveRecord::Relation]
|
102
|
-
scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
|
103
|
-
|
104
|
-
# Get Jobs that do not have subsequent retries
|
105
|
-
# @!method running
|
106
|
-
# @!scope class
|
107
|
-
# @return [ActiveRecord::Relation]
|
108
|
-
scope :head, -> { where(retried_good_job_id: nil) }
|
109
|
-
|
110
|
-
# Get Jobs have errored that will not be retried further
|
111
|
-
# @!method running
|
112
|
-
# @!scope class
|
113
|
-
# @return [ActiveRecord::Relation]
|
114
|
-
scope :dead, -> { head.where.not(error: nil) }
|
115
|
-
|
116
|
-
# Get Jobs on queues that match the given queue string.
|
117
|
-
# @!method queue_string(string)
|
118
|
-
# @!scope class
|
119
|
-
# @param string [String]
|
120
|
-
# A string expression describing what queues to select. See
|
121
|
-
# {Job.queue_parser} or
|
122
|
-
# {file:README.md#optimize-queues-threads-and-processes} for more details
|
123
|
-
# on the format of the string. Note this only handles individual
|
124
|
-
# semicolon-separated segments of that string format.
|
125
|
-
# @return [ActiveRecord::Relation]
|
126
|
-
scope :queue_string, (lambda do |string|
|
127
|
-
parsed = queue_parser(string)
|
128
|
-
|
129
|
-
if parsed[:all]
|
130
|
-
all
|
131
|
-
elsif parsed[:exclude]
|
132
|
-
where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
|
133
|
-
elsif parsed[:include]
|
134
|
-
where(queue_name: parsed[:include])
|
135
|
-
end
|
136
|
-
end)
|
137
|
-
|
138
|
-
# Get Jobs in display order with optional keyset pagination.
|
139
|
-
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
140
|
-
# @!scope class
|
141
|
-
# @param after_scheduled_at [DateTime, String, nil]
|
142
|
-
# Display records scheduled after this time for keyset pagination
|
143
|
-
# @param after_id [Numeric, String, nil]
|
144
|
-
# Display records after this ID for keyset pagination
|
145
|
-
# @return [ActiveRecord::Relation]
|
146
|
-
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
147
|
-
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
148
|
-
if after_scheduled_at.present? && after_id.present?
|
149
|
-
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
150
|
-
elsif after_scheduled_at.present?
|
151
|
-
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
152
|
-
end
|
153
|
-
query
|
154
|
-
end)
|
155
|
-
|
156
|
-
# Finds the next eligible Job, acquire an advisory lock related to it, and
|
157
|
-
# executes the job.
|
158
|
-
# @return [ExecutionResult, nil]
|
159
|
-
# If a job was executed, returns an array with the {Job} record, the
|
160
|
-
# return value for the job's +#perform+ method, and the exception the job
|
161
|
-
# raised, if any (if the job raised, then the second array entry will be
|
162
|
-
# +nil+). If there were no jobs to execute, returns +nil+.
|
163
|
-
def self.perform_with_advisory_lock
|
164
|
-
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |good_jobs|
|
165
|
-
good_job = good_jobs.first
|
166
|
-
break if good_job.blank?
|
167
|
-
break :unlocked unless good_job&.executable?
|
168
|
-
|
169
|
-
begin
|
170
|
-
good_job.with_advisory_lock(key: "good_jobs-#{good_job.active_job_id}") do
|
171
|
-
good_job.perform
|
172
|
-
end
|
173
|
-
rescue RecordAlreadyAdvisoryLockedError => e
|
174
|
-
ExecutionResult.new(value: nil, handled_error: e)
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# Fetches the scheduled execution time of the next eligible Job(s).
|
180
|
-
# @param after [DateTime]
|
181
|
-
# @param limit [Integer]
|
182
|
-
# @param now_limit [Integer, nil]
|
183
|
-
# @return [Array<DateTime>]
|
184
|
-
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
185
|
-
query = advisory_unlocked.unfinished.schedule_ordered
|
186
|
-
|
187
|
-
after ||= Time.current
|
188
|
-
after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
|
189
|
-
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
190
|
-
|
191
|
-
if now_limit&.positive?
|
192
|
-
now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
|
193
|
-
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
194
|
-
end
|
195
|
-
|
196
|
-
Array(now_at) + after_at
|
197
|
-
end
|
198
|
-
|
199
|
-
# Places an ActiveJob job on a queue by creating a new {Job} record.
|
200
|
-
# @param active_job [ActiveJob::Base]
|
201
|
-
# The job to enqueue.
|
202
|
-
# @param scheduled_at [Float]
|
203
|
-
# Epoch timestamp when the job should be executed.
|
204
|
-
# @param create_with_advisory_lock [Boolean]
|
205
|
-
# Whether to establish a lock on the {Job} record after it is created.
|
206
|
-
# @return [Job]
|
207
|
-
# The new {Job} instance representing the queued ActiveJob job.
|
208
|
-
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
209
|
-
ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
|
210
|
-
good_job_args = {
|
211
|
-
active_job_id: active_job.job_id,
|
212
|
-
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
213
|
-
priority: active_job.priority || DEFAULT_PRIORITY,
|
214
|
-
serialized_params: active_job.serialize,
|
215
|
-
scheduled_at: scheduled_at,
|
216
|
-
create_with_advisory_lock: create_with_advisory_lock,
|
217
|
-
}
|
218
|
-
|
219
|
-
good_job_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
|
220
|
-
|
221
|
-
if CurrentExecution.cron_key
|
222
|
-
good_job_args[:cron_key] = CurrentExecution.cron_key
|
223
|
-
elsif CurrentExecution.active_job_id == active_job.job_id
|
224
|
-
good_job_args[:cron_key] = CurrentExecution.good_job.cron_key
|
225
|
-
end
|
226
|
-
|
227
|
-
good_job = GoodJob::Job.new(**good_job_args)
|
228
|
-
|
229
|
-
instrument_payload[:good_job] = good_job
|
230
|
-
|
231
|
-
good_job.save!
|
232
|
-
active_job.provider_job_id = good_job.id
|
233
|
-
|
234
|
-
CurrentExecution.good_job.retried_good_job_id = good_job.id if CurrentExecution.good_job && CurrentExecution.good_job.active_job_id == active_job.job_id
|
235
|
-
|
236
|
-
good_job
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
# Execute the ActiveJob job this {Job} represents.
|
241
|
-
# @return [ExecutionResult]
|
242
|
-
# An array of the return value of the job's +#perform+ method and the
|
243
|
-
# exception raised by the job, if any. If the job completed successfully,
|
244
|
-
# the second array entry (the exception) will be +nil+ and vice versa.
|
245
|
-
def perform
|
246
|
-
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
247
|
-
|
248
|
-
self.performed_at = Time.current
|
249
|
-
save! if GoodJob.preserve_job_records
|
250
|
-
|
251
|
-
result = execute
|
252
|
-
|
253
|
-
job_error = result.handled_error || result.unhandled_error
|
254
|
-
self.error = "#{job_error.class}: #{job_error.message}" if job_error
|
255
|
-
|
256
|
-
if result.unhandled_error && GoodJob.retry_on_unhandled_error
|
257
|
-
save!
|
258
|
-
elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
259
|
-
self.finished_at = Time.current
|
260
|
-
save!
|
261
|
-
else
|
262
|
-
destroy!
|
263
|
-
end
|
264
|
-
|
265
|
-
result
|
266
|
-
end
|
267
|
-
|
268
|
-
# Tests whether this job is safe to be executed by this thread.
|
269
|
-
# @return [Boolean]
|
270
|
-
def executable?
|
271
|
-
self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
|
272
|
-
end
|
273
|
-
|
274
|
-
private
|
275
|
-
|
276
|
-
# @return [ExecutionResult]
|
277
|
-
def execute
|
278
|
-
GoodJob::CurrentExecution.reset
|
279
|
-
GoodJob::CurrentExecution.good_job = self
|
280
|
-
|
281
|
-
job_data = serialized_params.deep_dup
|
282
|
-
job_data["provider_job_id"] = id
|
283
|
-
|
284
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
285
|
-
value = ActiveJob::Base.execute(job_data)
|
286
|
-
|
287
|
-
if value.is_a?(Exception)
|
288
|
-
handled_error = value
|
289
|
-
value = nil
|
290
|
-
end
|
291
|
-
handled_error ||= GoodJob::CurrentExecution.error_on_retry || GoodJob::CurrentExecution.error_on_discard
|
292
|
-
|
293
|
-
ExecutionResult.new(value: value, handled_error: handled_error)
|
294
|
-
rescue StandardError => e
|
295
|
-
ExecutionResult.new(value: nil, unhandled_error: e)
|
296
|
-
end
|
3
|
+
# @deprecated Use {GoodJob::Execution} instead.
|
4
|
+
class Job < Execution
|
5
|
+
after_initialize do |_job|
|
6
|
+
ActiveSupport::Deprecation.warn(
|
7
|
+
"The `GoodJob::Job` class name is deprecated. Replace with `GoodJob::Execution`."
|
8
|
+
)
|
297
9
|
end
|
298
10
|
end
|
299
11
|
end
|