good_job 3.0.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,9 +7,9 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
7
7
  t.text :queue_name
8
8
  t.integer :priority
9
9
  t.jsonb :serialized_params
10
- t.timestamp :scheduled_at
11
- t.timestamp :performed_at
12
- t.timestamp :finished_at
10
+ t.datetime :scheduled_at
11
+ t.datetime :performed_at
12
+ t.datetime :finished_at
13
13
  t.text :error
14
14
 
15
15
  t.timestamps
@@ -18,7 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
18
18
  t.text :concurrency_key
19
19
  t.text :cron_key
20
20
  t.uuid :retried_good_job_id
21
- t.timestamp :cron_at
21
+ t.datetime :cron_at
22
22
  end
23
23
 
24
24
  create_table :good_job_processes, id: :uuid do |t|
@@ -7,9 +7,9 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
7
7
  t.text :queue_name
8
8
  t.integer :priority
9
9
  t.jsonb :serialized_params
10
- t.timestamp :scheduled_at
11
- t.timestamp :performed_at
12
- t.timestamp :finished_at
10
+ t.datetime :scheduled_at
11
+ t.datetime :performed_at
12
+ t.datetime :finished_at
13
13
  t.text :error
14
14
 
15
15
  t.timestamps
@@ -18,7 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
18
18
  t.text :concurrency_key
19
19
  t.text :cron_key
20
20
  t.uuid :retried_good_job_id
21
- t.timestamp :cron_at
21
+ t.datetime :cron_at
22
22
  end
23
23
 
24
24
  create_table :good_job_processes, id: :uuid do |t|
@@ -26,7 +26,7 @@ module GoodJob
26
26
  # @return [void]
27
27
  def write_pid
28
28
  File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(::Process.pid.to_s) }
29
- at_exit { File.delete(pidfile) if File.exist?(pidfile) }
29
+ at_exit { File.delete(pidfile) if File.exist?(pidfile) } # rubocop:disable Lint/NonAtomicFileOperation
30
30
  rescue Errno::EEXIST
31
31
  check_pid
32
32
  retry
@@ -34,7 +34,7 @@ module GoodJob
34
34
 
35
35
  # @return [void]
36
36
  def delete_pid
37
- File.delete(pidfile) if File.exist?(pidfile)
37
+ File.delete(pidfile) if File.exist?(pidfile) # rubocop:disable Lint/NonAtomicFileOperation
38
38
  end
39
39
 
40
40
  # @return [void]
@@ -24,7 +24,7 @@ module GoodJob
24
24
  # Perform the next eligible job
25
25
  # @return [Object, nil] Returns job result or +nil+ if no job was found
26
26
  def next
27
- job_query.perform_with_advisory_lock
27
+ job_query.perform_with_advisory_lock(parsed_queues: parsed_queues)
28
28
  end
29
29
 
30
30
  # Tests whether this performer should be used in GoodJob's current state.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.0.2'
4
+ VERSION = '3.3.0'
5
5
  end
@@ -112,9 +112,11 @@ module GoodJob # :nodoc:
112
112
  class: job_class,
113
113
  cron: schedule,
114
114
  set: display_property(set),
115
- args: display_property(args),
116
115
  description: display_property(description),
117
- }
116
+ }.tap do |properties|
117
+ properties[:args] = display_property(args) if args.present?
118
+ properties[:kwargs] = display_property(kwargs) if kwargs.present?
119
+ end
118
120
  end
119
121
 
120
122
  private
@@ -4,6 +4,7 @@ module GoodJob
4
4
  class Execution < BaseRecord
5
5
  include Lockable
6
6
  include Filterable
7
+ include Reportable
7
8
 
8
9
  # Raised if something attempts to execute a previously completed Execution again.
9
10
  PreviouslyPerformedError = Class.new(StandardError)
@@ -29,15 +30,21 @@ module GoodJob
29
30
  # not match.
30
31
  # - +{ include: Array<String> }+ indicates the listed queue names should
31
32
  # match.
33
+ # - +{ include: Array<String>, ordered_queues: true }+ indicates the listed
34
+ # queue names should match, and dequeue should respect queue order.
32
35
  # @example
