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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +51 -20
  4. data/engine/app/assets/vendor/rails_ujs.js +747 -0
  5. data/engine/app/controllers/good_job/assets_controller.rb +4 -0
  6. data/engine/app/controllers/good_job/base_controller.rb +8 -0
  7. data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
  8. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  9. data/engine/app/filters/good_job/base_filter.rb +12 -7
  10. data/engine/app/filters/good_job/executions_filter.rb +1 -1
  11. data/engine/app/filters/good_job/jobs_filter.rb +4 -2
  12. data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
  13. data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
  14. data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
  15. data/engine/app/views/good_job/executions/index.html.erb +1 -1
  16. data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +17 -5
  17. data/engine/app/views/good_job/jobs/index.html.erb +14 -1
  18. data/engine/app/views/good_job/jobs/show.html.erb +2 -2
  19. data/engine/app/views/good_job/shared/_filter.erb +9 -10
  20. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  21. data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
  22. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  23. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  24. data/engine/app/views/layouts/good_job/base.html.erb +3 -1
  25. data/engine/config/routes.rb +15 -2
  26. data/lib/generators/good_job/install_generator.rb +6 -0
  27. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
  28. data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
  29. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  30. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  31. data/lib/generators/good_job/update_generator.rb +6 -0
  32. data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
  33. data/lib/good_job/active_job_job.rb +245 -0
  34. data/lib/good_job/adapter.rb +4 -2
  35. data/lib/good_job/cli.rb +3 -1
  36. data/lib/good_job/configuration.rb +5 -1
  37. data/lib/good_job/cron_entry.rb +138 -0
  38. data/lib/good_job/cron_manager.rb +17 -31
  39. data/lib/good_job/current_thread.rb +38 -5
  40. data/lib/good_job/execution.rb +50 -25
  41. data/lib/good_job/lockable.rb +1 -1
  42. data/lib/good_job/log_subscriber.rb +3 -3
  43. data/lib/good_job/scheduler.rb +1 -0
  44. data/lib/good_job/version.rb +1 -1
  45. metadata +21 -12
  46. data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
  47. data/engine/app/models/good_job/active_job_job.rb +0 -127
  48. data/engine/app/views/good_job/cron_schedules/index.html.erb +0 -50
@@ -10,6 +10,9 @@ module GoodJob
10
10
  # Raised if something attempts to execute a previously completed Execution again.
11
11
  PreviouslyPerformedError = Class.new(StandardError)
12
12
 
13
+ # String separating Error Class from Error Message
14
+ ERROR_MESSAGE_SEPARATOR = ": "
15
+
13
16
  # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
14
17
  DEFAULT_QUEUE_NAME = 'default'
15
18
  # ActiveJob jobs without a +priority+ attribute are given this priority.
@@ -50,6 +53,16 @@ module GoodJob
50
53
  end
51
54
  end
52
55
 
56
+ def self._migration_pending_warning
57
+ ActiveSupport::Deprecation.warn(<<~DEPRECATION)
58
+ GoodJob has pending database migrations. To create the migration files, run:
59
+ rails generate good_job:update
60
+ To apply the migration files, run:
61
+ rails db:migrate
62
+ DEPRECATION
63
+ nil
64
+ end
65
+
53
66
  # Get Jobs with given ActiveJob ID
54
67
  # @!method active_job_id
55
68
  # @!scope class
@@ -174,13 +187,7 @@ module GoodJob
174
187
  break if execution.blank?
175
188
  break :unlocked unless execution&.executable?
176
189
 
177
- begin
178
- execution.with_advisory_lock(key: "good_jobs-#{execution.active_job_id}") do
179
- execution.perform
180
- end
181
- rescue RecordAlreadyAdvisoryLockedError => e
182
- ExecutionResult.new(value: nil, handled_error: e)
183
- end
190
+ execution.perform
184
191
  end
185
192
  end
186
193
 
@@ -228,7 +235,15 @@ module GoodJob
228
235
 
229
236
  if CurrentThread.cron_key
230
237
  execution_args[:cron_key] = CurrentThread.cron_key
231
- elsif CurrentThread.active_job_id == active_job.job_id
238
+
239
+ @cron_at_index = column_names.include?('cron_at') && connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) unless instance_variable_defined?(:@cron_at_index)
240
+
241
+ if @cron_at_index
242
+ execution_args[:cron_at] = CurrentThread.cron_at
243
+ else
244
+ _migration_pending_warning
245
+ end
246
+ elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
232
247
  execution_args[:cron_key] = CurrentThread.execution.cron_key
233
248
  end
234
249
 
