good_job 2.99.0 → 3.0.2
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 +53 -1
- data/README.md +38 -26
- data/app/charts/good_job/scheduled_by_queue_chart.rb +1 -1
- data/app/controllers/good_job/jobs_controller.rb +11 -11
- data/app/controllers/good_job/processes_controller.rb +1 -1
- data/app/filters/good_job/jobs_filter.rb +2 -2
- data/app/views/good_job/processes/index.html.erb +1 -9
- data/app/views/layouts/good_job/application.html.erb +1 -1
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +9 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +20 -0
- data/lib/good_job/adapter.rb +6 -49
- data/lib/good_job/configuration.rb +4 -4
- data/lib/good_job/current_thread.rb +2 -2
- data/lib/good_job/notifier/process_registration.rb +0 -4
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +7 -9
- data/lib/models/good_job/active_job_job.rb +6 -219
- data/lib/models/good_job/cron_entry.rb +3 -3
- data/lib/models/good_job/execution.rb +4 -11
- data/lib/models/good_job/job.rb +219 -6
- data/lib/models/good_job/process.rb +0 -9
- metadata +8 -12
- data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +0 -14
- data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +0 -20
- data/lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb +0 -17
- data/lib/generators/good_job/templates/update/migrations/04_index_good_job_jobs_on_finished_at.rb.erb +0 -25
@@ -1,224 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
include Lockable
|
10
|
-
|
11
|
-
# Raised when an inappropriate action is applied to a Job based on its state.
|
12
|
-
ActionForStateMismatchError = Class.new(StandardError)
|
13
|
-
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
14
|
-
AdapterNotGoodJobError = Class.new(StandardError)
|
15
|
-
# Attached to a Job's Execution when the Job is discarded.
|
16
|
-
DiscardJobError = Class.new(StandardError)
|
17
|
-
|
18
|
-
class << self
|
19
|
-
delegate :table_name, to: GoodJob::Execution
|
20
|
-
|
21
|
-
def table_name=(_value)
|
22
|
-
raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
self.primary_key = 'active_job_id'
|
27
|
-
self.advisory_lockable_column = 'active_job_id'
|
28
|
-
|
29
|
-
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job
|
30
|
-
|
31
|
-
# Only the most-recent unretried execution represents a "Job"
|
32
|
-
default_scope { where(retried_good_job_id: nil) }
|
33
|
-
|
34
|
-
# Get Jobs with given class name
|
35
|
-
# @!method job_class
|
36
|
-
# @!scope class
|
37
|
-
# @param string [String] Execution class name
|
38
|
-
# @return [ActiveRecord::Relation]
|
39
|
-
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
40
|
-
|
41
|
-
# Get Jobs finished before the given timestamp.
|
42
|
-
# @!method finished_before(timestamp)
|
43
|
-
# @!scope class
|
44
|
-
# @param timestamp (DateTime, Time)
|
45
|
-
# @return [ActiveRecord::Relation]
|
46
|
-
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(timestamp)) }
|
47
|
-
|
48
|
-
# First execution will run in the future
|
49
|
-
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
50
|
-
# Execution errored, will run in the future
|
51
|
-
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
52
|
-
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
53
|
-
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
54
|
-
# Advisory locked and executing
|
55
|
-
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
56
|
-
# Completed executing successfully
|
57
|
-
scope :finished, -> { not_discarded.where.not(finished_at: nil) }
|
58
|
-
# Errored but will not be retried
|
59
|
-
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
60
|
-
# Not errored
|
61
|
-
scope :not_discarded, -> { where(error: nil) }
|
62
|
-
|
63
|
-
# The job's ActiveJob UUID
|
64
|
-
# @return [String]
|
65
|
-
def id
|
66
|
-
active_job_id
|
67
|
-
end
|
68
|
-
|
69
|
-
# The ActiveJob job class, as a string
|
70
|
-
# @return [String]
|
71
|
-
def job_class
|
72
|
-
serialized_params['job_class']
|
73
|
-
end
|
74
|
-
|
75
|
-
# The status of the Job, based on the state of its most recent execution.
|
76
|
-
# @return [Symbol]
|
77
|
-
delegate :status, :last_status_at, to: :head_execution
|
78
|
-
|
79
|
-
# This job's most recent {Execution}
|
80
|
-
# @param reload [Booelan] whether to reload executions
|
81
|
-
# @return [Execution]
|
82
|
-
def head_execution(reload: false)
|
83
|
-
executions.reload if reload
|
84
|
-
executions.load # memoize the results
|
85
|
-
executions.last
|
86
|
-
end
|
87
|
-
|
88
|
-
# This job's initial/oldest {Execution}
|
89
|
-
# @return [Execution]
|
90
|
-
def tail_execution
|
91
|
-
executions.first
|
92
|
-
end
|
93
|
-
|
94
|
-
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
95
|
-
# @return [Numeric]
|
96
|
-
def executions_count
|
97
|
-
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
98
|
-
# The execution count within serialized_params is not updated
|
99
|
-
# once the underlying execution has been executed.
|
100
|
-
if status.in? [:discarded, :finished, :running]
|
101
|
-
aj_count + 1
|
102
|
-
else
|
103
|
-
aj_count
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
108
|
-
# @return [Numeric]
|
109
|
-
def preserved_executions_count
|
110
|
-
executions.size
|
111
|
-
end
|
112
|
-
|
113
|
-
# The most recent error message.
|
114
|
-
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
115
|
-
# @return [String]
|
116
|
-
def recent_error
|
117
|
-
head_execution.error || executions[-2]&.error
|
118
|
-
end
|
119
|
-
|
120
|
-
# Tests whether the job is being executed right now.
|
121
|
-
# @return [Boolean]
|
122
|
-
def running?
|
123
|
-
# Avoid N+1 Query: `.includes_advisory_locks`
|
124
|
-
if has_attribute?(:locktype)
|
125
|
-
self['locktype'].present?
|
126
|
-
else
|
127
|
-
advisory_locked?
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Retry a job that has errored and been discarded.
|
132
|
-
# This action will create a new {Execution} record for the job.
|
133
|
-
# @return [ActiveJob::Base]
|
134
|
-
def retry_job
|
135
|
-
with_advisory_lock do
|
136
|
-
execution = head_execution(reload: true)
|
137
|
-
active_job = execution.active_job
|
138
|
-
|
139
|
-
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
140
|
-
raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
|
141
|
-
|
142
|
-
# Update the executions count because the previous execution will not have been preserved
|
143
|
-
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
144
|
-
active_job.executions = (active_job.executions || 0) + 1
|
145
|
-
|
146
|
-
new_active_job = nil
|
147
|
-
GoodJob::CurrentThread.within do |current_thread|
|
148
|
-
current_thread.execution = execution
|
149
|
-
|
150
|
-
execution.class.transaction(joinable: false, requires_new: true) do
|
151
|
-
new_active_job = active_job.retry_job(wait: 0, error: execution.error)
|
152
|
-
execution.save
|
153
|
-
end
|
154
|
-
end
|
155
|
-
new_active_job
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# Discard a job so that it will not be executed further.
|
160
|
-
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
161
|
-
# @return [void]
|
162
|
-
def discard_job(message)
|
163
|
-
with_advisory_lock do
|
164
|
-
execution = head_execution(reload: true)
|
165
|
-
active_job = execution.active_job
|
166
|
-
|
167
|
-
raise ActionForStateMismatchError if execution.finished_at.present?
|
168
|
-
|
169
|
-
job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
|
170
|
-
|
171
|
-
update_execution = proc do
|
172
|
-
execution.update(
|
173
|
-
finished_at: Time.current,
|
174
|
-
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
175
|
-
)
|
176
|
-
end
|
177
|
-
|
178
|
-
if active_job.respond_to?(:instrument)
|
179
|
-
active_job.send :instrument, :discard, error: job_error, &update_execution
|
180
|
-
else
|
181
|
-
update_execution.call
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
187
|
-
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
188
|
-
# @return [void]
|
189
|
-
def reschedule_job(scheduled_at = Time.current)
|
190
|
-
with_advisory_lock do
|
191
|
-
execution = head_execution(reload: true)
|
192
|
-
|
193
|
-
raise ActionForStateMismatchError if execution.finished_at.present?
|
194
|
-
|
195
|
-
execution = head_execution(reload: true)
|
196
|
-
execution.update(scheduled_at: scheduled_at)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Destroy all of a discarded or finished job's executions from the database so that it will no longer appear on the dashboard.
|
201
|
-
# @return [void]
|
202
|
-
def destroy_job
|
203
|
-
with_advisory_lock do
|
204
|
-
execution = head_execution(reload: true)
|
205
|
-
|
206
|
-
raise ActionForStateMismatchError if execution.finished_at.blank?
|
207
|
-
|
208
|
-
destroy
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
# Utility method to determine which execution record is used to represent this job
|
213
|
-
# @return [String]
|
214
|
-
def _execution_id
|
215
|
-
attributes['id']
|
216
|
-
end
|
217
|
-
|
218
|
-
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
219
|
-
# @return [Boolean]
|
220
|
-
def _head?
|
221
|
-
_execution_id == head_execution(reload: true).id
|
3
|
+
# @deprecated Use {GoodJob::Job} instead.
|
4
|
+
class ActiveJobJob < Execution
|
5
|
+
after_initialize do |_job|
|
6
|
+
ActiveSupport::Deprecation.warn(
|
7
|
+
"The `GoodJob::ActiveJobJob` class name is deprecated. Replace with `GoodJob::Job`."
|
8
|
+
)
|
222
9
|
end
|
223
10
|
end
|
224
11
|
end
|
@@ -73,13 +73,13 @@ module GoodJob # :nodoc:
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def jobs
|
76
|
-
GoodJob::
|
76
|
+
GoodJob::Job.where(cron_key: key)
|
77
77
|
end
|
78
78
|
|
79
79
|
def last_at
|
80
80
|
return if last_job.blank?
|
81
81
|
|
82
|
-
if GoodJob::
|
82
|
+
if GoodJob::Job.column_names.include?('cron_at')
|
83
83
|
(last_job.cron_at || last_job.created_at).localtime
|
84
84
|
else
|
85
85
|
last_job.created_at
|
@@ -99,7 +99,7 @@ module GoodJob # :nodoc:
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def last_job
|
102
|
-
if GoodJob::
|
102
|
+
if GoodJob::Job.column_names.include?('cron_at')
|
103
103
|
jobs.order("cron_at DESC NULLS LAST").first
|
104
104
|
else
|
105
105
|
jobs.order(created_at: :asc).last
|
@@ -51,7 +51,7 @@ module GoodJob
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
belongs_to :job, class_name: 'GoodJob::
|
54
|
+
belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
|
55
55
|
|
56
56
|
# Get Jobs with given ActiveJob ID
|
57
57
|
# @!method active_job_id
|
@@ -207,14 +207,7 @@ module GoodJob
|
|
207
207
|
|
208
208
|
if CurrentThread.cron_key
|
209
209
|
execution_args[:cron_key] = CurrentThread.cron_key
|
210
|
-
|
211
|
-
@cron_at_index = column_names.include?('cron_at') && connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) unless instance_variable_defined?(:@cron_at_index)
|
212
|
-
|
213
|
-
if @cron_at_index
|
214
|
-
execution_args[:cron_at] = CurrentThread.cron_at
|
215
|
-
else
|
216
|
-
migration_pending_warning!
|
217
|
-
end
|
210
|
+
execution_args[:cron_at] = CurrentThread.cron_at
|
218
211
|
elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
|
219
212
|
execution_args[:cron_key] = CurrentThread.execution.cron_key
|
220
213
|
end
|
@@ -337,6 +330,7 @@ module GoodJob
|
|
337
330
|
serialized_params.deep_dup
|
338
331
|
.tap do |job_data|
|
339
332
|
job_data["provider_job_id"] = id
|
333
|
+
job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
|
340
334
|
end
|
341
335
|
end
|
342
336
|
|
@@ -346,8 +340,7 @@ module GoodJob
|
|
346
340
|
current_thread.reset
|
347
341
|
current_thread.execution = self
|
348
342
|
|
349
|
-
|
350
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
|
343
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
|
351
344
|
value = ActiveJob::Base.execute(active_job_data)
|
352
345
|
|
353
346
|
if value.is_a?(Exception)
|
data/lib/models/good_job/job.rb
CHANGED
@@ -1,11 +1,224 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# There is not a table in the database whose discrete rows represents "Jobs".
|
5
|
+
# The +good_jobs+ table is a table of individual {GoodJob::Execution}s that share the same +active_job_id+.
|
6
|
+
# A single row from the +good_jobs+ table of executions is fetched to represent an Job
|
7
|
+
class Job < BaseRecord
|
8
|
+
include Filterable
|
9
|
+
include Lockable
|
10
|
+
|
11
|
+
# Raised when an inappropriate action is applied to a Job based on its state.
|
12
|
+
ActionForStateMismatchError = Class.new(StandardError)
|
13
|
+
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
14
|
+
AdapterNotGoodJobError = Class.new(StandardError)
|
15
|
+
# Attached to a Job's Execution when the Job is discarded.
|
16
|
+
DiscardJobError = Class.new(StandardError)
|
17
|
+
|
18
|
+
class << self
|
19
|
+
delegate :table_name, to: GoodJob::Execution
|
20
|
+
|
21
|
+
def table_name=(_value)
|
22
|
+
raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
self.primary_key = 'active_job_id'
|
27
|
+
self.advisory_lockable_column = 'active_job_id'
|
28
|
+
|
29
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job
|
30
|
+
|
31
|
+
# Only the most-recent unretried execution represents a "Job"
|
32
|
+
default_scope { where(retried_good_job_id: nil) }
|
33
|
+
|
34
|
+
# Get Jobs with given class name
|
35
|
+
# @!method job_class
|
36
|
+
# @!scope class
|
37
|
+
# @param string [String] Execution class name
|
38
|
+
# @return [ActiveRecord::Relation]
|
39
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
40
|
+
|
41
|
+
# Get Jobs finished before the given timestamp.
|
42
|
+
# @!method finished_before(timestamp)
|
43
|
+
# @!scope class
|
44
|
+
# @param timestamp (DateTime, Time)
|
45
|
+
# @return [ActiveRecord::Relation]
|
46
|
+
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(timestamp)) }
|
47
|
+
|
48
|
+
# First execution will run in the future
|
49
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
50
|
+
# Execution errored, will run in the future
|
51
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
52
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
53
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
54
|
+
# Advisory locked and executing
|
55
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
56
|
+
# Completed executing successfully
|
57
|
+
scope :finished, -> { not_discarded.where.not(finished_at: nil) }
|
58
|
+
# Errored but will not be retried
|
59
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
60
|
+
# Not errored
|
61
|
+
scope :not_discarded, -> { where(error: nil) }
|
62
|
+
|
63
|
+
# The job's ActiveJob UUID
|
64
|
+
# @return [String]
|
65
|
+
def id
|
66
|
+
active_job_id
|
67
|
+
end
|
68
|
+
|
69
|
+
# The ActiveJob job class, as a string
|
70
|
+
# @return [String]
|
71
|
+
def job_class
|
72
|
+
serialized_params['job_class']
|
73
|
+
end
|
74
|
+
|
75
|
+
# The status of the Job, based on the state of its most recent execution.
|
76
|
+
# @return [Symbol]
|
77
|
+
delegate :status, :last_status_at, to: :head_execution
|
78
|
+
|
79
|
+
# This job's most recent {Execution}
|
80
|
+
# @param reload [Booelan] whether to reload executions
|
81
|
+
# @return [Execution]
|
82
|
+
def head_execution(reload: false)
|
83
|
+
executions.reload if reload
|
84
|
+
executions.load # memoize the results
|
85
|
+
executions.last
|
86
|
+
end
|
87
|
+
|
88
|
+
# This job's initial/oldest {Execution}
|
89
|
+
# @return [Execution]
|
90
|
+
def tail_execution
|
91
|
+
executions.first
|
92
|
+
end
|
93
|
+
|
94
|
+
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
95
|
+
# @return [Numeric]
|
96
|
+
def executions_count
|
97
|
+
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
98
|
+
# The execution count within serialized_params is not updated
|
99
|
+
# once the underlying execution has been executed.
|
100
|
+
if status.in? [:discarded, :finished, :running]
|
101
|
+
aj_count + 1
|
102
|
+
else
|
103
|
+
aj_count
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
108
|
+
# @return [Numeric]
|
109
|
+
def preserved_executions_count
|
110
|
+
executions.size
|
111
|
+
end
|
112
|
+
|
113
|
+
# The most recent error message.
|
114
|
+
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
115
|
+
# @return [String]
|
116
|
+
def recent_error
|
117
|
+
head_execution.error || executions[-2]&.error
|
118
|
+
end
|
119
|
+
|
120
|
+
# Tests whether the job is being executed right now.
|
121
|
+
# @return [Boolean]
|
122
|
+
def running?
|
123
|
+
# Avoid N+1 Query: `.includes_advisory_locks`
|
124
|
+
if has_attribute?(:locktype)
|
125
|
+
self['locktype'].present?
|
126
|
+
else
|
127
|
+
advisory_locked?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Retry a job that has errored and been discarded.
|
132
|
+
# This action will create a new {Execution} record for the job.
|
133
|
+
# @return [ActiveJob::Base]
|
134
|
+
def retry_job
|
135
|
+
with_advisory_lock do
|
136
|
+
execution = head_execution(reload: true)
|
137
|
+
active_job = execution.active_job
|
138
|
+
|
139
|
+
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
140
|
+
raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
|
141
|
+
|
142
|
+
# Update the executions count because the previous execution will not have been preserved
|
143
|
+
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
144
|
+
active_job.executions = (active_job.executions || 0) + 1
|
145
|
+
|
146
|
+
new_active_job = nil
|
147
|
+
GoodJob::CurrentThread.within do |current_thread|
|
148
|
+
current_thread.execution = execution
|
149
|
+
|
150
|
+
execution.class.transaction(joinable: false, requires_new: true) do
|
151
|
+
new_active_job = active_job.retry_job(wait: 0, error: execution.error)
|
152
|
+
execution.save
|
153
|
+
end
|
154
|
+
end
|
155
|
+
new_active_job
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Discard a job so that it will not be executed further.
|
160
|
+
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
161
|
+
# @return [void]
|
162
|
+
def discard_job(message)
|
163
|
+
with_advisory_lock do
|
164
|
+
execution = head_execution(reload: true)
|
165
|
+
active_job = execution.active_job
|
166
|
+
|
167
|
+
raise ActionForStateMismatchError if execution.finished_at.present?
|
168
|
+
|
169
|
+
job_error = GoodJob::Job::DiscardJobError.new(message)
|
170
|
+
|
171
|
+
update_execution = proc do
|
172
|
+
execution.update(
|
173
|
+
finished_at: Time.current,
|
174
|
+
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
if active_job.respond_to?(:instrument)
|
179
|
+
active_job.send :instrument, :discard, error: job_error, &update_execution
|
180
|
+
else
|
181
|
+
update_execution.call
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
187
|
+
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
188
|
+
# @return [void]
|
189
|
+
def reschedule_job(scheduled_at = Time.current)
|
190
|
+
with_advisory_lock do
|
191
|
+
execution = head_execution(reload: true)
|
192
|
+
|
193
|
+
raise ActionForStateMismatchError if execution.finished_at.present?
|
194
|
+
|
195
|
+
execution = head_execution(reload: true)
|
196
|
+
execution.update(scheduled_at: scheduled_at)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Destroy all of a discarded or finished job's executions from the database so that it will no longer appear on the dashboard.
|
201
|
+
# @return [void]
|
202
|
+
def destroy_job
|
203
|
+
with_advisory_lock do
|
204
|
+
execution = head_execution(reload: true)
|
205
|
+
|
206
|
+
raise ActionForStateMismatchError if execution.finished_at.blank?
|
207
|
+
|
208
|
+
destroy
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Utility method to determine which execution record is used to represent this job
|
213
|
+
# @return [String]
|
214
|
+
def _execution_id
|
215
|
+
attributes['id']
|
216
|
+
end
|
217
|
+
|
218
|
+
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
219
|
+
# @return [Boolean]
|
220
|
+
def _head?
|
221
|
+
_execution_id == head_execution(reload: true).id
|
9
222
|
end
|
10
223
|
end
|
11
224
|
end
|
@@ -25,15 +25,6 @@ module GoodJob # :nodoc:
|
|
25
25
|
# @return [ActiveRecord::Relation]
|
26
26
|
scope :inactive, -> { advisory_unlocked }
|
27
27
|
|
28
|
-
# Whether the +good_job_processes+ table exsists.
|
29
|
-
# @return [Boolean]
|
30
|
-
def self.migrated?
|
31
|
-
return true if connection.table_exists?(table_name)
|
32
|
-
|
33
|
-
migration_pending_warning!
|
34
|
-
false
|
35
|
-
end
|
36
|
-
|
37
28
|
# UUID that is unique to the current process and changes when forked.
|
38
29
|
# @return [String]
|
39
30
|
def self.current_id
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 6.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 6.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: concurrent-ruby
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 6.0.0
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 6.0.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: thor
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -414,10 +414,6 @@ files:
|
|
414
414
|
- lib/generators/good_job/install_generator.rb
|
415
415
|
- lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb
|
416
416
|
- lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb
|
417
|
-
- lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb
|
418
|
-
- lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb
|
419
|
-
- lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb
|
420
|
-
- lib/generators/good_job/templates/update/migrations/04_index_good_job_jobs_on_finished_at.rb.erb
|
421
417
|
- lib/generators/good_job/update_generator.rb
|
422
418
|
- lib/good_job.rb
|
423
419
|
- lib/good_job/active_job_extensions.rb
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class AddCronAtToGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
3
|
-
def change
|
4
|
-
reversible do |dir|
|
5
|
-
dir.up do
|
6
|
-
# Ensure this incremental update migration is idempotent
|
7
|
-
# with monolithic install migration.
|
8
|
-
return if connection.column_exists?(:good_jobs, :cron_at)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
add_column :good_jobs, :cron_at, :timestamp
|
13
|
-
end
|
14
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class AddCronKeyCronAtIndexToGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
3
|
-
disable_ddl_transaction!
|
4
|
-
|
5
|
-
def change
|
6
|
-
reversible do |dir|
|
7
|
-
dir.up do
|
8
|
-
# Ensure this incremental update migration is idempotent
|
9
|
-
# with monolithic install migration.
|
10
|
-
return if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
add_index :good_jobs,
|
15
|
-
[:cron_key, :cron_at],
|
16
|
-
algorithm: :concurrently,
|
17
|
-
name: :index_good_jobs_on_cron_key_and_cron_at,
|
18
|
-
unique: true
|
19
|
-
end
|
20
|
-
end
|
data/lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class CreateGoodJobProcesses < ActiveRecord::Migration<%= migration_version %>
|
3
|
-
def change
|
4
|
-
reversible do |dir|
|
5
|
-
dir.up do
|
6
|
-
# Ensure this incremental update migration is idempotent
|
7
|
-
# with monolithic install migration.
|
8
|
-
return if connection.table_exists?(:good_job_processes)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
create_table :good_job_processes, id: :uuid do |t|
|
13
|
-
t.timestamps
|
14
|
-
t.jsonb :state
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|