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.
- 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
|