good_job 2.4.2 → 2.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/README.md +12 -4
- data/engine/app/assets/scripts.js +1 -0
- data/engine/app/assets/style.css +5 -0
- data/engine/app/assets/vendor/chartjs/chart.min.js +13 -0
- data/engine/app/assets/vendor/rails_ujs.js +747 -0
- data/engine/app/charts/good_job/scheduled_by_queue_chart.rb +69 -0
- data/engine/app/controllers/good_job/assets_controller.rb +8 -4
- data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
- data/engine/app/filters/good_job/base_filter.rb +18 -56
- data/engine/app/filters/good_job/executions_filter.rb +9 -8
- data/engine/app/filters/good_job/jobs_filter.rb +12 -9
- data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
- data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
- data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
- data/engine/app/views/good_job/executions/index.html.erb +2 -2
- data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +18 -6
- data/engine/app/views/good_job/jobs/index.html.erb +15 -2
- data/engine/app/views/good_job/jobs/show.html.erb +2 -2
- data/engine/app/views/good_job/shared/_chart.erb +19 -46
- data/engine/app/views/good_job/shared/_filter.erb +27 -13
- data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
- data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
- data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
- data/engine/app/views/layouts/good_job/base.html.erb +6 -4
- data/engine/config/routes.rb +17 -4
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +2 -0
- data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
- data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
- data/lib/good_job/active_job_job.rb +228 -0
- data/lib/good_job/configuration.rb +1 -1
- data/lib/good_job/cron_entry.rb +78 -5
- data/lib/good_job/cron_manager.rb +4 -6
- data/lib/good_job/current_thread.rb +38 -5
- data/lib/good_job/execution.rb +53 -39
- data/lib/good_job/filterable.rb +42 -0
- data/lib/good_job/notifier.rb +17 -7
- data/lib/good_job/version.rb +1 -1
- metadata +31 -21
- data/engine/app/assets/vendor/chartist/chartist.css +0 -613
- data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
- data/engine/app/models/good_job/active_job_job.rb +0 -127
- data/engine/app/views/good_job/cron_schedules/index.html.erb +0 -72
@@ -0,0 +1,4 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/play/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
|
3
|
+
<path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z" />
|
4
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/skip-forward/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-forward" viewBox="0 0 16 16">
|
3
|
+
<path d="M15.5 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V8.752l-6.267 3.636c-.52.302-1.233-.043-1.233-.696v-2.94l-6.267 3.636C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696L7.5 7.248v-2.94c0-.653.713-.998 1.233-.696L15 7.248V4a.5.5 0 0 1 .5-.5zM1 4.633v6.734L6.804 8 1 4.633zm7.5 0v6.734L14.304 8 8.5 4.633z" />
|
4
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/stop/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stop" viewBox="0 0 16 16">
|
3
|
+
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5H5z" />
|
4
|
+
</svg>
|
@@ -1,16 +1,18 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
|
-
<html>
|
2
|
+
<html lang="en">
|
3
3
|
<head>
|
4
4
|
<title>Good Job Dashboard</title>
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
|
8
8
|
<%= stylesheet_link_tag bootstrap_path(format: :css, v: GoodJob::VERSION) %>
|
9
|
-
<%= stylesheet_link_tag chartist_path(format: :css, v: GoodJob::VERSION) %>
|
10
9
|
<%= stylesheet_link_tag style_path(format: :css, v: GoodJob::VERSION) %>
|
11
10
|
|
12
11
|
<%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
|
13
|
-
<%= javascript_include_tag
|
12
|
+
<%= javascript_include_tag chartjs_path(format: :js, v: GoodJob::VERSION) %>
|
13
|
+
<%= javascript_include_tag scripts_path(format: :js, v: GoodJob::VERSION) %>
|
14
|
+
|
15
|
+
<%= javascript_include_tag rails_ujs_path(format: :js, v: GoodJob::VERSION) %>
|
14
16
|
</head>
|
15
17
|
<body>
|
16
18
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
@@ -29,7 +31,7 @@
|
|
29
31
|
<%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
|
30
32
|
</li>
|
31
33
|
<li class="nav-item">
|
32
|
-
<%= link_to "Cron Schedules",
|
34
|
+
<%= link_to "Cron Schedules", cron_entries_path, class: ["nav-link", ("active" if current_page?(cron_entries_path))] %>
|
33
35
|
</li>
|
34
36
|
<li class="nav-item">
|
35
37
|
<div class="nav-link">
|
data/engine/config/routes.rb
CHANGED
@@ -1,20 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
GoodJob::Engine.routes.draw do
|
3
3
|
root to: 'executions#index'
|
4
|
-
|
5
|
-
resources :
|
4
|
+
|
5
|
+
resources :cron_entries, only: %i[index show] do
|
6
|
+
member do
|
7
|
+
post :enqueue
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
resources :jobs, only: %i[index show] do
|
12
|
+
member do
|
13
|
+
put :discard
|
14
|
+
put :reschedule
|
15
|
+
put :retry
|
16
|
+
end
|
17
|
+
end
|
6
18
|
resources :executions, only: %i[destroy]
|
7
19
|
|
8
20
|
scope controller: :assets do
|
9
21
|
constraints(format: :css) do
|
10
22
|
get :bootstrap, action: :bootstrap_css
|
11
|
-
get :chartist, action: :chartist_css
|
12
23
|
get :style, action: :style_css
|
13
24
|
end
|
14
25
|
|
15
26
|
constraints(format: :js) do
|
16
27
|
get :bootstrap, action: :bootstrap_js
|
17
|
-
get :
|
28
|
+
get :rails_ujs, action: :rails_ujs_js
|
29
|
+
get :chartjs, action: :chartjs_js
|
30
|
+
get :scripts, action: :scripts_js
|
18
31
|
end
|
19
32
|
end
|
20
33
|
end
|
@@ -18,6 +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
22
|
end
|
22
23
|
|
23
24
|
add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: "index_good_jobs_on_scheduled_at"
|
@@ -25,5 +26,6 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
25
26
|
add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at
|
26
27
|
add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished
|
27
28
|
add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at
|
29
|
+
add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true
|
28
30
|
end
|
29
31
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class AddCronAtToGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
3
|
+
def change
|
4
|
+
reversible do |dir|
|
5
|
+
dir.up do
|
6
|
+
# Ensure this incremental update migration is idempotent
|
7
|
+
# with monolithic install migration.
|
8
|
+
return if connection.column_exists?(:good_jobs, :cron_at)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
add_column :good_jobs, :cron_at, :timestamp
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class AddCronKeyCronAtIndexToGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
3
|
+
disable_ddl_transaction!
|
4
|
+
|
5
|
+
def change
|
6
|
+
reversible do |dir|
|
7
|
+
dir.up do
|
8
|
+
# Ensure this incremental update migration is idempotent
|
9
|
+
# with monolithic install migration.
|
10
|
+
return if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :good_jobs,
|
15
|
+
[:cron_key, :cron_at],
|
16
|
+
algorithm: :concurrently,
|
17
|
+
name: :index_good_jobs_on_cron_key_and_cron_at,
|
18
|
+
unique: true
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,228 @@
|
|
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 ActiveJobJob
|
7
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
8
|
+
# @!parse
|
9
|
+
# class ActiveJob < ActiveRecord::Base; end
|
10
|
+
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
11
|
+
include Filterable
|
12
|
+
include Lockable
|
13
|
+
|
14
|
+
# Raised when an inappropriate action is applied to a Job based on its state.
|
15
|
+
ActionForStateMismatchError = Class.new(StandardError)
|
16
|
+
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
17
|
+
AdapterNotGoodJobError = Class.new(StandardError)
|
18
|
+
# Attached to a Job's Execution when the Job is discarded.
|
19
|
+
DiscardJobError = Class.new(StandardError)
|
20
|
+
|
21
|
+
self.table_name = 'good_jobs'
|
22
|
+
self.primary_key = 'active_job_id'
|
23
|
+
self.advisory_lockable_column = 'active_job_id'
|
24
|
+
|
25
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
|
26
|
+
|
27
|
+
# Only the most-recent unretried execution represents a "Job"
|
28
|
+
default_scope { where(retried_good_job_id: nil) }
|
29
|
+
|
30
|
+
# Get Jobs with given class name
|
31
|
+
# @!method job_class
|
32
|
+
# @!scope class
|
33
|
+
# @param string [String]
|
34
|
+
# Execution class name
|
35
|
+
# @return [ActiveRecord::Relation]
|
36
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
37
|
+
|
38
|
+
# First execution will run in the future
|
39
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
40
|
+
# Execution errored, will run in the future
|
41
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
42
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
43
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
44
|
+
# Advisory locked and executing
|
45
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
46
|
+
# Completed executing successfully
|
47
|
+
scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
|
48
|
+
# Errored but will not be retried
|
49
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
50
|
+
|
51
|
+
# The job's ActiveJob UUID
|
52
|
+
# @return [String]
|
53
|
+
def id
|
54
|
+
active_job_id
|
55
|
+
end
|
56
|
+
|
57
|
+
# The ActiveJob job class, as a string
|
58
|
+
# @return [String]
|
59
|
+
def job_class
|
60
|
+
serialized_params['job_class']
|
61
|
+
end
|
62
|
+
|
63
|
+
# The status of the Job, based on the state of its most recent execution.
|
64
|
+
# There are 3 buckets of non-overlapping statuses:
|
65
|
+
# 1. The job will be executed
|
66
|
+
# - queued: The job will execute immediately when an execution thread becomes available.
|
67
|
+
# - scheduled: The job is scheduled to execute in the future.
|
68
|
+
# - retried: The job previously errored on execution and will be re-executed in the future.
|
69
|
+
# 2. The job is being executed
|
70
|
+
# - running: the job is actively being executed by an execution thread
|
71
|
+
# 3. The job will not execute
|
72
|
+
# - finished: The job executed successfully
|
73
|
+
# - discarded: The job previously errored on execution and will not be re-executed in the future.
|
74
|
+
#
|
75
|
+
# @return [Symbol]
|
76
|
+
def status
|
77
|
+
execution = head_execution
|
78
|
+
if execution.finished_at.present?
|
79
|
+
if execution.error.present?
|
80
|
+
:discarded
|
81
|
+
else
|
82
|
+
:finished
|
83
|
+
end
|
84
|
+
elsif (execution.scheduled_at || execution.created_at) > DateTime.current
|
85
|
+
if execution.serialized_params.fetch('executions', 0) > 1
|
86
|
+
:retried
|
87
|
+
else
|
88
|
+
:scheduled
|
89
|
+
end
|
90
|
+
elsif running?
|
91
|
+
:running
|
92
|
+
else
|
93
|
+
:queued
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# This job's most recent {Execution}
|
98
|
+
# @param reload [Booelan] whether to reload executions
|
99
|
+
# @return [Execution]
|
100
|
+
def head_execution(reload: false)
|
101
|
+
executions.reload if reload
|
102
|
+
executions.load # memoize the results
|
103
|
+
executions.last
|
104
|
+
end
|
105
|
+
|
106
|
+
# This job's initial/oldest {Execution}
|
107
|
+
# @return [Execution]
|
108
|
+
def tail_execution
|
109
|
+
executions.first
|
110
|
+
end
|
111
|
+
|
112
|
+
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
113
|
+
# @return [Numeric]
|
114
|
+
def executions_count
|
115
|
+
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
116
|
+
# The execution count within serialized_params is not updated
|
117
|
+
# once the underlying execution has been executed.
|
118
|
+
if status.in? [:discarded, :finished, :running]
|
119
|
+
aj_count + 1
|
120
|
+
else
|
121
|
+
aj_count
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
126
|
+
# @return [Numeric]
|
127
|
+
def preserved_executions_count
|
128
|
+
executions.size
|
129
|
+
end
|
130
|
+
|
131
|
+
# The most recent error message.
|
132
|
+
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
133
|
+
# @return [String]
|
134
|
+
def recent_error
|
135
|
+
head_execution.error || executions[-2]&.error
|
136
|
+
end
|
137
|
+
|
138
|
+
# Tests whether the job is being executed right now.
|
139
|
+
# @return [Boolean]
|
140
|
+
def running?
|
141
|
+
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
142
|
+
if has_attribute?(:locktype)
|
143
|
+
self['locktype'].present?
|
144
|
+
else
|
145
|
+
advisory_locked?
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Retry a job that has errored and been discarded.
|
150
|
+
# This action will create a new job {Execution} record.
|
151
|
+
# @return [ActiveJob::Base]
|
152
|
+
def retry_job
|
153
|
+
with_advisory_lock do
|
154
|
+
execution = head_execution(reload: true)
|
155
|
+
active_job = execution.active_job
|
156
|
+
|
157
|
+
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
158
|
+
raise ActionForStateMismatchError unless status == :discarded
|
159
|
+
|
160
|
+
# Update the executions count because the previous execution will not have been preserved
|
161
|
+
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
162
|
+
active_job.executions = (active_job.executions || 0) + 1
|
163
|
+
|
164
|
+
new_active_job = nil
|
165
|
+
GoodJob::CurrentThread.within do |current_thread|
|
166
|
+
current_thread.execution = execution
|
167
|
+
|
168
|
+
execution.class.transaction(joinable: false, requires_new: true) do
|
169
|
+
new_active_job = active_job.retry_job(wait: 0, error: error)
|
170
|
+
execution.save
|
171
|
+
end
|
172
|
+
end
|
173
|
+
new_active_job
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Discard a job so that it will not be executed further.
|
178
|
+
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
179
|
+
# @return [void]
|
180
|
+
def discard_job(message)
|
181
|
+
with_advisory_lock do
|
182
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
183
|
+
|
184
|
+
execution = head_execution(reload: true)
|
185
|
+
active_job = execution.active_job
|
186
|
+
|
187
|
+
job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
|
188
|
+
|
189
|
+
update_execution = proc do
|
190
|
+
execution.update(
|
191
|
+
finished_at: Time.current,
|
192
|
+
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
if active_job.respond_to?(:instrument)
|
197
|
+
active_job.send :instrument, :discard, error: job_error, &update_execution
|
198
|
+
else
|
199
|
+
update_execution.call
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
205
|
+
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
206
|
+
# @return [void]
|
207
|
+
def reschedule_job(scheduled_at = Time.current)
|
208
|
+
with_advisory_lock do
|
209
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
210
|
+
|
211
|
+
execution = head_execution(reload: true)
|
212
|
+
execution.update(scheduled_at: scheduled_at)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Utility method to determine which execution record is used to represent this job
|
217
|
+
# @return [String]
|
218
|
+
def _execution_id
|
219
|
+
attributes['id']
|
220
|
+
end
|
221
|
+
|
222
|
+
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
223
|
+
# @return [Boolean]
|
224
|
+
def _head?
|
225
|
+
_execution_id == head_execution(reload: true).id
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -157,7 +157,7 @@ module GoodJob
|
|
157
157
|
alias enable_cron? enable_cron
|
158
158
|
|
159
159
|
def cron
|
160
|
-
env_cron = JSON.parse(ENV['GOOD_JOB_CRON']) if ENV['GOOD_JOB_CRON'].present?
|
160
|
+
env_cron = JSON.parse(ENV['GOOD_JOB_CRON'], symbolize_names: true) if ENV['GOOD_JOB_CRON'].present?
|
161
161
|
|
162
162
|
options[:cron] ||
|
163
163
|
rails_config[:cron] ||
|
data/lib/good_job/cron_entry.rb
CHANGED
@@ -12,14 +12,29 @@ module GoodJob # :nodoc:
|
|
12
12
|
|
13
13
|
attr_reader :params
|
14
14
|
|
15
|
+
def self.all(configuration: nil)
|
16
|
+
configuration ||= GoodJob::Configuration.new({})
|
17
|
+
configuration.cron_entries
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find(key, configuration: nil)
|
21
|
+
all(configuration: configuration).find { |entry| entry.key == key.to_sym }.tap do |cron_entry|
|
22
|
+
raise ActiveRecord::RecordNotFound unless cron_entry
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
15
26
|
def initialize(params = {})
|
16
|
-
@params = params
|
27
|
+
@params = params
|
28
|
+
|
29
|
+
raise ArgumentError, "Invalid cron format: '#{cron}'" unless fugit.instance_of?(Fugit::Cron)
|
17
30
|
end
|
18
31
|
|
19
32
|
def key
|
20
33
|
params.fetch(:key)
|
21
34
|
end
|
35
|
+
|
22
36
|
alias id key
|
37
|
+
alias to_param key
|
23
38
|
|
24
39
|
def job_class
|
25
40
|
params.fetch(:class)
|
@@ -42,12 +57,59 @@ module GoodJob # :nodoc:
|
|
42
57
|
end
|
43
58
|
|
44
59
|
def next_at
|
45
|
-
fugit
|
46
|
-
|
60
|
+
fugit.next_time.to_t
|
61
|
+
end
|
62
|
+
|
63
|
+
def schedule
|
64
|
+
fugit.original
|
65
|
+
end
|
66
|
+
|
67
|
+
def fugit
|
68
|
+
@_fugit ||= Fugit.parse(cron)
|
69
|
+
end
|
70
|
+
|
71
|
+
def jobs
|
72
|
+
GoodJob::ActiveJobJob.where(cron_key: key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def last_at
|
76
|
+
return if last_job.blank?
|
77
|
+
|
78
|
+
if GoodJob::ActiveJobJob.column_names.include?('cron_at')
|
79
|
+
(last_job.cron_at || last_job.created_at).localtime
|
80
|
+
else
|
81
|
+
last_job.created_at
|
82
|
+
end
|
47
83
|
end
|
48
84
|
|
49
|
-
def enqueue
|
50
|
-
|
85
|
+
def enqueue(cron_at = nil)
|
86
|
+
GoodJob::CurrentThread.within do |current_thread|
|
87
|
+
current_thread.cron_key = key
|
88
|
+
current_thread.cron_at = cron_at
|
89
|
+
|
90
|
+
job_class.constantize.set(set_value).perform_later(*args_value)
|
91
|
+
end
|
92
|
+
rescue ActiveRecord::RecordNotUnique
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
def last_job
|
97
|
+
if GoodJob::ActiveJobJob.column_names.include?('cron_at')
|
98
|
+
jobs.order("cron_at DESC NULLS LAST").first
|
99
|
+
else
|
100
|
+
jobs.order(created_at: :asc).last
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def display_properties
|
105
|
+
{
|
106
|
+
key: key,
|
107
|
+
class: job_class,
|
108
|
+
cron: schedule,
|
109
|
+
set: display_property(set),
|
110
|
+
args: display_property(args),
|
111
|
+
description: display_property(description),
|
112
|
+
}
|
51
113
|
end
|
52
114
|
|
53
115
|
private
|
@@ -61,5 +123,16 @@ module GoodJob # :nodoc:
|
|
61
123
|
value = args || []
|
62
124
|
value.respond_to?(:call) ? value.call : value
|
63
125
|
end
|
126
|
+
|
127
|
+
def display_property(value)
|
128
|
+
case value
|
129
|
+
when NilClass
|
130
|
+
"None"
|
131
|
+
when Proc
|
132
|
+
"Lambda/Callable"
|
133
|
+
else
|
134
|
+
value
|
135
|
+
end
|
136
|
+
end
|
64
137
|
end
|
65
138
|
end
|
@@ -82,16 +82,14 @@ module GoodJob # :nodoc:
|
|
82
82
|
# Enqueues a scheduled task
|
83
83
|
# @param cron_entry [CronEntry] the CronEntry object to schedule
|
84
84
|
def create_task(cron_entry)
|
85
|
-
|
86
|
-
|
85
|
+
cron_at = cron_entry.next_at
|
86
|
+
delay = [(cron_at - Time.current).to_f, 0].max
|
87
|
+
future = Concurrent::ScheduledTask.new(delay, args: [self, cron_entry, cron_at]) do |thr_scheduler, thr_cron_entry, thr_cron_at|
|
87
88
|
# Re-schedule the next cron task before executing the current task
|
88
89
|
thr_scheduler.create_task(thr_cron_entry)
|
89
90
|
|
90
91
|
Rails.application.executor.wrap do
|
91
|
-
|
92
|
-
CurrentThread.cron_key = thr_cron_entry.key
|
93
|
-
|
94
|
-
cron_entry.enqueue
|
92
|
+
cron_entry.enqueue(thr_cron_at)
|
95
93
|
end
|
96
94
|
end
|
97
95
|
|
@@ -5,6 +5,21 @@ module GoodJob
|
|
5
5
|
# Thread-local attributes for passing values from Instrumentation.
|
6
6
|
# (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
|
7
7
|
module CurrentThread
|
8
|
+
# Resettable accessors for thread-local values.
|
9
|
+
ACCESSORS = %i[
|
10
|
+
cron_at
|
11
|
+
cron_key
|
12
|
+
error_on_discard
|
13
|
+
error_on_retry
|
14
|
+
execution
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
# @!attribute [rw] cron_at
|
18
|
+
# @!scope class
|
19
|
+
# Cron At
|
20
|
+
# @return [DateTime, nil]
|
21
|
+
thread_mattr_accessor :cron_at
|
22
|
+
|
8
23
|
# @!attribute [rw] cron_key
|
9
24
|
# @!scope class
|
10
25
|
# Cron Key
|
@@ -30,12 +45,20 @@ module GoodJob
|
|
30
45
|
thread_mattr_accessor :execution
|
31
46
|
|
32
47
|
# Resets attributes
|
48
|
+
# @param [Hash] values to assign
|
33
49
|
# @return [void]
|
34
|
-
def self.reset
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
50
|
+
def self.reset(values = {})
|
51
|
+
ACCESSORS.each do |accessor|
|
52
|
+
send("#{accessor}=", values[accessor])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Exports values to hash
|
57
|
+
# @return [Hash]
|
58
|
+
def self.to_h
|
59
|
+
ACCESSORS.each_with_object({}) do |accessor, hash|
|
60
|
+
hash[accessor] = send(accessor)
|
61
|
+
end
|
39
62
|
end
|
40
63
|
|
41
64
|
# @return [String] UUID of the currently executing GoodJob::Execution
|
@@ -52,5 +75,15 @@ module GoodJob
|
|
52
75
|
def self.thread_name
|
53
76
|
(Thread.current.name || Thread.current.object_id).to_s
|
54
77
|
end
|
78
|
+
|
79
|
+
# Wrap the yielded block with CurrentThread values and reset after the block
|
80
|
+
# @yield [self]
|
81
|
+
# @return [void]
|
82
|
+
def self.within
|
83
|
+
original_values = to_h
|
84
|
+
yield(self)
|
85
|
+
ensure
|
86
|
+
reset(original_values)
|
87
|
+
end
|
55
88
|
end
|
56
89
|
end
|