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.
- 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
|