good_job 2.4.0 → 2.6.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 +89 -0
- data/README.md +51 -20
- data/engine/app/assets/vendor/rails_ujs.js +747 -0
- data/engine/app/controllers/good_job/assets_controller.rb +4 -0
- data/engine/app/controllers/good_job/base_controller.rb +8 -0
- 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 +12 -7
- data/engine/app/filters/good_job/executions_filter.rb +1 -1
- data/engine/app/filters/good_job/jobs_filter.rb +4 -2
- 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 +1 -1
- data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +17 -5
- data/engine/app/views/good_job/jobs/index.html.erb +14 -1
- data/engine/app/views/good_job/jobs/show.html.erb +2 -2
- data/engine/app/views/good_job/shared/_filter.erb +9 -10
- 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 +3 -1
- data/engine/config/routes.rb +15 -2
- data/lib/generators/good_job/install_generator.rb +6 -0
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
- data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
- 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/generators/good_job/update_generator.rb +6 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
- data/lib/good_job/active_job_job.rb +245 -0
- data/lib/good_job/adapter.rb +4 -2
- data/lib/good_job/cli.rb +3 -1
- data/lib/good_job/configuration.rb +5 -1
- data/lib/good_job/cron_entry.rb +138 -0
- data/lib/good_job/cron_manager.rb +17 -31
- data/lib/good_job/current_thread.rb +38 -5
- data/lib/good_job/execution.rb +50 -25
- data/lib/good_job/lockable.rb +1 -1
- data/lib/good_job/log_subscriber.rb +3 -3
- data/lib/good_job/scheduler.rb +1 -0
- data/lib/good_job/version.rb +1 -1
- metadata +21 -12
- 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 -50
@@ -0,0 +1,245 @@
|
|
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 GoodJob::Lockable
|
12
|
+
|
13
|
+
# Raised when an inappropriate action is applied to a Job based on its state.
|
14
|
+
ActionForStateMismatchError = Class.new(StandardError)
|
15
|
+
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
16
|
+
AdapterNotGoodJobError = Class.new(StandardError)
|
17
|
+
# Attached to a Job's Execution when the Job is discarded.
|
18
|
+
DiscardJobError = Class.new(StandardError)
|
19
|
+
|
20
|
+
self.table_name = 'good_jobs'
|
21
|
+
self.primary_key = 'active_job_id'
|
22
|
+
self.advisory_lockable_column = 'active_job_id'
|
23
|
+
|
24
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
|
25
|
+
|
26
|
+
# Only the most-recent unretried execution represents a "Job"
|
27
|
+
default_scope { where(retried_good_job_id: nil) }
|
28
|
+
|
29
|
+
# Get Jobs with given class name
|
30
|
+
# @!method job_class
|
31
|
+
# @!scope class
|
32
|
+
# @param string [String]
|
33
|
+
# Execution class name
|
34
|
+
# @return [ActiveRecord::Relation]
|
35
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
36
|
+
|
37
|
+
# First execution will run in the future
|
38
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
39
|
+
# Execution errored, will run in the future
|
40
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
41
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
42
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
43
|
+
# Advisory locked and executing
|
44
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
45
|
+
# Completed executing successfully
|
46
|
+
scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
|
47
|
+
# Errored but will not be retried
|
48
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
49
|
+
|
50
|
+
# Get Jobs in display order with optional keyset pagination.
|
51
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
52
|
+
# @!scope class
|
53
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
54
|
+
# Display records scheduled after this time for keyset pagination
|
55
|
+
# @param after_id [Numeric, String, nil]
|
56
|
+
# Display records after this ID for keyset pagination
|
57
|
+
# @return [ActiveRecord::Relation]
|
58
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
59
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
60
|
+
if after_scheduled_at.present? && after_id.present?
|
61
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
62
|
+
elsif after_scheduled_at.present?
|
63
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
64
|
+
end
|
65
|
+
query
|
66
|
+
end)
|
67
|
+
|
68
|
+
# The job's ActiveJob UUID
|
69
|
+
# @return [String]
|
70
|
+
def id
|
71
|
+
active_job_id
|
72
|
+
end
|
73
|
+
|
74
|
+
# The ActiveJob job class, as a string
|
75
|
+
# @return [String]
|
76
|
+
def job_class
|
77
|
+
serialized_params['job_class']
|
78
|
+
end
|
79
|
+
|
80
|
+
# The status of the Job, based on the state of its most recent execution.
|
81
|
+
# There are 3 buckets of non-overlapping statuses:
|
82
|
+
# 1. The job will be executed
|
83
|
+
# - queued: The job will execute immediately when an execution thread becomes available.
|
84
|
+
# - scheduled: The job is scheduled to execute in the future.
|
85
|
+
# - retried: The job previously errored on execution and will be re-executed in the future.
|
86
|
+
# 2. The job is being executed
|
87
|
+
# - running: the job is actively being executed by an execution thread
|
88
|
+
# 3. The job will not execute
|
89
|
+
# - finished: The job executed successfully
|
90
|
+
# - discarded: The job previously errored on execution and will not be re-executed in the future.
|
91
|
+
#
|
92
|
+
# @return [Symbol]
|
93
|
+
def status
|
94
|
+
execution = head_execution
|
95
|
+
if execution.finished_at.present?
|
96
|
+
if execution.error.present?
|
97
|
+
:discarded
|
98
|
+
else
|
99
|
+
:finished
|
100
|
+
end
|
101
|
+
elsif (execution.scheduled_at || execution.created_at) > DateTime.current
|
102
|
+
if execution.serialized_params.fetch('executions', 0) > 1
|
103
|
+
:retried
|
104
|
+
else
|
105
|
+
:scheduled
|
106
|
+
end
|
107
|
+
elsif running?
|
108
|
+
:running
|
109
|
+
else
|
110
|
+
:queued
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# This job's most recent {Execution}
|
115
|
+
# @param reload [Booelan] whether to reload executions
|
116
|
+
# @return [Execution]
|
117
|
+
def head_execution(reload: false)
|
118
|
+
executions.reload if reload
|
119
|
+
executions.load # memoize the results
|
120
|
+
executions.last
|
121
|
+
end
|
122
|
+
|
123
|
+
# This job's initial/oldest {Execution}
|
124
|
+
# @return [Execution]
|
125
|
+
def tail_execution
|
126
|
+
executions.first
|
127
|
+
end
|
128
|
+
|
129
|
+
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
130
|
+
# @return [Numeric]
|
131
|
+
def executions_count
|
132
|
+
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
133
|
+
# The execution count within serialized_params is not updated
|
134
|
+
# once the underlying execution has been executed.
|
135
|
+
if status.in? [:discarded, :finished, :running]
|
136
|
+
aj_count + 1
|
137
|
+
else
|
138
|
+
aj_count
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
143
|
+
# @return [Numeric]
|
144
|
+
def preserved_executions_count
|
145
|
+
executions.size
|
146
|
+
end
|
147
|
+
|
148
|
+
# The most recent error message.
|
149
|
+
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
150
|
+
# @return [String]
|
151
|
+
def recent_error
|
152
|
+
head_execution.error || executions[-2]&.error
|
153
|
+
end
|
154
|
+
|
155
|
+
# Tests whether the job is being executed right now.
|
156
|
+
# @return [Boolean]
|
157
|
+
def running?
|
158
|
+
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
159
|
+
if has_attribute?(:locktype)
|
160
|
+
self['locktype'].present?
|
161
|
+
else
|
162
|
+
advisory_locked?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Retry a job that has errored and been discarded.
|
167
|
+
# This action will create a new job {Execution} record.
|
168
|
+
# @return [ActiveJob::Base]
|
169
|
+
def retry_job
|
170
|
+
with_advisory_lock do
|
171
|
+
execution = head_execution(reload: true)
|
172
|
+
active_job = execution.active_job
|
173
|
+
|
174
|
+
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
175
|
+
raise ActionForStateMismatchError unless status == :discarded
|
176
|
+
|
177
|
+
# Update the executions count because the previous execution will not have been preserved
|
178
|
+
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
179
|
+
active_job.executions = (active_job.executions || 0) + 1
|
180
|
+
|
181
|
+
new_active_job = nil
|
182
|
+
GoodJob::CurrentThread.within do |current_thread|
|
183
|
+
current_thread.execution = execution
|
184
|
+
|
185
|
+
execution.class.transaction(joinable: false, requires_new: true) do
|
186
|
+
new_active_job = active_job.retry_job(wait: 0, error: error)
|
187
|
+
execution.save
|
188
|
+
end
|
189
|
+
end
|
190
|
+
new_active_job
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Discard a job so that it will not be executed further.
|
195
|
+
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
196
|
+
# @return [void]
|
197
|
+
def discard_job(message)
|
198
|
+
with_advisory_lock do
|
199
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
200
|
+
|
201
|
+
execution = head_execution(reload: true)
|
202
|
+
active_job = execution.active_job
|
203
|
+
|
204
|
+
job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
|
205
|
+
|
206
|
+
update_execution = proc do
|
207
|
+
execution.update(
|
208
|
+
finished_at: Time.current,
|
209
|
+
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
if active_job.respond_to?(:instrument)
|
214
|
+
active_job.send :instrument, :discard, error: job_error, &update_execution
|
215
|
+
else
|
216
|
+
update_execution.call
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
222
|
+
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
223
|
+
# @return [void]
|
224
|
+
def reschedule_job(scheduled_at = Time.current)
|
225
|
+
with_advisory_lock do
|
226
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
227
|
+
|
228
|
+
execution = head_execution(reload: true)
|
229
|
+
execution.update(scheduled_at: scheduled_at)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Utility method to determine which execution record is used to represent this job
|
234
|
+
# @return [String]
|
235
|
+
def _execution_id
|
236
|
+
attributes['id']
|
237
|
+
end
|
238
|
+
|
239
|
+
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
240
|
+
# @return [Boolean]
|
241
|
+
def _head?
|
242
|
+
_execution_id == head_execution(reload: true).id
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -38,7 +38,7 @@ module GoodJob
|
|
38
38
|
@notifier.recipients << [@scheduler, :create_thread]
|
39
39
|
@poller.recipients << [@scheduler, :create_thread]
|
40
40
|
|
41
|
-
@cron_manager = GoodJob::CronManager.new(@configuration.
|
41
|
+
@cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: Rails.application.initialized?) if @configuration.enable_cron?
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -64,10 +64,12 @@ module GoodJob
|
|
64
64
|
|
65
65
|
if execute_inline?
|
66
66
|
begin
|
67
|
-
execution.perform
|
67
|
+
result = execution.perform
|
68
68
|
ensure
|
69
69
|
execution.advisory_unlock
|
70
70
|
end
|
71
|
+
|
72
|
+
raise result.unhandled_error if result.unhandled_error
|
71
73
|
else
|
72
74
|
job_state = { queue_name: execution.queue_name }
|
73
75
|
job_state[:scheduled_at] = execution.scheduled_at if execution.scheduled_at
|
data/lib/good_job/cli.rb
CHANGED
@@ -91,7 +91,9 @@ module GoodJob
|
|
91
91
|
scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
92
92
|
notifier.recipients << [scheduler, :create_thread]
|
93
93
|
poller.recipients << [scheduler, :create_thread]
|
94
|
-
|
94
|
+
|
95
|
+
cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true) if configuration.enable_cron?
|
96
|
+
|
95
97
|
@stop_good_job_executable = false
|
96
98
|
%w[INT TERM].each do |signal|
|
97
99
|
trap(signal) { @stop_good_job_executable = true }
|
@@ -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] ||
|
@@ -165,6 +165,10 @@ module GoodJob
|
|
165
165
|
{}
|
166
166
|
end
|
167
167
|
|
168
|
+
def cron_entries
|
169
|
+
cron.map { |cron_key, params| GoodJob::CronEntry.new(params.merge(key: cron_key)) }
|
170
|
+
end
|
171
|
+
|
168
172
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
169
173
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
170
174
|
# @return [Integer]
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "concurrent/hash"
|
3
|
+
require "concurrent/scheduled_task"
|
4
|
+
require "fugit"
|
5
|
+
|
6
|
+
module GoodJob # :nodoc:
|
7
|
+
#
|
8
|
+
# A CronEntry represents a single scheduled item's properties.
|
9
|
+
#
|
10
|
+
class CronEntry
|
11
|
+
include ActiveModel::Model
|
12
|
+
|
13
|
+
attr_reader :params
|
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
|
+
|
26
|
+
def initialize(params = {})
|
27
|
+
@params = params
|
28
|
+
|
29
|
+
raise ArgumentError, "Invalid cron format: '#{cron}'" unless fugit.instance_of?(Fugit::Cron)
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
params.fetch(:key)
|
34
|
+
end
|
35
|
+
|
36
|
+
alias id key
|
37
|
+
alias to_param key
|
38
|
+
|
39
|
+
def job_class
|
40
|
+
params.fetch(:class)
|
41
|
+
end
|
42
|
+
|
43
|
+
def cron
|
44
|
+
params.fetch(:cron)
|
45
|
+
end
|
46
|
+
|
47
|
+
def set
|
48
|
+
params[:set]
|
49
|
+
end
|
50
|
+
|
51
|
+
def args
|
52
|
+
params[:args]
|
53
|
+
end
|
54
|
+
|
55
|
+
def description
|
56
|
+
params[:description]
|
57
|
+
end
|
58
|
+
|
59
|
+
def next_at
|
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
|
83
|
+
end
|
84
|
+
|
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
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def set_value
|
118
|
+
value = set || {}
|
119
|
+
value.respond_to?(:call) ? value.call : value
|
120
|
+
end
|
121
|
+
|
122
|
+
def args_value
|
123
|
+
value = args || []
|
124
|
+
value.respond_to?(:call) ? value.call : value
|
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
|
137
|
+
end
|
138
|
+
end
|
@@ -11,7 +11,7 @@ module GoodJob # :nodoc:
|
|
11
11
|
# @!attribute [r] instances
|
12
12
|
# @!scope class
|
13
13
|
# List of all instantiated CronManagers in the current process.
|
14
|
-
# @return [Array<GoodJob::
|
14
|
+
# @return [Array<GoodJob::CronManager>, nil]
|
15
15
|
cattr_reader :instances, default: [], instance_reader: false
|
16
16
|
|
17
17
|
# Task observer for cron task
|
@@ -26,13 +26,13 @@ module GoodJob # :nodoc:
|
|
26
26
|
|
27
27
|
# Execution configuration to be scheduled
|
28
28
|
# @return [Hash]
|
29
|
-
attr_reader :
|
29
|
+
attr_reader :cron_entries
|
30
30
|
|
31
|
-
# @param
|
31
|
+
# @param cron_entries [Array<CronEntry>]
|
32
32
|
# @param start_on_initialize [Boolean]
|
33
|
-
def initialize(
|
33
|
+
def initialize(cron_entries = [], start_on_initialize: false)
|
34
34
|
@running = false
|
35
|
-
@
|
35
|
+
@cron_entries = cron_entries
|
36
36
|
@tasks = Concurrent::Hash.new
|
37
37
|
|
38
38
|
self.class.instances << self
|
@@ -42,9 +42,11 @@ module GoodJob # :nodoc:
|
|
42
42
|
|
43
43
|
# Schedule tasks that will enqueue jobs based on their schedule
|
44
44
|
def start
|
45
|
-
ActiveSupport::Notifications.instrument("cron_manager_start.good_job",
|
45
|
+
ActiveSupport::Notifications.instrument("cron_manager_start.good_job", cron_entries: cron_entries) do
|
46
46
|
@running = true
|
47
|
-
|
47
|
+
cron_entries.each do |cron_entry|
|
48
|
+
create_task(cron_entry)
|
49
|
+
end
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
@@ -78,36 +80,20 @@ module GoodJob # :nodoc:
|
|
78
80
|
end
|
79
81
|
|
80
82
|
# Enqueues a scheduled task
|
81
|
-
# @param
|
82
|
-
def create_task(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
fugit = Fugit::Cron.parse(schedule.fetch(:cron))
|
87
|
-
delay = [(fugit.next_time - Time.current).to_f, 0].max
|
88
|
-
|
89
|
-
future = Concurrent::ScheduledTask.new(delay, args: [self, cron_key]) do |thr_scheduler, thr_cron_key|
|
83
|
+
# @param cron_entry [CronEntry] the CronEntry object to schedule
|
84
|
+
def create_task(cron_entry)
|
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|
|
90
88
|
# Re-schedule the next cron task before executing the current task
|
91
|
-
thr_scheduler.create_task(
|
92
|
-
|
93
|
-
CurrentThread.reset
|
94
|
-
CurrentThread.cron_key = thr_cron_key
|
89
|
+
thr_scheduler.create_task(thr_cron_entry)
|
95
90
|
|
96
91
|
Rails.application.executor.wrap do
|
97
|
-
|
98
|
-
job_class = schedule.fetch(:class).constantize
|
99
|
-
|
100
|
-
job_set_value = schedule.fetch(:set, {})
|
101
|
-
job_set = job_set_value.respond_to?(:call) ? job_set_value.call : job_set_value
|
102
|
-
|
103
|
-
job_args_value = schedule.fetch(:args, [])
|
104
|
-
job_args = job_args_value.respond_to?(:call) ? job_args_value.call : job_args_value
|
105
|
-
|
106
|
-
job_class.set(job_set).perform_later(*job_args)
|
92
|
+
cron_entry.enqueue(thr_cron_at)
|
107
93
|
end
|
108
94
|
end
|
109
95
|
|
110
|
-
@tasks[
|
96
|
+
@tasks[cron_entry.key] = future
|
111
97
|
future.add_observer(self.class, :task_observer)
|
112
98
|
future.execute
|
113
99
|
end
|
@@ -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
|