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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/README.md +51 -20
- data/engine/app/assets/vendor/rails_ujs.js +747 -0
- data/engine/app/controllers/good_job/assets_controller.rb +4 -0
- data/engine/app/controllers/good_job/base_controller.rb +8 -0
- data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
- data/engine/app/filters/good_job/base_filter.rb +12 -7
- data/engine/app/filters/good_job/executions_filter.rb +1 -1
- data/engine/app/filters/good_job/jobs_filter.rb +4 -2
- data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
- data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
- data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
- data/engine/app/views/good_job/executions/index.html.erb +1 -1
- data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +17 -5
- data/engine/app/views/good_job/jobs/index.html.erb +14 -1
- data/engine/app/views/good_job/jobs/show.html.erb +2 -2
- data/engine/app/views/good_job/shared/_filter.erb +9 -10
- data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -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 +3 -1
- data/engine/config/routes.rb +15 -2
- 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/active_job_job.rb +245 -0
- data/lib/good_job/adapter.rb +4 -2
- data/lib/good_job/cli.rb +3 -1
- data/lib/good_job/configuration.rb +5 -1
- data/lib/good_job/cron_entry.rb +138 -0
- data/lib/good_job/cron_manager.rb +17 -31
- data/lib/good_job/current_thread.rb +38 -5
- data/lib/good_job/execution.rb +50 -25
- 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 +21 -12
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
- data/engine/app/models/good_job/active_job_job.rb +0 -127
- data/engine/app/views/good_job/cron_schedules/index.html.erb +0 -50
@@ -23,6 +23,10 @@ module GoodJob
|
|
23
23
|
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.js")
|
24
24
|
end
|
25
25
|
|
26
|
+
def rails_ujs_js
|
27
|
+
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "rails_ujs.js")
|
28
|
+
end
|
29
|
+
|
26
30
|
def style_css
|
27
31
|
render file: GoodJob::Engine.root.join("app", "assets", "style.css")
|
28
32
|
end
|
@@ -2,5 +2,13 @@
|
|
2
2
|
module GoodJob
|
3
3
|
class BaseController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
4
4
|
protect_from_forgery with: :exception
|
5
|
+
|
6
|
+
around_action :switch_locale
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def switch_locale(&action)
|
11
|
+
I18n.with_locale(:en, &action)
|
12
|
+
end
|
5
13
|
end
|
6
14
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class CronEntriesController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@cron_entries = CronEntry.all
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
@cron_entry = CronEntry.find(params[:id])
|
10
|
+
@jobs_filter = JobsFilter.new(params, @cron_entry.jobs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def enqueue
|
14
|
+
@cron_entry = CronEntry.find(params[:id])
|
15
|
+
@cron_entry.enqueue(Time.current)
|
16
|
+
redirect_back(fallback_location: cron_entries_path, notice: "Cron entry has been enqueued.")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class JobsController < GoodJob::BaseController
|
4
|
+
rescue_from GoodJob::ActiveJobJob::AdapterNotGoodJobError,
|
5
|
+
GoodJob::ActiveJobJob::ActionForStateMismatchError,
|
6
|
+
with: :redirect_on_error
|
7
|
+
|
4
8
|
def index
|
5
9
|
@filter = JobsFilter.new(params)
|
6
10
|
end
|
@@ -10,5 +14,37 @@ module GoodJob
|
|
10
14
|
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
11
15
|
redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
12
16
|
end
|
17
|
+
|
18
|
+
def discard
|
19
|
+
@job = ActiveJobJob.find(params[:id])
|
20
|
+
@job.discard_job("Discarded through dashboard")
|
21
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been discarded")
|
22
|
+
end
|
23
|
+
|
24
|
+
def reschedule
|
25
|
+
@job = ActiveJobJob.find(params[:id])
|
26
|
+
@job.reschedule_job
|
27
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been rescheduled")
|
28
|
+
end
|
29
|
+
|
30
|
+
def retry
|
31
|
+
@job = ActiveJobJob.find(params[:id])
|
32
|
+
@job.retry_job
|
33
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been retried")
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def redirect_on_error(exception)
|
39
|
+
alert = case exception
|
40
|
+
when GoodJob::ActiveJobJob::AdapterNotGoodJobError
|
41
|
+
"ActiveJob Queue Adapter must be GoodJob."
|
42
|
+
when GoodJob::ActiveJobJob::ActionForStateMismatchError
|
43
|
+
"Job is not in an appropriate state for this action."
|
44
|
+
else
|
45
|
+
exception.to_s
|
46
|
+
end
|
47
|
+
redirect_back(fallback_location: jobs_path, alert: alert)
|
48
|
+
end
|
13
49
|
end
|
14
50
|
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class BaseFilter
|
4
|
-
|
4
|
+
DEFAULT_LIMIT = 25
|
5
5
|
|
6
|
-
|
6
|
+
attr_accessor :params, :base_query
|
7
|
+
|
8
|
+
def initialize(params, base_query = nil)
|
7
9
|
@params = params
|
10
|
+
@base_query = base_query || default_base_query
|
8
11
|
end
|
9
12
|
|
10
13
|
def records
|
@@ -13,7 +16,7 @@ module GoodJob
|
|
13
16
|
filtered_query.display_all(
|
14
17
|
after_scheduled_at: after_scheduled_at,
|
15
18
|
after_id: params[:after_id]
|
16
|
-
).limit(params.fetch(:limit,
|
19
|
+
).limit(params.fetch(:limit, DEFAULT_LIMIT))
|
17
20
|
end
|
18
21
|
|
19
22
|
def last
|
@@ -22,13 +25,13 @@ module GoodJob
|
|
22
25
|
|
23
26
|
def job_classes
|
24
27
|
base_query.group("serialized_params->>'job_class'").count
|
25
|
-
.sort_by { |name, _count| name }
|
28
|
+
.sort_by { |name, _count| name.to_s }
|
26
29
|
.to_h
|
27
30
|
end
|
28
31
|
|
29
32
|
def queues
|
30
33
|
base_query.group(:queue_name).count
|
31
|
-
.sort_by { |name, _count| name }
|
34
|
+
.sort_by { |name, _count| name.to_s }
|
32
35
|
.to_h
|
33
36
|
end
|
34
37
|
|
@@ -38,8 +41,10 @@ module GoodJob
|
|
38
41
|
|
39
42
|
def to_params(override)
|
40
43
|
{
|
41
|
-
state: params[:state],
|
42
44
|
job_class: params[:job_class],
|
45
|
+
limit: params[:limit],
|
46
|
+
queue_name: params[:queue_name],
|
47
|
+
state: params[:state],
|
43
48
|
}.merge(override).delete_if { |_, v| v.nil? }
|
44
49
|
end
|
45
50
|
|
@@ -90,7 +95,7 @@ module GoodJob
|
|
90
95
|
|
91
96
|
private
|
92
97
|
|
93
|
-
def
|
98
|
+
def default_base_query
|
94
99
|
raise NotImplementedError
|
95
100
|
end
|
96
101
|
|
@@ -14,12 +14,14 @@ module GoodJob
|
|
14
14
|
|
15
15
|
private
|
16
16
|
|
17
|
-
def
|
17
|
+
def default_base_query
|
18
18
|
GoodJob::ActiveJobJob.all
|
19
19
|
end
|
20
20
|
|
21
21
|
def filtered_query
|
22
|
-
query = base_query
|
22
|
+
query = base_query.includes(:executions)
|
23
|
+
.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
|
24
|
+
|
23
25
|
query = query.job_class(params[:job_class]) if params[:job_class]
|
24
26
|
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
25
27
|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<% if @cron_entries.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>Key</th>
|
7
|
+
<th>Schedule</th>
|
8
|
+
<th>
|
9
|
+
Properties
|
10
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
11
|
+
data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
|
12
|
+
aria: { expanded: false, controls: @cron_entries.map { |cron_entry| "##{dom_id(cron_entry, 'properties')}" }.join(" ") }
|
13
|
+
%>
|
14
|
+
</th>
|
15
|
+
<th>Description</th>
|
16
|
+
<th>Next scheduled</th>
|
17
|
+
<th>Last run</th>
|
18
|
+
<th>Actions</th>
|
19
|
+
</thead>
|
20
|
+
<tbody>
|
21
|
+
<% @cron_entries.each do |cron_entry| %>
|
22
|
+
<tr id="<%= dom_id(cron_entry) %>">
|
23
|
+
<td class="font-monospace"><%= cron_entry.key %></td>
|
24
|
+
<td class="font-monospace"><%= cron_entry.schedule %></td>
|
25
|
+
<td>
|
26
|
+
<%= tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
27
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
|
28
|
+
aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) %>
|
29
|
+
<%= tag.pre(JSON.pretty_generate(cron_entry.display_properties), id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties") %>
|
30
|
+
</td>
|
31
|
+
<td><%= cron_entry.description %></td>
|
32
|
+
<td><%= cron_entry.next_at %></td>
|
33
|
+
<td>
|
34
|
+
<% if cron_entry.last_job.present? %>
|
35
|
+
<%= link_to cron_entry.last_at, cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
|
36
|
+
<% end %>
|
37
|
+
</td>
|
38
|
+
<td>
|
39
|
+
<%= button_to enqueue_cron_entry_path(cron_entry.id), method: :post, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Run cron entry now" }, title: "Run cron entry now", data: { confirm: "Confirm run cron entry now" } do %>
|
40
|
+
<%= render "good_job/shared/icons/play" %>
|
41
|
+
<% end %>
|
42
|
+
</td>
|
43
|
+
</tr>
|
44
|
+
<% end %>
|
45
|
+
</tbody>
|
46
|
+
</table>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
<% else %>
|
50
|
+
<em>No cron schedules present.</em>
|
51
|
+
<% end %>
|
@@ -44,7 +44,7 @@
|
|
44
44
|
<%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
|
45
45
|
</td>
|
46
46
|
<td>
|
47
|
-
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
|
47
|
+
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
|
48
48
|
<%= render "good_job/shared/icons/trash" %>
|
49
49
|
<% end %>
|
50
50
|
</td>
|
@@ -5,7 +5,7 @@
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
6
|
|
7
7
|
<% if @filter.records.present? %>
|
8
|
-
<%= render 'good_job/
|
8
|
+
<%= render 'good_job/executions/table', executions: @filter.records %>
|
9
9
|
|
10
10
|
<nav aria-label="Job pagination" class="mt-3">
|
11
11
|
<ul class="pagination">
|
@@ -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", data: { confirm: "Confirm reschedule" } 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", data: { confirm: "Confirm discard" } 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", data: { confirm: "Confirm retry" } do %>
|
59
|
+
<%= render "good_job/shared/icons/arrow_clockwise" %>
|
60
|
+
<% end %>
|
61
|
+
</div>
|
62
|
+
</td>
|
51
63
|
</tr>
|
52
64
|
<% end %>
|
53
65
|
</tbody>
|
@@ -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/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 %>
|
@@ -1,3 +1,3 @@
|
|
1
|
-
<h1>ActiveJob ID: <code><%= @executions.first.id %></code></h1>
|
1
|
+
<h1 class="mb-3">ActiveJob ID: <code><%= @executions.first.id %></code></h1>
|
2
2
|
|
3
|
-
<%= render 'good_job/
|
3
|
+
<%= render 'good_job/executions/table', executions: @executions %>
|
@@ -1,16 +1,15 @@
|
|
1
1
|
<div class='card mb-2'>
|
2
2
|
<div class='card-body d-flex flex-wrap'>
|
3
|
-
|
4
3
|
<div class='me-4'>
|
5
4
|
<small>Filter by job class</small>
|
6
5
|
<br>
|
7
|
-
<%
|
6
|
+
<% filter.job_classes.each do |name, count| %>
|
8
7
|
<% if params[:job_class] == name %>
|
9
|
-
<%= link_to(
|
8
|
+
<%= link_to(filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
10
9
|
<%= name %> (<%= count %>)
|
11
10
|
<% end %>
|
12
11
|
<% else %>
|
13
|
-
<%= link_to(
|
12
|
+
<%= link_to(filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
14
13
|
<%= name %> (<%= count %>)
|
15
14
|
<% end %>
|
16
15
|
<% end %>
|
@@ -20,13 +19,13 @@
|
|
20
19
|
<div class='me-4'>
|
21
20
|
<small>Filter by state</small>
|
22
21
|
<br>
|
23
|
-
<%
|
22
|
+
<% filter.states.each do |name, count| %>
|
24
23
|
<% if params[:state] == name %>
|
25
|
-
<%= link_to(
|
24
|
+
<%= link_to(filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
26
25
|
<%= name %> (<%= count %>)
|
27
26
|
<% end %>
|
28
27
|
<% else %>
|
29
|
-
<%= link_to(
|
28
|
+
<%= link_to(filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
30
29
|
<%= name %> (<%= count %>)
|
31
30
|
<% end %>
|
32
31
|
<% end %>
|
@@ -36,13 +35,13 @@
|
|
36
35
|
<div>
|
37
36
|
<small>Filter by queue</small>
|
38
37
|
<br>
|
39
|
-
<%
|
38
|
+
<% filter.queues.each do |name, count| %>
|
40
39
|
<% if params[:queue_name] == name %>
|
41
|
-
<%= link_to(
|
40
|
+
<%= link_to(filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
42
41
|
<%= name %> (<%= count %>)
|
43
42
|
<% end %>
|
44
43
|
<% else %>
|
45
|
-
<%= link_to(
|
44
|
+
<%= link_to(filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
46
45
|
<%= name %> (<%= count %>)
|
47
46
|
<% end %>
|
48
47
|
<% end %>
|
@@ -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/play/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
|
3
|
+
<path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z" />
|
4
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/skip-forward/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-forward" viewBox="0 0 16 16">
|
3
|
+
<path d="M15.5 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V8.752l-6.267 3.636c-.52.302-1.233-.043-1.233-.696v-2.94l-6.267 3.636C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696L7.5 7.248v-2.94c0-.653.713-.998 1.233-.696L15 7.248V4a.5.5 0 0 1 .5-.5zM1 4.633v6.734L6.804 8 1 4.633zm7.5 0v6.734L14.304 8 8.5 4.633z" />
|
4
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/stop/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stop" viewBox="0 0 16 16">
|
3
|
+
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5H5z" />
|
4
|
+
</svg>
|
@@ -11,6 +11,7 @@
|
|
11
11
|
|
12
12
|
<%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
|
13
13
|
<%= javascript_include_tag chartist_path(format: :js, v: GoodJob::VERSION) %>
|
14
|
+
<%= javascript_include_tag rails_ujs_path(format: :js, v: GoodJob::VERSION) %>
|
14
15
|
</head>
|
15
16
|
<body>
|
16
17
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
@@ -29,7 +30,7 @@
|
|
29
30
|
<%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
|
30
31
|
</li>
|
31
32
|
<li class="nav-item">
|
32
|
-
<%= link_to "Cron Schedules",
|
33
|
+
<%= link_to "Cron Schedules", cron_entries_path, class: ["nav-link", ("active" if current_page?(cron_entries_path))] %>
|
33
34
|
</li>
|
34
35
|
<li class="nav-item">
|
35
36
|
<div class="nav-link">
|
@@ -49,6 +50,7 @@
|
|
49
50
|
</li>
|
50
51
|
-->
|
51
52
|
</ul>
|
53
|
+
<div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
|
52
54
|
</div>
|
53
55
|
</div>
|
54
56
|
</nav>
|
data/engine/config/routes.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
GoodJob::Engine.routes.draw do
|
3
3
|
root to: 'executions#index'
|
4
|
-
|
5
|
-
resources :
|
4
|
+
|
5
|
+
resources :cron_entries, only: %i[index show] do
|
6
|
+
member do
|
7
|
+
post :enqueue
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
resources :jobs, only: %i[index show] do
|
12
|
+
member do
|
13
|
+
put :discard
|
14
|
+
put :reschedule
|
15
|
+
put :retry
|
16
|
+
end
|
17
|
+
end
|
6
18
|
resources :executions, only: %i[destroy]
|
7
19
|
|
8
20
|
scope controller: :assets do
|
@@ -14,6 +26,7 @@ GoodJob::Engine.routes.draw do
|
|
14
26
|
|
15
27
|
constraints(format: :js) do
|
16
28
|
get :bootstrap, action: :bootstrap_js
|
29
|
+
get :rails_ujs, action: :rails_ujs_js
|
17
30
|
get :chartist, action: :chartist_js
|
18
31
|
end
|
19
32
|
end
|
@@ -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
|