33
36
  # GoodJob::Execution.queue_parser('-queue1,queue2')
34
37
  # => { exclude: [ 'queue1', 'queue2' ] }
35
38
  def self.queue_parser(string)
36
39
  string = string.presence || '*'
37
40
 
38
- if string.first == '-'
41
+ case string.first
42
+ when '-'
39
43
  exclude_queues = true
40
44
  string = string[1..-1]
45
+ when '+'
46
+ ordered_queues = true
47
+ string = string[1..-1]
41
48
  end
42
49
 
43
50
  queues = string.split(',').map(&:strip)
@@ -46,6 +53,11 @@ module GoodJob
46
53
  { all: true }
47
54
  elsif exclude_queues
48
55
  { exclude: queues }
56
+ elsif ordered_queues
57
+ {
58
+ include: queues,
59
+ ordered_queues: true,
60
+ }
49
61
  else
50
62
  { include: queues }
51
63
  end
@@ -88,6 +100,42 @@ module GoodJob
88
100
  # @return [ActiveRecord::Relation]
89
101
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
90
102
 
103
+ # Order jobs by created_at, for first-in first-out
104
+ # @!method creation_ordered
105
+ # @!scope class
106
+ # @return [ActiveRecord:Relation]
107
+ scope :creation_ordered, -> { order('created_at ASC') }
108
+
109
+ # Order jobs for de-queueing
110
+ # @!method dequeueing_ordered
111
+ # @!scope class
112
+ # @param parsed_queues [Hash]
113
+ # optional output of .queue_parser, parsed queues, will be used for
114
+ # ordered queues.
115
+ # @return [ActiveRecord::Relation]
116
+ scope :dequeueing_ordered, (lambda do |parsed_queues|
117
+ relation = self
118
+ relation = relation.queue_ordered(parsed_queues[:include]) if parsed_queues && parsed_queues[:ordered_queues] && parsed_queues[:include]
119
+ relation = relation.priority_ordered.creation_ordered
120
+
121
+ relation
122
+ end)
123
+
124
+ # Order jobs in order of queues in array param
125
+ # @!method queue_ordered
126
+ # @!scope class
127
+ # @param queues [Array<string] ordered names of queues
128
+ # @return [ActiveRecord::Relation]
129
+ scope :queue_ordered, (lambda do |queues|
130
+ clauses = queues.map.with_index do |queue_name, index|
131
+ "WHEN queue_name = '#{queue_name}' THEN #{index}"
132
+ end
133
+
134
+ order(
135
+ Arel.sql("(CASE #{clauses.join(' ')} ELSE #{queues.length} END)")
136
+ )
137
+ end)
138
+
91
139
  # Order jobs by scheduled or created (oldest first).
92
140
  # @!method schedule_ordered
93
141
  # @!scope class
@@ -153,8 +201,8 @@ module GoodJob
153
201
  # return value for the job's +#perform+ method, and the exception the job
154
202
  # raised, if any (if the job raised, then the second array entry will be
155
203
  # +nil+). If there were no jobs to execute, returns +nil+.
156
- def self.perform_with_advisory_lock
157
- unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |executions|
204
+ def self.perform_with_advisory_lock(parsed_queues: nil)
205
+ unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |executions|
158
206
  execution = executions.first
159
207
  break if execution.blank?
160
208
  break :unlocked unless execution&.executable?
@@ -265,56 +313,31 @@ module GoodJob
265
313
  end
266
314
  end
267
315
 
