good_job 2.3.1 → 2.5.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 +90 -1
- data/README.md +50 -19
- data/engine/app/controllers/good_job/base_controller.rb +8 -0
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -1
- data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
- data/engine/app/filters/good_job/base_filter.rb +6 -2
- data/engine/app/filters/good_job/jobs_filter.rb +3 -1
- data/engine/app/helpers/good_job/application_helper.rb +4 -0
- data/engine/app/models/good_job/active_job_job.rb +130 -12
- data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
- data/engine/app/views/good_job/jobs/index.html.erb +14 -1
- data/engine/app/views/good_job/shared/_executions_table.erb +1 -1
- data/engine/app/views/good_job/shared/_jobs_table.erb +18 -6
- data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -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 +2 -1
- data/engine/config/routes.rb +7 -1
- 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/adapter.rb +4 -2
- data/lib/good_job/cli.rb +3 -1
- data/lib/good_job/configuration.rb +4 -0
- data/lib/good_job/cron_entry.rb +67 -0
- data/lib/good_job/cron_manager.rb +20 -30
- data/lib/good_job/current_thread.rb +15 -0
- data/lib/good_job/execution.rb +37 -14
- 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 +9 -3
@@ -4,4 +4,17 @@
|
|
4
4
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
6
|
|
7
|
-
|
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">»</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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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/
|
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>
|
data/engine/config/routes.rb
CHANGED
@@ -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
|
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
|
@@ -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
|
@@ -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
|
-
|
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.
|
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.
|
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
|
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 }
|
@@ -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::
|
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,24 @@ 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
|
-
|
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
|
-
|
96
|
+
cron_entry.enqueue
|
107
97
|
end
|
108
98
|
end
|
109
99
|
|
110
|
-
@tasks[
|
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
|
data/lib/good_job/execution.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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 =
|
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(
|
317
|
+
value = ActiveJob::Base.execute(active_job_data)
|
295
318
|
|
296
319
|
if value.is_a?(Exception)
|
297
320
|
handled_error = value
|
data/lib/good_job/lockable.rb
CHANGED
@@ -59,11 +59,11 @@ module GoodJob
|
|
59
59
|
|
60
60
|
# @!macro notification_responder
|
61
61
|
def cron_manager_start(event)
|
62
|
-
|
63
|
-
cron_jobs_count =
|
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} #{'
|
66
|
+
"GoodJob started cron with #{cron_jobs_count} #{'job'.pluralize(cron_jobs_count)}."
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
data/lib/good_job/scheduler.rb
CHANGED
@@ -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
|
data/lib/good_job/version.rb
CHANGED