good_job 1.9.0 → 1.9.5

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.
data/lib/good_job/cli.rb CHANGED
@@ -83,7 +83,7 @@ module GoodJob
83
83
 
84
84
  notifier = GoodJob::Notifier.new
85
85
  poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
86
- scheduler = GoodJob::Scheduler.from_configuration(configuration)
86
+ scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true)
87
87
  notifier.recipients << [scheduler, :create_thread]
88
88
  poller.recipients << [scheduler, :create_thread]
89
89
 
@@ -84,9 +84,9 @@ module GoodJob
84
84
  # on the format of this string.
85
85
  # @return [String]
86
86
  def queue_string
87
- options[:queues] ||
88
- rails_config[:queues] ||
89
- env['GOOD_JOB_QUEUES'] ||
87
+ options[:queues].presence ||
88
+ rails_config[:queues].presence ||
89
+ env['GOOD_JOB_QUEUES'].presence ||
90
90
  '*'
91
91
  end
92
92
 
@@ -3,7 +3,6 @@ require 'active_support/core_ext/module/attribute_accessors_per_thread'
3
3
  module GoodJob
4
4
  # Thread-local attributes for passing values from Instrumentation.
5
5
  # (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
6
-
7
6
  module CurrentExecution
8
7
  # @!attribute [rw] error_on_retry
9
8
  # @!scope class
@@ -13,6 +13,7 @@ module GoodJob
13
13
  end
14
14
 
15
15
  # Daemonizes the current process and writes out a pidfile.
16
+ # @return [void]
16
17
  def daemonize
17
18
  check_pid
18
19
  Process.daemon
@@ -21,6 +22,7 @@ module GoodJob
21
22
 
22
23
  private
23
24
 
25
+ # @return [void]
24
26
  def write_pid
25
27
  File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
26
28
  at_exit { File.delete(pidfile) if File.exist?(pidfile) }
@@ -29,10 +31,12 @@ module GoodJob
29
31
  retry
30
32
  end
31
33
 
34
+ # @return [void]
32
35
  def delete_pid
33
36
  File.delete(pidfile) if File.exist?(pidfile)
34
37
  end
35
38
 
39
+ # @return [void]
36
40
  def check_pid
37
41
  case pid_status(pidfile)
38
42
  when :running, :not_owned
@@ -42,6 +46,8 @@ module GoodJob
42
46
  end
43
47
  end
44
48
 
49
+ # @param pidfile [Pathname, String]
50
+ # @return [Symbol]
45
51
  def pid_status(pidfile)
46
52
  return :exited unless File.exist?(pidfile)
47
53
 
@@ -0,0 +1,20 @@
1
+ module GoodJob
2
+ # Stores the results of job execution
3
+ class ExecutionResult
4
+ # @return [Object, nil]
5
+ attr_reader :value
6
+ # @return [Exception, nil]
7
+ attr_reader :handled_error
8
+ # @return [Exception, nil]
9
+ attr_reader :unhandled_error
10
+
11
+ # @param value [Object, nil]
12
+ # @param handled_error [Exception, nil]
13
+ # @param unhandled_error [Exception, nil]
14
+ def initialize(value:, handled_error: nil, unhandled_error: nil)
15
+ @value = value
16
+ @handled_error = handled_error
17
+ @unhandled_error = unhandled_error
18
+ end
19
+ end
20
+ end
data/lib/good_job/job.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  module GoodJob
2
- #
3
- # Represents a request to perform an +ActiveJob+ job.
4
- #
5
- class Job < ActiveRecord::Base
2
+ # ActiveRecord model that represents an +ActiveJob+ job.
3
+ # Parent class can be configured with +GoodJob.active_record_parent_class+.
4
+ # @!parse
5
+ # class Job < ActiveRecord::Base; end
6
+ class Job < Object.const_get(GoodJob.active_record_parent_class)
6
7
  include Lockable
7
8
 
8
9
  # Raised if something attempts to execute a previously completed Job again.
@@ -19,6 +20,7 @@ module GoodJob
19
20
 
20
21
  # Parse a string representing a group of queues into a more readable data
21
22
  # structure.
23
+ # @param string [String] Queue string
22
24
  # @return [Hash]
23
25
  # How to match a given queue. It can have the following keys and values:
24
26
  # - +{ all: true }+ indicates that all queues match.
@@ -48,6 +50,14 @@ module GoodJob
48
50
  end
49
51
  end