@@ -239,7 +254,7 @@ module GoodJob
239
254
  execution.save!
240
255
  active_job.provider_job_id = execution.id
241
256
 
242
- CurrentThread.execution.retried_good_job_id = execution.id if CurrentThread.execution && CurrentThread.execution.active_job_id == active_job.job_id
257
+ CurrentThread.execution.retried_good_job_id = execution.id if CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
243
258
 
244
259
  execution
245
260
  end
@@ -259,7 +274,7 @@ module GoodJob
259
274
  result = execute
260
275
 
261
276
  job_error = result.handled_error || result.unhandled_error
262
- self.error = "#{job_error.class}: #{job_error.message}" if job_error
277
+ self.error = [job_error.class, ERROR_MESSAGE_SEPARATOR, job_error.message].join if job_error
263
278
 
264
279
  if result.unhandled_error && GoodJob.retry_on_unhandled_error
265
280
  save!
@@ -279,29 +294,39 @@ module GoodJob
279
294
  self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
280
295
  end
281
296
 
297
+ def active_job
298
+ ActiveJob::Base.deserialize(active_job_data)
299
+ end
300
+
282
301
  private
283
302
 
303
+ def active_job_data
304
+ serialized_params.deep_dup
305
+ .tap do |job_data|
306
+ job_data["provider_job_id"] = id
307
+ end
308
+ end
309
+
284
310
  # @return [ExecutionResult]
285
311
  def execute
286
- GoodJob::CurrentThread.reset
287
- GoodJob::CurrentThread.execution = self
312
+ GoodJob::CurrentThread.within do |current_thread|
313
+ current_thread.reset
314
+ current_thread.execution = self
288
315
 
289
- job_data = serialized_params.deep_dup
290
- job_data["provider_job_id"] = id
316
+ # DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
317
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
318
+ value = ActiveJob::Base.execute(active_job_data)
291
319
 
292
- # DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
293
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, execution: self, process_id: GoodJob::CurrentThread.process_id, thread_name: GoodJob::CurrentThread.thread_name }) do
294
- value = ActiveJob::Base.execute(job_data)
320
+ if value.is_a?(Exception)
321
+ handled_error = value
322
+ value = nil
323
+ end
324
+ handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
295
325
 
296
- if value.is_a?(Exception)
297
- handled_error = value
298
- value = nil
326
+ ExecutionResult.new(value: value, handled_error: handled_error)
327
+ rescue StandardError => e
328
+ ExecutionResult.new(value: nil, unhandled_error: e)
299
329
  end
300
- handled_error ||= GoodJob::CurrentThread.error_on_retry || GoodJob::CurrentThread.error_on_discard
301
-
302
- ExecutionResult.new(value: value, handled_error: handled_error)
303
- rescue StandardError => e
304
- ExecutionResult.new(value: nil, unhandled_error: e)
305
330
  end
306
331
  end
307
332
  end
@@ -149,7 +149,7 @@ module GoodJob
149
149
 
150
150
  records = advisory_lock(column: column, function: function).to_a
151
151
  begin
152
- yield(records)
152
+ unscoped { yield(records) }
153
153
  ensure
154
154
  if unlock_session
155
155
  advisory_unlock_session
@@ -59,11 +59,11 @@ module GoodJob
59
59
 
60
60
  # @!macro notification_responder
61
61
  def cron_manager_start(event)
62
- cron_jobs = event.payload[:cron_jobs]
63
- cron_jobs_count = cron_jobs.size
62
+ cron_entries = event.payload[:cron_entries]
63
+ cron_jobs_count = cron_entries.size
64
64
 
65
65
  info do
66
- "GoodJob started cron with #{cron_jobs_count} #{'jobs'.pluralize(cron_jobs_count)}."
66
+ "GoodJob started cron with #{cron_jobs_count} #{'job'.pluralize(cron_jobs_count)}."
67
67
  end
68
68
  end
69
69
 
@@ -230,6 +230,7 @@ module GoodJob # :nodoc:
230
230
  # @return [void]
231
231
  def create_task(delay = 0)
232
232
  future = Concurrent::ScheduledTask.new(delay, args: [performer], executor: executor, timer_set: timer_set) do |thr_performer|
233
+ Thread.current.name = Thread.current.name.sub("-worker-", "-thread-") if Thread.current.name
233
234
  Rails.application.reloader.wrap do
234
235
  thr_performer.next
235
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.4.0'
4
+ VERSION = '2.6.0'
5
5
  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: 2.4.0
