good_job 2.3.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -1
  3. data/README.md +50 -19
  4. data/engine/app/controllers/good_job/base_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -1
  6. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  7. data/engine/app/filters/good_job/base_filter.rb +6 -2
  8. data/engine/app/filters/good_job/jobs_filter.rb +3 -1
  9. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  10. data/engine/app/models/good_job/active_job_job.rb +130 -12
  11. data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
  12. data/engine/app/views/good_job/jobs/index.html.erb +14 -1
  13. data/engine/app/views/good_job/shared/_executions_table.erb +1 -1
  14. data/engine/app/views/good_job/shared/_jobs_table.erb +18 -6
  15. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  16. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  17. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  18. data/engine/app/views/layouts/good_job/base.html.erb +2 -1
  19. data/engine/config/routes.rb +7 -1
  20. data/lib/generators/good_job/install_generator.rb +6 -0
  21. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
  22. data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
  23. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  24. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  25. data/lib/generators/good_job/update_generator.rb +6 -0
  26. data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
  27. data/lib/good_job/adapter.rb +4 -2
  28. data/lib/good_job/cli.rb +3 -1
  29. data/lib/good_job/configuration.rb +4 -0
  30. data/lib/good_job/cron_entry.rb +67 -0
  31. data/lib/good_job/cron_manager.rb +20 -30
  32. data/lib/good_job/current_thread.rb +15 -0
  33. data/lib/good_job/execution.rb +37 -14
  34. data/lib/good_job/lockable.rb +1 -1
  35. data/lib/good_job/log_subscriber.rb +3 -3
  36. data/lib/good_job/scheduler.rb +1 -0
  37. data/lib/good_job/version.rb +1 -1
  38. metadata +9 -3
@@ -4,4 +4,17 @@
4
4
 
5
5
  <%= render 'good_job/shared/filter', filter: @filter %>
6
6
 
7
- <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
7
+ <% if @filter.records.present? %>
8
+ <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
9
+ <nav aria-label="Job pagination" class="mt-3">
10
+ <ul class="pagination">
11
+ <li class="page-item">
12
+ <%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
13
+ Older jobs <span aria-hidden="true">&raquo;</span>
14
+ <% end %>
15
+ </li>
16
+ </ul>
17
+ </nav>
18
+ <% else %>
19
+ <em>No jobs present.</em>
20
+ <% end %>
@@ -34,7 +34,7 @@
34
34
  </td>
35
35
  <td><%= execution.serialized_params['job_class'] %></td>
36
36
  <td><%= execution.queue_name %></td>
37
- <td><%= execution.scheduled_at || execution.created_at %></td>
37
+ <td><%= relative_time(execution.scheduled_at || execution.created_at) %></td>
38
38
  <td class="text-break"><%= truncate(execution.error, length: 1_000) %></td>
39
39
  <td>
40
40
  <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
@@ -33,7 +33,7 @@
33
33
  </td>
34
34
  <td><%= job.job_class %></td>
35
35
  <td><%= job.queue_name %></td>
36
- <td><%= job.scheduled_at || job.created_at %></td>
36
+ <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
37
37
  <td><%= job.executions_count %></td>
38
38
  <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
39
39
  <td>
@@ -43,11 +43,23 @@
43
43
  %>
44
44
  <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
45
45
  </td>
46
- <!-- <td>-->
47
- <%#= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
48
- <%#= render "good_job/shared/icons/trash" %>
49
- <%# end %>
50
- <!-- </td>-->
46
+ <td>
47
+ <div class="text-nowrap">
48
+ <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
49
+ <%= button_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_reschedulable, aria: { label: "Reschedule job" }, title: "Reschedule job" do %>
50
+ <%= render "good_job/shared/icons/skip_forward" %>
51
+ <% end %>
52
+
53
+ <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
54
+ <%= button_to discard_job_path(job.id), method: :put, class: "btn btn-sm #{job_discardable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_discardable, aria: { label: "Discard job" }, title: "Discard job" do %>
55
+ <%= render "good_job/shared/icons/stop" %>
56
+ <% end %>
57
+
58
+ <%= button_to retry_job_path(job.id), method: :put, class: "btn btn-sm #{job.status == :discarded ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: job.status != :discarded, aria: { label: "Retry job" }, title: "Retry job" do %>
59
+ <%= render "good_job/shared/icons/arrow_clockwise" %>
60
+ <% end %>
61
+ </div>
62
+ </td>
51
63
  </tr>
