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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +12 -4
  4. data/engine/app/assets/scripts.js +1 -0
  5. data/engine/app/assets/style.css +5 -0
  6. data/engine/app/assets/vendor/chartjs/chart.min.js +13 -0
  7. data/engine/app/assets/vendor/rails_ujs.js +747 -0
  8. data/engine/app/charts/good_job/scheduled_by_queue_chart.rb +69 -0
  9. data/engine/app/controllers/good_job/assets_controller.rb +8 -4
  10. data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
  11. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  12. data/engine/app/filters/good_job/base_filter.rb +18 -56
  13. data/engine/app/filters/good_job/executions_filter.rb +9 -8
  14. data/engine/app/filters/good_job/jobs_filter.rb +12 -9
  15. data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
  16. data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
  17. data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
  18. data/engine/app/views/good_job/executions/index.html.erb +2 -2
  19. data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +18 -6
  20. data/engine/app/views/good_job/jobs/index.html.erb +15 -2
  21. data/engine/app/views/good_job/jobs/show.html.erb +2 -2
  22. data/engine/app/views/good_job/shared/_chart.erb +19 -46
  23. data/engine/app/views/good_job/shared/_filter.erb +27 -13
  24. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  25. data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
  26. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  27. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  28. data/engine/app/views/layouts/good_job/base.html.erb +6 -4
  29. data/engine/config/routes.rb +17 -4
  30. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +2 -0
  31. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  32. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  33. data/lib/good_job/active_job_job.rb +228 -0
  34. data/lib/good_job/configuration.rb +1 -1
  35. data/lib/good_job/cron_entry.rb +78 -5
  36. data/lib/good_job/cron_manager.rb +4 -6
  37. data/lib/good_job/current_thread.rb +38 -5
  38. data/lib/good_job/execution.rb +53 -39
  39. data/lib/good_job/filterable.rb +42 -0
  40. data/lib/good_job/notifier.rb +17 -7
  41. data/lib/good_job/version.rb +1 -1
  42. metadata +31 -21
  43. data/engine/app/assets/vendor/chartist/chartist.css +0 -613
  44. data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
  45. data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
  46. data/engine/app/models/good_job/active_job_job.rb +0 -127
  47. 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 chartist_path(format: :js, v: GoodJob::VERSION) %>
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", cron_schedules_path, class: ["nav-link", ("active" if current_page?(cron_schedules_path))] %>
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">
@@ -1,20 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
3
  root to: 'executions#index'
4
- resources :cron_schedules, only: %i[index]
5
- resources :jobs, only: %i[index show]
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 :chartist, action: :chartist_js
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] ||
@@ -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.with_indifferent_access
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 = Fugit::Cron.parse(cron)
46
- fugit.next_time
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
- job_class.constantize.set(set_value).perform_later(*args_value)
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
- delay = [(cron_entry.next_at - Time.current).to_f, 0].max
86
- future = Concurrent::ScheduledTask.new(delay, args: [self, cron_entry]) do |thr_scheduler, thr_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|
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
- CurrentThread.reset
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
- self.cron_key = nil
36
- self.execution = nil
37
- self.error_on_discard = nil
38
- self.error_on_retry = nil
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