4
+ version: 2.6.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: 2021-10-02 00:00:00.000000000 Z
11
+ date: 2021-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -126,16 +126,16 @@ dependencies:
126
126
  name: capybara
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: 3.35.0
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: 3.35.0
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: database_cleaner
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -351,26 +351,31 @@ files:
351
351
  - engine/app/assets/vendor/bootstrap/bootstrap.min.css
352
352
  - engine/app/assets/vendor/chartist/chartist.css
353
353
  - engine/app/assets/vendor/chartist/chartist.js
354
+ - engine/app/assets/vendor/rails_ujs.js
354
355
  - engine/app/controllers/good_job/assets_controller.rb
355
356
  - engine/app/controllers/good_job/base_controller.rb
356
- - engine/app/controllers/good_job/cron_schedules_controller.rb
357
+ - engine/app/controllers/good_job/cron_entries_controller.rb
357
358
  - engine/app/controllers/good_job/executions_controller.rb
358
359
  - engine/app/controllers/good_job/jobs_controller.rb
359
360
  - engine/app/filters/good_job/base_filter.rb
360
361
  - engine/app/filters/good_job/executions_filter.rb
361
362
  - engine/app/filters/good_job/jobs_filter.rb
362
363
  - engine/app/helpers/good_job/application_helper.rb
363
- - engine/app/models/good_job/active_job_job.rb
364
- - engine/app/views/good_job/cron_schedules/index.html.erb
364
+ - engine/app/views/good_job/cron_entries/index.html.erb
365
+ - engine/app/views/good_job/cron_entries/show.html.erb
366
+ - engine/app/views/good_job/executions/_table.erb
365
367
  - engine/app/views/good_job/executions/index.html.erb
368
+ - engine/app/views/good_job/jobs/_table.erb
366
369
  - engine/app/views/good_job/jobs/index.html.erb
367
370
  - engine/app/views/good_job/jobs/show.html.erb
368
371
  - engine/app/views/good_job/shared/_chart.erb
369
- - engine/app/views/good_job/shared/_executions_table.erb
370
372
  - engine/app/views/good_job/shared/_filter.erb
371
- - engine/app/views/good_job/shared/_jobs_table.erb
373
+ - engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb
372
374
  - engine/app/views/good_job/shared/icons/_check.html.erb
373
375
  - engine/app/views/good_job/shared/icons/_exclamation.html.erb
376
+ - engine/app/views/good_job/shared/icons/_play.html.erb
377
+ - engine/app/views/good_job/shared/icons/_skip_forward.html.erb
378
+ - engine/app/views/good_job/shared/icons/_stop.html.erb
374
379
  - engine/app/views/good_job/shared/icons/_trash.html.erb
375
380
  - engine/app/views/layouts/good_job/base.html.erb
376
381
  - engine/config/routes.rb
@@ -379,14 +384,18 @@ files:
379
384
  - lib/active_job/queue_adapters/good_job_adapter.rb
380
385
  - lib/generators/good_job/install_generator.rb
381
386
  - lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb
382
- - lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb
387
+ - lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb
388
+ - lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb
389
+ - lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb
383
390
  - lib/generators/good_job/update_generator.rb
384
391
  - lib/good_job.rb
385
392
  - lib/good_job/active_job_extensions.rb
386
393
  - lib/good_job/active_job_extensions/concurrency.rb
394
+ - lib/good_job/active_job_job.rb
387
395
  - lib/good_job/adapter.rb
388
396
  - lib/good_job/cli.rb
389
397
  - lib/good_job/configuration.rb
398
+ - lib/good_job/cron_entry.rb
390
399
  - lib/good_job/cron_manager.rb
391
400
  - lib/good_job/current_thread.rb
