good_job 2.4.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
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 %>