52
64
  <% end %>
53
65
  </tbody>
@@ -0,0 +1,5 @@
1
+ <!-- https://icons.getbootstrap.com/icons/arrow-clockwise/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
3
+ <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
4
+ <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
5
+ </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>
@@ -49,6 +49,7 @@
49
49
  </li>
50
50
  -->
51
51
  </ul>
52
+ <div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
52
53
  </div>
53
54
  </div>
54
55
  </nav>
@@ -68,7 +69,7 @@
68
69
  </div>
69
70
  <% elsif alert %>
70
71
  <div class="alert alert-warning alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
71
- <%= render "good_job/shared/icons/check", class: "flex-shrink-0 me-2" %>
72
+ <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 me-2" %>
72
73
  <div><%= alert %></div>
73
74
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
74
75
  </div>
@@ -2,7 +2,13 @@
2
2
  GoodJob::Engine.routes.draw do
3
3
  root to: 'executions#index'
4
4
  resources :cron_schedules, only: %i[index]
5
- resources :jobs, only: %i[index show]
5
+ resources :jobs, only: %i[index show] do
6
+ member do
7
+ put :discard
8
+ put :reschedule
9
+ put :retry
10
+ end
11
+ end
6
12
  resources :executions, only: %i[destroy]
7
13
 
8
14
  scope controller: :assets do
@@ -19,5 +19,11 @@ module GoodJob
19
19
  def create_migration_file
20
20
  migration_template 'migrations/create_good_jobs.rb.erb', File.join(db_migrate_path, "create_good_jobs.rb")
21
21
  end
22
+
23
+ private
24
+
25
+ def migration_version
26
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
27
+ end
22
28
  end
23
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- class CreateGoodJobs < ActiveRecord::Migration[5.2]
2
+ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  enable_extension 'pgcrypto'
5
5
 
@@ -18,6 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration[5.2]
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[5.2]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- class CreateGoodJobs < ActiveRecord::Migration[5.2]
2
+ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  enable_extension 'pgcrypto'
5
5
 
@@ -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
@@ -24,5 +24,11 @@ module GoodJob
24
24
  migration_template "migrations/#{template_file}", File.join(db_migrate_path, destination_file), skip: true
25
25
  end
26
26
  end
27
+
28
+ private
29
+
30
+ def migration_version
31
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
32
+ end
27
33
  end
28
34
  end
@@ -32,10 +32,9 @@ module GoodJob
32
32
 
33
33
  GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
34
34
  enqueue_concurrency = if enqueue_limit
35
- # TODO: Why is `unscoped` necessary? Nested scope is bleeding into subsequent query?
36
- GoodJob::Execution.unscoped.where(concurrency_key: key).unfinished.advisory_unlocked.count
35
+ GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
37
36
  else
38
- GoodJob::Execution.unscoped.where(concurrency_key: key).unfinished.count
37
+ GoodJob::Execution.where(concurrency_key: key).unfinished.count
39
38
  end
40
39
 
41
40
  # The job has not yet been enqueued, so check if adding it will go over the limit
@@ -63,7 +62,7 @@ module GoodJob
63
62
  next if key.blank?
64
63
 
65
64
  GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
66
- allowed_active_job_ids = GoodJob::Execution.unscoped.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
65
+ allowed_active_job_ids = GoodJob::Execution.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
67
66
  # The current job has already been locked and will appear in the previous query
68
67
  raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
69
68
  end