392
401
  - lib/good_job/daemon.rb
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
- module GoodJob
3
- class CronSchedulesController < GoodJob::BaseController
4
- def index
5
- configuration = GoodJob::Configuration.new({})
6
- @cron_schedules = configuration.cron
7
- end
8
- end
9
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
- module GoodJob
3
- # ActiveRecord model that represents an +ActiveJob+ job.
4
- # Is the same record data as a {GoodJob::Execution} but only the most recent execution.
5
- # Parent class can be configured with +GoodJob.active_record_parent_class+.
6
- # @!parse
7
- # class ActiveJob < ActiveRecord::Base; end
8
- class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
9
- include GoodJob::Lockable
10
-
11
- self.table_name = 'good_jobs'
12
- self.primary_key = 'active_job_id'
13
- self.advisory_lockable_column = 'active_job_id'
14
-
15
- has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
16
-
17
- # Only the most-recent unretried execution represents a "Job"
18
- default_scope { where(retried_good_job_id: nil) }
19
-
20
- # Get Jobs with given class name
21
- # @!method job_class
22
- # @!scope class
23
- # @param string [String]
24
- # Execution class name
25
- # @return [ActiveRecord::Relation]
26
- scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
27
-
28
- # First execution will run in the future
29
- scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
30
- # Execution errored, will run in the future
31
- scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
32
- # Immediate/Scheduled time to run has passed, waiting for an available thread run
33
- scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
34
- # Advisory locked and executing
35
- scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
36
- # Completed executing successfully
37
- scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
38
- # Errored but will not be retried
39
- scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
40
-
41
- # Get Jobs in display order with optional keyset pagination.
42
- # @!method display_all(after_scheduled_at: nil, after_id: nil)
43
- # @!scope class
44
- # @param after_scheduled_at [DateTime, String, nil]
45
- # Display records scheduled after this time for keyset pagination
46
- # @param after_id [Numeric, String, nil]
47
- # Display records after this ID for keyset pagination
48
- # @return [ActiveRecord::Relation]
49
- scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
50
- query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
51
- if after_scheduled_at.present? && after_id.present?
52
- 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)
53
- elsif after_scheduled_at.present?
54
- query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
55
- end
56
- query
57
- end)
58
-
59
- def id
60
- active_job_id
61
- end
62
-
63
- def _execution_id
64
- attributes['id']
65
- end
66
-
67
- def job_class
68
- serialized_params['job_class']
69
- end
70
-
71
- def status
72
- if finished_at.present?
73
- if error.present?
74
- :discarded
75
- else
76
- :finished
77
- end
78
- elsif (scheduled_at || created_at) > DateTime.current
79
- if serialized_params.fetch('executions', 0) > 1
80
- :retried
81
- else
82
- :scheduled
83
- end
84
- elsif running?
85
- :running
86
- else
87
- :queued
88
- end
89
- end
90
-
91
- def head_execution
92
- executions.last
93
- end
94
-
95
- def tail_execution
96
- executions.first
97
- end
98
-
99
- def executions_count
100
- aj_count = serialized_params.fetch('executions', 0)
101
- # The execution count within serialized_params is not updated
102
- # once the underlying execution has been executed.
103
- if status.in? [:discarded, :finished, :running]
104
- aj_count + 1
105
- else
106
- aj_count
107
- end
108
- end
109
-
110
- def preserved_executions_count
111
- executions.size
112
- end
113
-
114
- def recent_error
115
- error.presence || executions[-2]&.error
116
- end
117
-
118
- def running?
119
- # Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
120
- if has_attribute?(:locktype)
121
- self['locktype'].present?
122
- else
123
- advisory_locked?
124
- end
125
- end
126
- end
127
- end
@@ -1,50 +0,0 @@
1
- <% if @cron_schedules.present? %>
2
- <div class="card my-3">
3
- <div class="table-responsive">
4
- <table class="table card-table table-bordered table-hover table-sm mb-0">
5
- <thead>
6
- <th>Cron Job Name</th>
7
- <th>Configuration</th>
8
- <th>
9
- Set&nbsp;
10
- <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
11
- data: { bs_toggle: "collapse", bs_target: ".job-properties" },
12
- aria: { expanded: false, controls: @cron_schedules.map { |job_key, _| "##{job_key.to_param}" }.join(" ") }
13
- %>
14
- </th>
15
- <th>Class</th>
16
- <th>Description</th>
17
- <th>Next scheduled</th>
18
- </thead>
19
- <tbody>
20
- <% @cron_schedules.each do |job_key, job| %>
21
- <tr>
22
- <td class="font-monospace"><%= job_key %></td>
23
- <td class="font-monospace"><%= job[:cron] %></td>
24
- <td>
25
- <%=
26
- case job[:set]
27
- when NilClass
28
- "None"
29
- when Proc
30
- "Lambda/Callable"
31
- when Hash
32
- tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
33
- data: { bs_toggle: "collapse", bs_target: "##{job_key.to_param}" },
34
- aria: { expanded: false, controls: job_key.to_param }) +
35
- tag.pre(JSON.pretty_generate(job[:set]), id: job_key.to_param, class: "collapse job-properties")
36
- end
37
- %>
38
- </td>
39
- <td class="font-monospace"><%= job[:class] %></td>
40
- <td><%= job[:description] %></td>
41
- <td><%= Fugit.parse_cron(job[:cron]).next_time.to_local_time %></td>
42
- </tr>
43
- <% end %>
44
- </tbody>
45
- </table>
46
- </div>
47
- </div>
48
- <% else %>
49
- <em>No cron jobs present.</em>
50
- <% end %>