good_job 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/README.md +32 -0
  4. data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +2 -2
  5. data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +2 -2
  6. data/engine/app/controllers/good_job/active_jobs_controller.rb +3 -2
  7. data/engine/app/controllers/good_job/cron_schedules_controller.rb +10 -0
  8. data/engine/app/controllers/good_job/dashboards_controller.rb +17 -16
  9. data/engine/app/controllers/good_job/executions_controller.rb +10 -0
  10. data/engine/app/views/good_job/active_jobs/show.html.erb +3 -1
  11. data/engine/app/views/good_job/cron_schedules/index.html.erb +28 -0
  12. data/engine/app/views/good_job/dashboards/index.html.erb +5 -5
  13. data/engine/app/views/good_job/shared/_executions_table.erb +56 -0
  14. data/engine/app/views/layouts/good_job/base.html.erb +9 -3
  15. data/engine/config/routes.rb +2 -1
  16. data/lib/good_job/active_job_extensions/concurrency.rb +6 -6
  17. data/lib/good_job/adapter.rb +8 -8
  18. data/lib/good_job/cron_manager.rb +3 -3
  19. data/lib/good_job/{current_execution.rb → current_thread.rb} +8 -8
  20. data/lib/good_job/execution.rb +308 -0
  21. data/lib/good_job/job.rb +6 -294
  22. data/lib/good_job/job_performer.rb +2 -2
  23. data/lib/good_job/log_subscriber.rb +4 -4
  24. data/lib/good_job/notifier.rb +3 -3
  25. data/lib/good_job/railtie.rb +2 -2
  26. data/lib/good_job/scheduler.rb +2 -2
  27. data/lib/good_job/version.rb +1 -1
  28. data/lib/good_job.rb +2 -2
  29. metadata +8 -5
  30. data/engine/app/controllers/good_job/jobs_controller.rb +0 -10
  31. 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
- # Job configuration to be scheduled
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
- CurrentExecution.reset
94
- CurrentExecution.cron_key = thr_cron_key
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 CurrentExecution
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] good_job
26
+ # @!attribute [rw] executions
27
27
  # @!scope class
28
- # Cron Key
29
- # @return [GoodJob::Job, nil]
30
- thread_mattr_accessor :good_job
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.good_job = nil
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::Job
41
+ # @return [String] UUID of the currently executing GoodJob::Execution
42
42
  def self.active_job_id
43
- good_job&.active_job_id
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
- # ActiveRecord model that represents an +ActiveJob+ job.
4
- # Parent class can be configured with +GoodJob.active_record_parent_class+.
5
- # @!parse
6
- # class Job < ActiveRecord::Base; end
7
- class Job < Object.const_get(GoodJob.active_record_parent_class)
8
- include Lockable
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