50
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
+
51
61
  # Get Jobs that have not yet been completed.
52
62
  # @!method unfinished
53
63
  # @!scope class
@@ -92,6 +102,12 @@ module GoodJob
92
102
  # @return [ActiveRecord::Relation]
93
103
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
94
104
 
105
+ # Get Jobs that started but not finished yet.
106
+ # @!method running
107
+ # @!scope class
108
+ # @return [ActiveRecord::Relation]
109
+ scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
110
+
95
111
  # Get Jobs on queues that match the given queue string.
96
112
  # @!method queue_string(string)
97
113
  # @!scope class
@@ -134,29 +150,26 @@ module GoodJob
134
150
 
135
151
  # Finds the next eligible Job, acquire an advisory lock related to it, and
136
152
  # executes the job.
137
- # @return [Array<(GoodJob::Job, Object, Exception)>, nil]
153
+ # @return [ExecutionResult, nil]
138
154
  # If a job was executed, returns an array with the {Job} record, the
139
155
  # return value for the job's +#perform+ method, and the exception the job
140
156
  # raised, if any (if the job raised, then the second array entry will be
141
157
  # +nil+). If there were no jobs to execute, returns +nil+.
142
158
  def self.perform_with_advisory_lock
143
- good_job = nil
144
- result = nil
145
- error = nil
146
-
147
159
  unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
148
160
  good_job = good_jobs.first
149
161
  # TODO: Determine why some records are fetched without an advisory lock at all
150
162
  break unless good_job&.executable?
151
163
 
152
- result, error = good_job.perform
164
+ good_job.perform
153
165
  end
154
-
155
- [good_job, result, error] if good_job
156
166
  end
157
167
 
158
168
  # Fetches the scheduled execution time of the next eligible Job(s).
159
- # @return [Array<(DateTime)>]
169
+ # @param after [DateTime]
170
+ # @param limit [Integer]
171
+ # @param now_limit [Integer, nil]
172
+ # @return [Array<DateTime>]
160
173
  def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
161
174
  query = advisory_unlocked.unfinished.schedule_ordered
162
175
 
@@ -182,7 +195,6 @@ module GoodJob
182
195
  # @return [Job]
183
196
  # The new {Job} instance representing the queued ActiveJob job.
184
197
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
185
- good_job = nil
186
198
  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|
