good_job 2.17.1 → 3.0.1

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/README.md +45 -26
  4. data/app/charts/good_job/scheduled_by_queue_chart.rb +1 -1
  5. data/app/controllers/good_job/jobs_controller.rb +11 -11
  6. data/app/controllers/good_job/processes_controller.rb +1 -1
  7. data/app/filters/good_job/jobs_filter.rb +2 -2
  8. data/app/views/good_job/processes/index.html.erb +1 -9
  9. data/app/views/layouts/good_job/application.html.erb +1 -1
  10. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +9 -0
  11. data/lib/good_job/adapter.rb +6 -49
  12. data/lib/good_job/configuration.rb +4 -4
  13. data/lib/good_job/current_thread.rb +2 -2
  14. data/lib/good_job/notifier/process_registration.rb +0 -4
  15. data/lib/good_job/version.rb +1 -1
  16. data/lib/good_job.rb +7 -9
  17. data/lib/models/good_job/active_job_job.rb +6 -219
  18. data/lib/{good_job → models/good_job}/cron_entry.rb +3 -3
  19. data/lib/models/good_job/execution.rb +3 -11
  20. data/lib/models/good_job/job.rb +224 -0
  21. data/lib/{good_job → models/good_job}/lockable.rb +0 -0
  22. data/lib/models/good_job/process.rb +0 -9
  23. metadata +23 -27
  24. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +0 -14
  25. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +0 -20
  26. data/lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb +0 -17
  27. data/lib/generators/good_job/templates/update/migrations/04_index_good_job_jobs_on_finished_at.rb.erb +0 -25
  28. data/lib/good_job/job.rb +0 -11
@@ -1,224 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
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 ActiveJobJob
7
- class ActiveJobJob < 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::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::ActiveJobJob.where(cron_key: key)
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::ActiveJobJob.column_names.include?('cron_at')
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::ActiveJobJob.column_names.include?('cron_at')
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::ActiveJobJob', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
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
@@ -346,8 +339,7 @@ module GoodJob
346
339
  current_thread.reset
347
340
  current_thread.execution = self
348
341
 
349
- # DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
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
342
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
351
343
  value = ActiveJob::Base.execute(active_job_data)
352
344
 
353
345
  if value.is_a?(Exception)
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
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
222
+ end
223
+ end
224
+ end
File without changes
@@ -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