@@ -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.cron, start_on_initialize: Rails.application.initialized?) if @configuration.enable_cron?
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
- cron_manager = GoodJob::CronManager.new(configuration.cron, start_on_initialize: true) if configuration.enable_cron?
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 }
@@ -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,67 @@
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 initialize(params = {})
16
+ @params = params.with_indifferent_access
17
+ end
18
+
19
+ def key
20
+ params.fetch(:key)
21
+ end
22
+ alias id key
23
+
24
+ def job_class
25
+ params.fetch(:class)
26
+ end
27
+
28
+ def cron
29
+ params.fetch(:cron)
30
+ end
31
+
32
+ def set
33
+ params[:set]
34
+ end
35
+
36
+ def args
37
+ params[:args]
38
+ end
39
+
40
+ def description
41
+ params[:description]
42
+ end
43
+
44
+ def next_at
45
+ fugit = Fugit::Cron.parse(cron)
46
+ fugit.next_time.to_t
47
+ end
48
+
49
+ def enqueue
50
+ job_class.constantize.set(set_value).perform_later(*args_value)
51
+ rescue ActiveRecord::RecordNotUnique
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ def set_value
58
+ value = set || {}
59
+ value.respond_to?(:call) ? value.call : value
60
+ end
61
+
62
+ def args_value
63
+ value = args || []
64
+ value.respond_to?(:call) ? value.call : value
65
+ end
66
+ end
67
+ 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::CronManagers>, nil]
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 :schedules
29
+ attr_reader :cron_entries
30
30
 
31
- # @param schedules [Hash]
31
+ # @param cron_entries [Array<CronEntry>]
32
32
  # @param start_on_initialize [Boolean]
33
- def initialize(schedules = {}, start_on_initialize: false)
33
+ def initialize(cron_entries = [], start_on_initialize: false)
34
34
  @running = false
35
- @schedules = schedules
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", cron_jobs: @schedules) do
45
+ ActiveSupport::Notifications.instrument("cron_manager_start.good_job", cron_entries: cron_entries) do
46
46
  @running = true
47
- schedules.each_key { |cron_key| create_task(cron_key) }
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,24 @@ module GoodJob # :nodoc:
78
80
  end
79
81
 
80
82
  # Enqueues a scheduled task
81
- # @param cron_key [Symbol, String] the key within the schedule to use
82
- def create_task(cron_key)
83
- schedule = @schedules[cron_key]
84
- return false if schedule.blank?
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(thr_cron_key)
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
- schedule = thr_scheduler.schedules.fetch(thr_cron_key).with_indifferent_access
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
92
+ CurrentThread.reset
93
+ CurrentThread.cron_key = thr_cron_entry.key
94
+ CurrentThread.cron_at = thr_cron_at
105
95
 
106
- job_class.set(job_set).perform_later(*job_args)
96
+ cron_entry.enqueue
107
97
  end
108
98
  end
109
99
 
110
- @tasks[cron_key] = future
100
+ @tasks[cron_entry.key] = future
111
101
  future.add_observer(self.class, :task_observer)
112
102
  future.execute
113
103
  end
@@ -5,6 +5,12 @@ 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
+ # @!attribute [rw] cron_at
9
+ # @!scope class
10
+ # Cron At
11
+ # @return [DateTime, nil]
12
+ thread_mattr_accessor :cron_at
13
+
8
14
  # @!attribute [rw] cron_key
9
15
  # @!scope class
10
16
  # Cron Key
@@ -32,6 +38,7 @@ module GoodJob
32
38
  # Resets attributes
33
39
  # @return [void]
34
40
  def self.reset
41
+ self.cron_at = nil
35
42
  self.cron_key = nil
36
43
  self.execution = nil
37
44
  self.error_on_discard = nil
@@ -52,5 +59,13 @@ module GoodJob
52
59
  def self.thread_name
53
60
  (Thread.current.name || Thread.current.object_id).to_s
54
61
  end
62
+
63
+ # @return [void]
64
+ def self.within
65
+ reset
66
+ yield(self)
67
+ ensure
68
+ reset
69
+ end
55
70
  end
56
71
  end
@@ -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,19 +294,27 @@ 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
312
  GoodJob::CurrentThread.reset
287
313
  GoodJob::CurrentThread.execution = self
288
314
 
289
- job_data = serialized_params.deep_dup
290
- job_data["provider_job_id"] = id
291
-
292
315
  # DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
293
316
  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)
317
+ value = ActiveJob::Base.execute(active_job_data)
295
318
 
296
319
  if value.is_a?(Exception)
297
320
  handled_error = value
@@ -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.3.1'
4
+ VERSION = '2.5.0'
5
5
  end