good_job 3.0.2 → 3.3.0
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 +52 -1
- data/README.md +35 -31
- data/app/assets/good_job/modules/application.js +2 -0
- data/app/assets/good_job/modules/popovers.js +7 -0
- data/app/assets/good_job/style.css +4 -0
- data/app/controllers/good_job/jobs_controller.rb +1 -1
- data/app/helpers/good_job/application_helper.rb +2 -2
- data/app/views/good_job/cron_entries/index.html.erb +60 -57
- data/app/views/good_job/jobs/_executions.erb +31 -28
- data/app/views/good_job/jobs/_table.erb +138 -109
- data/app/views/good_job/jobs/show.html.erb +41 -32
- data/app/views/good_job/processes/index.html.erb +60 -32
- data/app/views/good_job/shared/_filter.erb +1 -1
- data/app/views/good_job/shared/_navbar.erb +15 -3
- data/app/views/good_job/shared/icons/_dots.html.erb +3 -0
- data/app/views/good_job/shared/icons/_info.html.erb +4 -0
- data/config/locales/en.yml +19 -0
- data/config/locales/es.yml +19 -0
- data/config/locales/nl.yml +19 -0
- data/config/locales/ru.yml +19 -0
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +4 -4
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +4 -4
- data/lib/good_job/daemon.rb +2 -2
- data/lib/good_job/job_performer.rb +1 -1
- data/lib/good_job/version.rb +1 -1
- data/lib/models/good_job/cron_entry.rb +4 -2
- data/lib/models/good_job/execution.rb +63 -40
- data/lib/models/good_job/job.rb +32 -7
- data/lib/models/good_job/process.rb +2 -0
- data/lib/models/good_job/reportable.rb +43 -0
- metadata +6 -2
@@ -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.
|
11
|
-
t.
|
12
|
-
t.
|
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.
|
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.
|
11
|
-
t.
|
12
|
-
t.
|
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.
|
21
|
+
t.datetime :cron_at
|
22
22
|
end
|
23
23
|
|
24
24
|
create_table :good_job_processes, id: :uuid do |t|
|
data/lib/good_job/daemon.rb
CHANGED
@@ -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.
|
data/lib/good_job/version.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
-
#
|
269
|
-
#
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
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
|
data/lib/models/good_job/job.rb
CHANGED
@@ -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
|
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
|
-
#
|
76
|
-
# @return [
|
77
|
-
|
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 =
|
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
|
-
|
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
|
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-
|
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
|