187
199
  good_job = GoodJob::Job.new(
188
200
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
@@ -196,49 +208,37 @@ module GoodJob
196
208
 
197
209
  good_job.save!
198
210
  active_job.provider_job_id = good_job.id
199
- end
200
211
 
201
- good_job
212
+ good_job
213
+ end
202
214
  end
203
215
 
204
216
  # Execute the ActiveJob job this {Job} represents.
205
- # @return [Array<(Object, Exception)>]
217
+ # @return [ExecutionResult]
206
218
  # An array of the return value of the job's +#perform+ method and the
207
219
  # exception raised by the job, if any. If the job completed successfully,
208
220
  # the second array entry (the exception) will be +nil+ and vice versa.
209
221
  def perform
210
222
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
211
223
 
212
- GoodJob::CurrentExecution.reset
213
-
214
224
  self.performed_at = Time.current
215
225
  save! if GoodJob.preserve_job_records
216
226
 
217
- result, unhandled_error = execute
218
-
219
- result_error = nil
220
- if result.is_a?(Exception)
221
- result_error = result
222
- result = nil
223
- end
224
-
225
- job_error = unhandled_error ||
226
- result_error ||
227
- GoodJob::CurrentExecution.error_on_retry ||
228
- GoodJob::CurrentExecution.error_on_discard
227
+ result = execute
229
228
 
229
+ job_error = result.handled_error || result.unhandled_error
230
230
  self.error = "#{job_error.class}: #{job_error.message}" if job_error
231
231
 
232
- if unhandled_error && GoodJob.retry_on_unhandled_error
232
+ if result.unhandled_error && GoodJob.retry_on_unhandled_error
233
233
  save!
234
- elsif GoodJob.preserve_job_records == true || (unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
234
+ elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
235
235
  self.finished_at = Time.current
236
236
  save!
237
237
  else
238
238
  destroy!
239
239
  end
240
240
 
241
- [result, job_error]
241
+ result
242
242
  end
243
243
 
244
244
  # Tests whether this job is safe to be executed by this thread.
@@ -249,16 +249,26 @@ module GoodJob
249
249
 
250
250
  private
251
251
 
252
+ # @return [ExecutionResult]
252
253
  def execute
253
254
  params = serialized_params.merge(
254
255
  "provider_job_id" => id
255
256
  )
256
257
 
258
+ GoodJob::CurrentExecution.reset
257
259
  ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
258
- [ActiveJob::Base.execute(params), nil]
260
+ value = ActiveJob::Base.execute(params)
261
+
262
+ if value.is_a?(Exception)
263
+ handled_error = value
264
+ value = nil
265
+ end
266
+ handled_error ||= GoodJob::CurrentExecution.error_on_retry || GoodJob::CurrentExecution.error_on_discard
267
+
268
+ ExecutionResult.new(value: value, handled_error: handled_error)
269
+ rescue StandardError => e
270
+ ExecutionResult.new(value: nil, unhandled_error: e)
259
271
  end
260
- rescue StandardError => e
261
- [nil, e]
262
272
  end
263
273
  end
264
274
  end
@@ -24,7 +24,7 @@ module GoodJob
24
24
  end
25
25
 
26
26
  # Perform the next eligible job
27
- # @return [nil, Object] Returns job result or +nil+ if no job was found
27
+ # @return [Object, nil] Returns job result or +nil+ if no job was found
28
28
  def next
29
29
  job_query.perform_with_advisory_lock
30
30
  end
@@ -54,7 +54,7 @@ module GoodJob
54
54
  # @param after [DateTime, Time, nil] future jobs scheduled after this time
55
55
  # @param limit [Integer] number of future timestamps to return
56
56
  # @param now_limit [Integer] number of past timestamps to return
57
- # @return [Array<(Time, DateTime)>, nil]
57
+ # @return [Array<DateTime, Time>, nil]
58
58
  def next_at(after: nil, limit: nil, now_limit: nil)
59
59
  job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
60
60
  end
@@ -143,7 +143,7 @@ module GoodJob
143
143
  def supports_cte_materialization_specifiers?
144
144
  return @_supports_cte_materialization_specifiers if defined?(@_supports_cte_materialization_specifiers)
145
145
 
146
- @_supports_cte_materialization_specifiers = ActiveRecord::Base.connection.postgresql_version >= 120000
146
+ @_supports_cte_materialization_specifiers = connection.postgresql_version >= 120000
147
147
  end
148
148
  end
149
149
 
@@ -158,7 +158,7 @@ module GoodJob
158
158
  WHERE pg_try_advisory_lock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
159
159
  SQL
160
160
  binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
161
- ActiveRecord::Base.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
161
+ self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
162
162
  end
163
163
 
164
164
  # Releases an advisory lock on this record if it is locked by this database
@@ -245,11 +245,8 @@ module GoodJob
245
245
 
246
246
  private
247
247
 
248
- def sanitize_sql_for_conditions(*args)
249
- # Made public in Rails 5.2
250
- self.class.send(:sanitize_sql_for_conditions, *args)
251
- end
252
-
248
+ # @param query [String]
249
+ # @return [Boolean]
253
250
  def pg_or_jdbc_query(query)
254
251
  if Concurrent.on_jruby?
255
252
  # Replace $1 bind parameters with ?
@@ -14,6 +14,7 @@ module GoodJob
14
14
 
15
15
  # @!macro notification_responder
16
16
  # Responds to the +$0.good_job+ notification.
17
+ # @param event [ActiveSupport::Notifications::Event]
17
18
  # @return [void]
18
19
  def create(event)
19
20
  # FIXME: This method does not match any good_job notifications.
@@ -24,7 +25,7 @@ module GoodJob
24
25
  end
25
26
  end
26
27
 
27
- # @macro notification_responder
28
+ # @!macro notification_responder
28
29
  def finished_timer_task(event)
29
30
  exception = event.payload[:error]
30
31
  return unless exception
@@ -34,7 +35,7 @@ module GoodJob
34
35
  end
35
36
  end
36
37
 
37
- # @macro notification_responder
38
+ # @!macro notification_responder
38
39
  def finished_job_task(event)
39
40
  exception = event.payload[:error]
40
41
  return unless exception
@@ -44,7 +45,7 @@ module GoodJob
44
45
  end
45
46
  end
46
47
 
47
- # @macro notification_responder
48
+ # @!macro notification_responder
48
49
  def scheduler_create_pool(event)
49
50
  max_threads = event.payload[:max_threads]
50
51
  performer_name = event.payload[:performer_name]
@@ -55,7 +56,7 @@ module GoodJob
55
56
  end
56
57
  end
57
58
 
58
- # @macro notification_responder
59
+ # @!macro notification_responder
59
60
  def scheduler_shutdown_start(event)
60
61
  process_id = event.payload[:process_id]
61
62
 
@@ -64,7 +65,7 @@ module GoodJob
64
65
  end
65
66
  end
66
67
 
67
- # @macro notification_responder
68
+ # @!macro notification_responder
68
69
  def scheduler_shutdown(event)
69
70
  process_id = event.payload[:process_id]
70
71
 
@@ -73,7 +74,7 @@ module GoodJob
73
74
  end
74
75
  end
75
76
 
76
- # @macro notification_responder
77
+ # @!macro notification_responder
77
78
  def scheduler_restart_pools(event)
78
79
  process_id = event.payload[:process_id]
79
80
 
@@ -82,7 +83,7 @@ module GoodJob
82
83
  end
83
84
  end
84
85
 
85
- # @macro notification_responder
86
+ # @!macro notification_responder
86
87
  def perform_job(event)
87
88
  good_job = event.payload[:good_job]
88
89
  process_id = event.payload[:process_id]
@@ -93,14 +94,14 @@ module GoodJob
93
94
  end
94
95
  end
95
96
 
96
- # @macro notification_responder
97
- def notifier_listen(_event)
97
+ # @!macro notification_responder
98
+ def notifier_listen(event) # rubocop:disable Lint/UnusedMethodArgument
98
99
  info do
99
100
  "Notifier subscribed with LISTEN"
100
101
  end
101
102
  end
102
103
 
103
- # @macro notification_responder
104
+ # @!macro notification_responder
104
105
  def notifier_notified(event)
105
106
  payload = event.payload[:payload]
106
107
 
@@ -109,7 +110,7 @@ module GoodJob
109
110
  end
110
111
  end
111
112
 
112
- # @macro notification_responder
113
+ # @!macro notification_responder
113
114
  def notifier_notify_error(event)
114
115
  error = event.payload[:error]
115
116
 
@@ -118,14 +119,14 @@ module GoodJob
118
119
  end
119
120
  end
120
121
 
121
- # @macro notification_responder
122
- def notifier_unlisten(_event)
122
+ # @!macro notification_responder
123
+ def notifier_unlisten(event) # rubocop:disable Lint/UnusedMethodArgument
123
124
  info do
124
125
  "Notifier unsubscribed with UNLISTEN"
125
126
  end
126
127
  end
127
128
 
128
- # @macro notification_responder
129
+ # @!macro notification_responder
129
130
  def cleanup_preserved_jobs(event)
130
131
  timestamp = event.payload[:timestamp]
131
132
  deleted_records_count = event.payload[:deleted_records_count]
@@ -4,31 +4,40 @@ module GoodJob
4
4
  # @return [Array<Scheduler>] List of the scheduler delegates
5
5
  attr_reader :schedulers
6
6
 
7
+ # @param schedulers [Array<Scheduler>]
7
8
  def initialize(schedulers)
8
9
  @schedulers = schedulers
9
10
  end
10
11
 
11
12
  # Delegates to {Scheduler#running?}.
13
+ # @return [Boolean, nil]
12
14
  def running?
13
15
  schedulers.all?(&:running?)
14
16
  end
15
17
 
16
18
  # Delegates to {Scheduler#shutdown?}.
19
+ # @return [Boolean, nil]
17
20
  def shutdown?
18
21
  schedulers.all?(&:shutdown?)
19
22
  end
20
23
 
21
24
  # Delegates to {Scheduler#shutdown}.
25
+ # @param timeout [Numeric, nil]
26
+ # @return [void]
22
27
  def shutdown(timeout: -1)
23
28
  GoodJob._shutdown_all(schedulers, timeout: timeout)
24
29
  end
25
30
 
26
31
  # Delegates to {Scheduler#restart}.
32
+ # @param timeout [Numeric, nil]
33
+ # @return [void]
27
34
  def restart(timeout: -1)
28
35
  GoodJob._shutdown_all(schedulers, :restart, timeout: timeout)
29
36
  end
30
37
 
31
38
  # Delegates to {Scheduler#create_thread}.
39
+ # @param state [Hash]
40
+ # @return [Boolean, nil]
32
41
  def create_thread(state = nil)
33
42
  results = []
34
43