268
- # There are 3 buckets of non-overlapping statuses:
269
- # 1. The job will be executed
270
- # - queued: The job will execute immediately when an execution thread becomes available.
271
- # - scheduled: The job is scheduled to execute in the future.
272
- # - retried: The job previously errored on execution and will be re-executed in the future.
273
- # 2. The job is being executed
274
- # - running: the job is actively being executed by an execution thread
275
- # 3. The job will not execute
276
- # - finished: The job executed successfully
277
- # - discarded: The job previously errored on execution and will not be re-executed in the future.
278
- #
279
- # @return [Symbol]
280
- def status
281
- if finished_at.present?
282
- if error.present?
283
- :discarded
284
- else
285
- :finished
286
- end
287
- elsif (scheduled_at || created_at) > DateTime.current
288
- if serialized_params.fetch('executions', 0) > 1
289
- :retried
290
- else
291
- :scheduled
292
- end
293
- elsif running?
294
- :running
295
- else
296
- :queued
297
- end
316
+ # Return formatted serialized_params for display in the dashboard
317
+ # @return [Hash]
318
+ def display_serialized_params
319
+ serialized_params.merge({
320
+ _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
321
+ })
298
322
  end
299
323
 
300
324
  def running?
301
- performed_at? && !finished_at?
325
+ if has_attribute?(:locktype)
326
+ self['locktype'].present?
327
+ else
328
+ advisory_locked?
329
+ end
302
330
  end
303
331
 
304
332
  def number
305
333
  serialized_params.fetch('executions', 0) + 1
306
334
  end
307
335
 
308
- # The last relevant timestamp for this execution
309
- def last_status_at
310
- finished_at || performed_at || scheduled_at || created_at
311
- end
312
-
313
336
  # Time between when this job was expected to run and when it started running
314
337
  def queue_latency
315
338
  now = Time.zone.now
316
339
  expected_start = scheduled_at || created_at
317
- actual_start = performed_at || now
340
+ actual_start = performed_at || finished_at || now
318
341
 
319
342
  actual_start - expected_start unless expected_start >= now
320
343
  end
@@ -3,10 +3,12 @@ module GoodJob
3
3
  # ActiveRecord model that represents an +ActiveJob+ job.
4
4
  # There is not a table in the database whose discrete rows represents "Jobs".
5
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
6
+ # A single row from the +good_jobs+ table of executions is fetched to represent a Job.
7
+ #
7
8
  class Job < BaseRecord
8
9
  include Filterable
9
10
  include Lockable
11
+ include Reportable
10
12
 
11
13
  # Raised when an inappropriate action is applied to a Job based on its state.
12
14
  ActionForStateMismatchError = Class.new(StandardError)
@@ -72,9 +74,25 @@ module GoodJob
72
74
  serialized_params['job_class']
73
75
  end
74
76
 
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
77
+ # Override #reload to add a custom scope to ensure the reloaded record is the head execution
78
+ # @return [Job]
79
+ def reload(options = nil)
80
+ self.class.connection.clear_query_cache
81
+
82
+ # override with the `where(retried_good_job_id: nil)` scope
83
+ override_query = self.class.where(retried_good_job_id: nil)
84
+ fresh_object =
85
+ if options && options[:lock]
86
+ self.class.unscoped { override_query.lock(options[:lock]).find(id) }
87
+ else
88
+ self.class.unscoped { override_query.find(id) }
89
+ end
90
+
91
+ @attributes = fresh_object.instance_variable_get(:@attributes)
92
+ @new_record = false
93
+ @previously_new_record = false
94
+ self
95
+ end
78
96
 
79
97
  # This job's most recent {Execution}
80
98
  # @param reload [Booelan] whether to reload executions
@@ -94,7 +112,7 @@ module GoodJob
94
112
  # The number of times this job has been executed, according to ActiveJob's serialized state.
95
113
  # @return [Numeric]
96
114
  def executions_count
97
- aj_count = head_execution.serialized_params.fetch('executions', 0)
115
+ aj_count = serialized_params.fetch('executions', 0)
98
116
  # The execution count within serialized_params is not updated
99
117
  # once the underlying execution has been executed.
100
118
  if status.in? [:discarded, :finished, :running]
@@ -114,7 +132,15 @@ module GoodJob
114
132
  # If the job has been retried, the error will be fetched from the previous {Execution} record.
115
133
  # @return [String]
116
134
  def recent_error
117
- head_execution.error || executions[-2]&.error
135
+ error || executions[-2]&.error
136
+ end
137
+
138
+ # Return formatted serialized_params for display in the dashboard
139
+ # @return [Hash]
140
+ def display_serialized_params
141
+ serialized_params.merge({
142
+ _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
143
+ })
118
144
  end
119
145
 
120
146
  # Tests whether the job is being executed right now.
@@ -192,7 +218,6 @@ module GoodJob
192
218
 
193
219
  raise ActionForStateMismatchError if execution.finished_at.present?
194
220
 
195
- execution = head_execution(reload: true)
196
221
  execution.update(scheduled_at: scheduled_at)
197
222
  end
198
223
  end
@@ -45,6 +45,8 @@ module GoodJob # :nodoc:
45
45
  hostname: Socket.gethostname,
46
46
  pid: ::Process.pid,
47
47
  proctitle: $PROGRAM_NAME,
48
+ preserve_job_records: GoodJob.preserve_job_records,
49
+ retry_on_unhandled_error: GoodJob.retry_on_unhandled_error,
48
50
  schedulers: GoodJob::Scheduler.instances.map(&:name),
49
51
  }
50
52
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ module Reportable
4
+ # There are 3 buckets of non-overlapping statuses:
5
+ # 1. The job will be executed
6
+ # - queued: The job will execute immediately when an execution thread becomes available.
7
+ # - scheduled: The job is scheduled to execute in the future.
8
+ # - retried: The job previously errored on execution and will be re-executed in the future.
9
+ # 2. The job is being executed
10
+ # - running: the job is actively being executed by an execution thread
11
+ # 3. The job will not execute
12
+ # - finished: The job executed successfully
13
+ # - discarded: The job previously errored on execution and will not be re-executed in the future.
14
+ #
15
+ # @return [Symbol]
16
+ def status
17
+ if finished_at.present?
18
+ if error.present? && retried_good_job_id.present?
19
+ :retried
20
+ elsif error.present? && retried_good_job_id.nil?
21
+ :discarded
22
+ else
23
+ :finished
24
+ end
25
+ elsif (scheduled_at || created_at) > DateTime.current
26
+ if serialized_params.fetch('executions', 0) > 1
27
+ :retried
28
+ else
29
+ :scheduled
30
+ end
31
+ elsif running?
32
+ :running
33
+ else
34
+ :queued
35
+ end
36
+ end
37
+
38
+ # The last relevant timestamp for this execution
39
+ def last_status_at
40
+ finished_at || performed_at || scheduled_at || created_at
41
+ end
42
+ end
43
+ end
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: 3.0.2
4
+ version: 3.3.0
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-07-10 00:00:00.000000000 Z
11
+ date: 2022-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -365,6 +365,7 @@ files:
365
365
  - app/assets/good_job/modules/checkbox_toggle.js
366
366
  - app/assets/good_job/modules/document_ready.js
367
367
  - app/assets/good_job/modules/live_poll.js
368
+ - app/assets/good_job/modules/popovers.js
368
369
  - app/assets/good_job/modules/toasts.js
369
370
  - app/assets/good_job/scripts.js
370
371
  - app/assets/good_job/style.css
@@ -398,7 +399,9 @@ files:
398
399
  - app/views/good_job/shared/icons/_check.html.erb
399
400
  - app/views/good_job/shared/icons/_clock.html.erb
400
401
  - app/views/good_job/shared/icons/_dash_circle.html.erb
402
+ - app/views/good_job/shared/icons/_dots.html.erb
401
403
  - app/views/good_job/shared/icons/_exclamation.html.erb
404
+ - app/views/good_job/shared/icons/_info.html.erb
402
405
  - app/views/good_job/shared/icons/_play.html.erb
403
406
  - app/views/good_job/shared/icons/_skip_forward.html.erb
404
407
  - app/views/good_job/shared/icons/_stop.html.erb
@@ -446,6 +449,7 @@ files:
446
449
  - lib/models/good_job/job.rb
447
450
  - lib/models/good_job/lockable.rb
448
451
  - lib/models/good_job/process.rb
452
+ - lib/models/good_job/reportable.rb
449
453
  homepage: https://github.com/bensheldon/good_job
450
454
  licenses:
451
455
  - MIT