good_job 2.13.1 → 2.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -3
  3. data/engine/app/assets/{modules → good_job/modules}/application.js +3 -1
  4. data/engine/app/assets/{modules → good_job/modules}/charts.js +0 -0
  5. data/engine/app/assets/good_job/modules/checkbox_toggle.js +51 -0
  6. data/engine/app/assets/{modules → good_job/modules}/document_ready.js +0 -0
  7. data/engine/app/assets/{modules → good_job/modules}/poller.js +0 -0
  8. data/engine/app/assets/{modules → good_job/modules}/toasts.js +0 -0
  9. data/engine/app/assets/good_job/scripts.js +3 -0
  10. data/engine/app/assets/{style.css → good_job/style.css} +0 -0
  11. data/engine/app/assets/{vendor → good_job/vendor}/bootstrap/bootstrap.bundle.min.js +0 -0
  12. data/engine/app/assets/{vendor → good_job/vendor}/bootstrap/bootstrap.min.css +0 -0
  13. data/engine/app/assets/{vendor → good_job/vendor}/chartjs/chart.min.js +0 -0
  14. data/engine/app/assets/{vendor → good_job/vendor}/es_module_shims.js +0 -0
  15. data/engine/app/assets/{vendor → good_job/vendor}/rails_ujs.js +0 -0
  16. data/engine/app/controllers/good_job/assets_controller.rb +8 -8
  17. data/engine/app/controllers/good_job/jobs_controller.rb +44 -1
  18. data/engine/app/filters/good_job/base_filter.rb +3 -0
  19. data/engine/app/filters/good_job/jobs_filter.rb +5 -2
  20. data/engine/app/helpers/good_job/application_helper.rb +6 -0
  21. data/engine/app/views/good_job/executions/_table.erb +1 -1
  22. data/engine/app/views/good_job/jobs/_table.erb +91 -62
  23. data/engine/app/views/good_job/jobs/index.html.erb +1 -1
  24. data/engine/config/routes.rb +5 -0
  25. data/lib/good_job/active_job_job.rb +9 -7
  26. data/lib/good_job/lockable.rb +10 -0
  27. data/lib/good_job/version.rb +1 -1
  28. metadata +15 -14
  29. data/engine/app/assets/scripts.js +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8b2b2738eb9ae693690016074fd4624a5c5cc78abd5d87d416202e5671281e
4
- data.tar.gz: dbb67bc97bdeca64de1a6bb9b7c1fd16bf51a5fa048bf1d87ca637860d6e80eb
3
+ metadata.gz: 06d4bc71856f5762f7adc99e3ce5227df56f17176a83be5b539df1bc0be71294
4
+ data.tar.gz: 8cacdbd09e3938eb0ffdc549bf2e003fadb81d879ec1c3c552d0715d81907036
5
5
  SHA512:
6
- metadata.gz: 4bb91a8ab93b89c82084f58198d6dcdbcab5e44e3b42cdf6eb2f94fdd895bf126d146cfd6e314ea69830ed0013dd66796c727c2a2b48e0a7456089252b987faa
7
- data.tar.gz: 6b86d012b1deb3c71f5cfd4bb69ac680eee1b18cb35849d24adc0ad8ef2b4689c217b39984b4bf7af3075395bf3464d0c1fd1198e7f67c083b1ddcbd32950bb5
6
+ metadata.gz: b5d219e39b433103abab4b747ddc2b519cc77371ad409008c7df6954d546f6a72b6460b3a0d27a23cc14d4c09cc1ffbc9de74d26e6aad9eeeed8aee9d33aba95
7
+ data.tar.gz: b20b54072e87ffc298b397a6fbe32fb0b6c02573ff1c94ac4dacca9ae4efe845aa4def15d7b9677c746d5e29d3a7bfa9280684416a2f88a12ac9f49780e685dc
data/CHANGELOG.md CHANGED
@@ -1,9 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.14.1](https://github.com/bensheldon/good_job/tree/v2.14.1) (2022-04-26)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.14.0...v2.14.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Temporarily disable Mass Action "Apply to all" because the action is badly scoped [\#583](https://github.com/bensheldon/good_job/pull/583) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ ## [v2.14.0](https://github.com/bensheldon/good_job/tree/v2.14.0) (2022-04-26)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.2...v2.14.0)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - Add mass update operations for jobs to Dashboard [\#578](https://github.com/bensheldon/good_job/pull/578) ([bensheldon](https://github.com/bensheldon))
18
+
19
+ **Closed issues:**
20
+
21
+ - Allow "mass"-actions through Dashboard \(e.g. retry all\) [\#446](https://github.com/bensheldon/good_job/issues/446)
22
+
23
+ **Merged pull requests:**
24
+
25
+ - Track down incompatibility/race condition between JRuby and RSpec mocks in tests [\#581](https://github.com/bensheldon/good_job/pull/581) ([bensheldon](https://github.com/bensheldon))
26
+
27
+ ## [v2.13.2](https://github.com/bensheldon/good_job/tree/v2.13.2) (2022-04-25)
28
+
29
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.1...v2.13.2)
30
+
31
+ **Fixed bugs:**
32
+
33
+ - Namespaces assets per Rails docs [\#580](https://github.com/bensheldon/good_job/pull/580) ([kylekthompson](https://github.com/kylekthompson))
34
+
3
35
  ## [v2.13.1](https://github.com/bensheldon/good_job/tree/v2.13.1) (2022-04-22)
4
36
 
5
37
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.0...v2.13.1)
6
38
 
39
+ **Implemented enhancements:**
40
+
41
+ - Dashboard: Use toasts to show notices and alerts [\#577](https://github.com/bensheldon/good_job/pull/577) ([bkeepers](https://github.com/bkeepers))
42
+ - Remove executions from the dashboard [\#576](https://github.com/bensheldon/good_job/pull/576) ([bkeepers](https://github.com/bkeepers))
43
+
7
44
  **Fixed bugs:**
8
45
 
9
46
  - `ActionMailer::MailDeliveryJob` executing twice [\#329](https://github.com/bensheldon/good_job/issues/329)
@@ -18,8 +55,6 @@
18
55
 
19
56
  **Merged pull requests:**
20
57
 
21
- - Dashboard: Use toasts to show notices and alerts [\#577](https://github.com/bensheldon/good_job/pull/577) ([bkeepers](https://github.com/bkeepers))
22
- - Remove executions from the dashboard [\#576](https://github.com/bensheldon/good_job/pull/576) ([bkeepers](https://github.com/bkeepers))
23
58
  - Use javascript importmaps for Dashboard [\#574](https://github.com/bensheldon/good_job/pull/574) ([bensheldon](https://github.com/bensheldon))
24
59
 
25
60
  ## [v2.13.0](https://github.com/bensheldon/good_job/tree/v2.13.0) (2022-04-19)
@@ -42,10 +77,13 @@
42
77
 
43
78
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.12.1...v2.12.2)
44
79
 
80
+ **Fixed bugs:**
81
+
82
+ - Un-deprecate Adapter's `execution_mode` argument [\#567](https://github.com/bensheldon/good_job/pull/567) ([bensheldon](https://github.com/bensheldon))
83
+
45
84
  **Merged pull requests:**
46
85
 
47
86
  - Dashboard: added NL translations [\#568](https://github.com/bensheldon/good_job/pull/568) ([eelcoj](https://github.com/eelcoj))
48
- - Un-deprecate Adapter's `execution_mode` argument [\#567](https://github.com/bensheldon/good_job/pull/567) ([bensheldon](https://github.com/bensheldon))
49
87
 
50
88
  ## [v2.12.1](https://github.com/bensheldon/good_job/tree/v2.12.1) (2022-04-18)
51
89
 
@@ -1,12 +1,14 @@
1
1
  /*jshint esversion: 6, strict: false */
2
2
 
3
+ import renderCharts from "charts";
4
+ import checkboxToggle from "checkbox_toggle";
3
5
  import documentReady from "document_ready";
4
6
  import showToasts from "toasts";
5
- import renderCharts from "charts";
6
7
  import Poller from "poller";
7
8
 
8
9
  documentReady(function() {
9
10
  renderCharts();
10
11
  showToasts();
12
+ checkboxToggle();
11
13
  Poller.start();
12
14
  });
@@ -0,0 +1,51 @@
1
+ /*jshint esversion: 6, strict: false */
2
+
3
+ // How to use:
4
+ //<form data-checkbox-toggle="{key}">
5
+ // <input type="checkbox" data-checkbox-toggle-all="{key}" />
6
+ //
7
+ // <input type="checkbox" data-checkbox-toggle-each="{key}" />
8
+ // <input type="checkbox" data-checkbox-toggle-each="{key}" />
9
+ // ...
10
+
11
+ export default function checkboxToggle() {
12
+ document.querySelectorAll("form[data-checkbox-toggle]").forEach(function (form) {
13
+ const keyName = form.dataset.checkboxToggle;
14
+ const checkboxToggle = form.querySelector(`input[type=checkbox][data-checkbox-toggle-all=${keyName}]`);
15
+ const checkboxes = form.querySelectorAll(`input[type=checkbox][data-checkbox-toggle-each=${keyName}]`);
16
+ const showables = form.querySelectorAll(`[data-checkbox-toggle-show=${keyName}]`);
17
+
18
+ // Check or uncheck all checkboxes
19
+ checkboxToggle.addEventListener("change", function (event) {
20
+ checkboxes.forEach(function (checkbox) {
21
+ checkbox.checked = checkboxToggle.checked;
22
+ });
23
+
24
+ showables.forEach(function (showable) {
25
+ showable.classList.toggle("d-none", !checkboxToggle.checked);
26
+ showable.disabled = ! checkboxToggle.checked;
27
+ });
28
+ });
29
+
30
+ // check or uncheck the "all" checkbox when all checkboxes are checked or unchecked
31
+ form.addEventListener("change", function (event) {
32
+ if (!event.target.matches(`input[type=checkbox][data-checkbox-toggle-each=${keyName}]`)) {
33
+ return;
34
+ }
35
+ const checkedCount = Array.from(checkboxes).filter(function (checkbox) {
36
+ return checkbox.checked;
37
+ }).length;
38
+
39
+ const allChecked = checkedCount === checkboxes.length;
40
+ const indeterminateChecked = !allChecked && checkedCount > 0;
41
+
42
+ checkboxToggle.checked = allChecked;
43
+ checkboxToggle.indeterminate = indeterminateChecked;
44
+
45
+ showables.forEach(function (showable) {
46
+ showable.classList.toggle("d-none", !allChecked);
47
+ showable.disabled = !allChecked;
48
+ });
49
+ });
50
+ });
51
+ }
@@ -0,0 +1,3 @@
1
+ /*jshint esversion: 6, strict: false */
2
+
3
+ import "application"; // ./modules/good_job/application.js
File without changes
@@ -4,7 +4,7 @@ module GoodJob
4
4
  skip_before_action :verify_authenticity_token, raise: false
5
5
 
6
6
  def self.js_modules
7
- @_js_modules ||= GoodJob::Engine.root.join("app", "assets", "modules").children.select(&:file?).each_with_object({}) do |file, modules|
7
+ @_js_modules ||= GoodJob::Engine.root.join("app", "assets", "good_job", "modules").children.select(&:file?).each_with_object({}) do |file, modules|
8
8
  key = File.basename(file.basename.to_s, ".js").to_sym
9
9
  modules[key] = file
10
10
  end
@@ -15,31 +15,31 @@ module GoodJob
15
15
  end
16
16
 
17
17
  def es_module_shims_js
18
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "es_module_shims.js")
18
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "vendor", "es_module_shims.js")
19
19
  end
20
20
 
21
21
  def bootstrap_css
22
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.min.css")
22
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "vendor", "bootstrap", "bootstrap.min.css")
23
23
  end
24
24
 
25
25
  def bootstrap_js
26
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.bundle.min.js")
26
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "vendor", "bootstrap", "bootstrap.bundle.min.js")
27
27
  end
28
28
 
29
29
  def chartjs_js
30
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartjs", "chart.min.js")
30
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "vendor", "chartjs", "chart.min.js")
31
31
  end
32
32
 
33
33
  def rails_ujs_js
34
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "rails_ujs.js")
34
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "vendor", "rails_ujs.js")
35
35
  end
36
36
 
37
37
  def scripts_js
38
- render file: GoodJob::Engine.root.join("app", "assets", "scripts.js")
38
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "scripts.js")
39
39
  end
40
40
 
41
41
  def style_css
42
- render file: GoodJob::Engine.root.join("app", "assets", "style.css")
42
+ render file: GoodJob::Engine.root.join("app", "assets", "good_job", "style.css")
43
43
  end
44
44
 
45
45
  def modules_js
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  class JobsController < GoodJob::ApplicationController
4
+ DISCARD_MESSAGE = "Discarded through dashboard"
5
+
6
+ ACTIONS = {
7
+ discard: "discarded",
8
+ reschedule: "rescheduled",
9
+ retry: "retried",
10
+ }.freeze
11
+
4
12
  rescue_from GoodJob::ActiveJobJob::AdapterNotGoodJobError,
5
13
  GoodJob::ActiveJobJob::ActionForStateMismatchError,
6
14
  with: :redirect_on_error
@@ -9,6 +17,41 @@ module GoodJob
9
17
  @filter = JobsFilter.new(params)
10
18
  end
11
19
 
20
+ def mass_update
21
+ mass_action = params.fetch(:mass_action, "").to_sym
22
+ raise ActionController::BadRequest, "#{mass_action} is not a valid mass action" unless mass_action.in?(ACTIONS.keys)
23
+
24
+ jobs = if params[:all_job_ids]
25
+ ActiveJobJob.all
26
+ else
27
+ job_ids = params.fetch(:job_ids, [])
28
+ ActiveJobJob.where(active_job_id: job_ids)
29
+ end
30
+
31
+ processed_jobs = jobs.map do |job|
32
+ case mass_action
33
+ when :discard
34
+ job.discard_job(DISCARD_MESSAGE)
35
+ when :reschedule
36
+ job.reschedule_job
37
+ when :retry
38
+ job.retry_job
39
+ end
40
+
41
+ job
42
+ rescue GoodJob::ActiveJobJob::ActionForStateMismatchError
43
+ nil
44
+ end.compact
45
+
46
+ notice = if processed_jobs.any?
47
+ "Successfully #{ACTIONS[mass_action]} #{processed_jobs.count} #{'job'.pluralize(processed_jobs.count)}"
48
+ else
49
+ "No jobs were #{ACTIONS[mass_action]}"
50
+ end
51
+
52
+ redirect_to jobs_path, notice: notice
53
+ end
54
+
12
55
  def show
13
56
  @executions = GoodJob::Execution.active_job_id(params[:id])
14
57
  .order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
@@ -17,7 +60,7 @@ module GoodJob
17
60
 
18
61
  def discard
19
62
  @job = ActiveJobJob.find(params[:id])
20
- @job.discard_job("Discarded through dashboard")
63
+ @job.discard_job(DISCARD_MESSAGE)
21
64
  redirect_back(fallback_location: jobs_path, notice: "Job has been discarded")
22
65
  end
23
66
 
@@ -54,6 +54,9 @@ module GoodJob
54
54
  raise NotImplementedError
55
55
  end
56
56
 
57
+ # def filtered_query_count
58
+ delegate :count, to: :filtered_query, prefix: true
59
+
57
60
  private
58
61
 
59
62
  def default_base_query
@@ -13,8 +13,7 @@ module GoodJob
13
13
  end
14
14
 
15
15
  def filtered_query
16
- query = base_query.includes(:executions)
17
- .joins_advisory_locks.select("#{GoodJob::ActiveJobJob.table_name}.*", 'pg_locks.locktype AS locktype')
16
+ query = base_query.includes(:executions).includes_advisory_locks
18
17
 
19
18
  query = query.job_class(params[:job_class]) if params[:job_class].present?
20
19
  query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
@@ -40,6 +39,10 @@ module GoodJob
40
39
  query
41
40
  end
42
41
 
42
+ def filtered_query_count
43
+ filtered_query.unscope(:select).count
44
+ end
45
+
43
46
  private
44
47
 
45
48
  def default_base_query
@@ -20,5 +20,11 @@ module GoodJob
20
20
 
21
21
  content_tag :span, status.to_s, class: classes
22
22
  end
23
+
24
+ def render_icon(name)
25
+ # workaround to render svg icons without all of the log messages
26
+ partial = lookup_context.find_template("good_job/shared/icons/#{name}", [], true)
27
+ partial.render(self, {})
28
+ end
23
29
  end
24
30
  end
@@ -46,7 +46,7 @@
46
46
  </td>
47
47
  <td>
48
48
  <%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
49
- <%= render "good_job/shared/icons/trash" %>
49
+ <%= render_icon "trash" %>
50
50
  <% end %>
51
51
  </td>
52
52
  </tr>
@@ -1,72 +1,101 @@
1
1
  <div class="my-3" data-gj-poll-replace id="jobs-table">
2
2
  <div class="table-responsive">
3
- <table class="table table-hover table-sm mb-0">
4
- <thead>
5
- <tr>
6
- <th>ActiveJob ID</th>
7
- <th>State</th>
8
- <th>Job Class</th>
9
- <th>Queue</th>
10
- <th>Scheduled At</th>
11
- <th>Executions</th>
12
- <th>Error</th>
13
- <th>
14
- ActiveJob Params&nbsp;
15
- <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
16
- data: { bs_toggle: "collapse", bs_target: ".job-params" },
17
- aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
18
- %>
19
- </th>
20
- <th>Actions</th>
21
- </tr>
22
- </thead>
23
- <tbody>
24
- <% if jobs.present? %>
25
- <% jobs.each do |job| %>
26
- <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
27
- <td>
28
- <%= link_to job_path(job.id) do %>
29
- <code><%= job.id %></code>
3
+ <%= form_with(url: mass_update_jobs_path, method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
4
+ <table class="table table-hover table-sm mb-0">
5
+ <thead>
6
+ <tr>
7
+ <th><%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %></th>
8
+ <th>ActiveJob ID</th>
9
+ <th>State</th>
10
+ <th>Job Class</th>
11
+ <th>Queue</th>
12
+ <th>Scheduled At</th>
13
+ <th>Executions</th>
14
+ <th>Error</th>
15
+ <th>
16
+ ActiveJob Params&nbsp;
17
+ <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
18
+ data: { bs_toggle: "collapse", bs_target: ".job-params" },
19
+ aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
20
+ %>
21
+ </th>
22
+ <th>
23
+ Actions<br>
24
+
25
+ <div class="d-inline text-nowrap">
26
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'btn btn-sm btn-outline-primary', title: "Reschedule all", data: { confirm: "Confirm reschedule all", disable: true } do %>
27
+ <%= render_icon "skip_forward" %> All
30
28
  <% end %>
31
- </td>
32
- <td><%= status_badge(job.status) %></td>
33
- <td><%= job.job_class %></td>
34
- <td><%= job.queue_name %></td>
35
- <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
36
- <td><%= job.executions_count %></td>
37
- <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
38
- <td>
39
- <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
40
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
41
- aria: { expanded: false, controls: dom_id(job, "params") }
42
- %>
43
- <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
44
- </td>
45
- <td>
46
- <div class="text-nowrap">
47
- <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
48
- <%= 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 %>
49
- <%= render "good_job/shared/icons/skip_forward" %>
50
- <% end %>
51
29
 
52
- <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
53
- <%= 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 %>
54
- <%= render "good_job/shared/icons/stop" %>
55
- <% end %>
30
+ <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-primary', title: "Discard all", data: { confirm: "Confirm discard all", disable: true } do %>
31
+ <%= render_icon "stop" %> All
32
+ <% end %>
56
33
 
57
- <%= 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 %>
58
- <%= render "good_job/shared/icons/arrow_clockwise" %>
34
+ <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-primary', title: "Retry all", data: { confirm: "Confirm retry all", disable: true } do %>
35
+ <%= render_icon "arrow_clockwise" %> All
36
+ <% end %>
37
+ </div>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <% if jobs.present? %>
42
+ <% jobs.each do |job| %>
43
+ <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
44
+ <td><%= check_box_tag 'job_ids[]', job.id, false, data: { "checkbox-toggle-each": "job_ids" } %></td>
45
+ <td>
46
+ <%= link_to job_path(job.id) do %>
47
+ <code><%= job.id %></code>
59
48
  <% end %>
60
- </div>
61
- </td>
49
+ </td>
50
+ <td><%= status_badge(job.status) %></td>
51
+ <td><%= job.job_class %></td>
52
+ <td><%= job.queue_name %></td>
53
+ <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
54
+ <td><%= job.executions_count %></td>
55
+ <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
56
+ <td>
57
+ <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
58
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
59
+ aria: { expanded: false, controls: dom_id(job, "params") }
60
+ %>
61
+ <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
62
+ </td>
63
+ <td>
64
+ <div class="text-nowrap">
65
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
66
+ <%= link_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
67
+ <%= render_icon "skip_forward" %>
68
+ <% end %>
69
+ <% else %>
70
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "skip_forward" %></button>
71
+ <% end %>
72
+
73
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
74
+ <%= link_to discard_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
75
+ <%= render_icon "stop" %>
76
+ <% end %>
77
+ <% else %>
78
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "stop" %></button>
79
+ <% end %>
80
+
81
+ <% if job.status == :discarded %>
82
+ <%= link_to retry_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
83
+ <%= render_icon "arrow_clockwise" %>
84
+ <% end %>
85
+ <% else %>
86
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "arrow_clockwise" %></button>
87
+ <% end %>
88
+ </div>
89
+ </td>
90
+ </tr>
91
+ <% end %>
92
+ <% else %>
93
+ <tr>
94
+ <td colspan="10" class="py-2 text-center text-muted">No jobs found.</td>
62
95
  </tr>
63
96
  <% end %>
64
- <% else %>
65
- <tr>
66
- <td colspan="8" class="py-2 text-center text-muted">No jobs found.</td>
67
- </tr>
68
- <% end %>
69
- </tbody>
70
- </table>
97
+ </tbody>
98
+ </table>
99
+ <% end %>
71
100
  </div>
72
101
  </div>
@@ -1,6 +1,6 @@
1
1
  <%= render 'good_job/shared/filter', title: "Jobs", filter: @filter %>
2
2
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
- <%= render 'good_job/jobs/table', jobs: @filter.records %>
3
+ <%= render 'good_job/jobs/table', jobs: @filter.records, all_jobs_count: @filter.filtered_query_count %>
4
4
 
5
5
  <% if @filter.records.present? %>
6
6
  <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
@@ -5,6 +5,11 @@ GoodJob::Engine.routes.draw do
5
5
  resources :executions, only: %i[destroy]
6
6
 
7
7
  resources :jobs, only: %i[index show] do
8
+ collection do
9
+ get :mass_update, to: redirect(path: 'jobs')
10
+ put :mass_update
11
+ end
12
+
8
13
  member do
9
14
  put :discard
10
15
  put :reschedule
@@ -148,7 +148,7 @@ module GoodJob
148
148
  # Tests whether the job is being executed right now.
149
149
  # @return [Boolean]
150
150
  def running?
151
- # Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
151
+ # Avoid N+1 Query: `.includes_advisory_locks`
152
152
  if has_attribute?(:locktype)
153
153
  self['locktype'].present?
154
154
  else
@@ -157,7 +157,7 @@ module GoodJob
157
157
  end
158
158
 
159
159
  # Retry a job that has errored and been discarded.
160
- # This action will create a new job {Execution} record.
160
+ # This action will create a new {Execution} record for the job.
161
161
  # @return [ActiveJob::Base]
162
162
  def retry_job
163
163
  with_advisory_lock do
@@ -165,7 +165,7 @@ module GoodJob
165
165
  active_job = execution.active_job
166
166
 
167
167
  raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
168
- raise ActionForStateMismatchError unless status == :discarded
168
+ raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
169
169
 
170
170
  # Update the executions count because the previous execution will not have been preserved
171
171
  # Do not update `exception_executions` because that comes from rescue_from's arguments
@@ -176,7 +176,7 @@ module GoodJob
176
176
  current_thread.execution = execution
177
177
 
178
178
  execution.class.transaction(joinable: false, requires_new: true) do
179
- new_active_job = active_job.retry_job(wait: 0, error: error)
179
+ new_active_job = active_job.retry_job(wait: 0, error: execution.error)
180
180
  execution.save
181
181
  end
182
182
  end
@@ -189,11 +189,11 @@ module GoodJob
189
189
  # @return [void]
190
190
  def discard_job(message)
191
191
  with_advisory_lock do
192
- raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
193
-
194
192
  execution = head_execution(reload: true)
195
193
  active_job = execution.active_job
196
194
 
195
+ raise ActionForStateMismatchError if execution.finished_at.present?
196
+
197
197
  job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
198
198
 
199
199
  update_execution = proc do
@@ -216,7 +216,9 @@ module GoodJob
216
216
  # @return [void]
217
217
  def reschedule_job(scheduled_at = Time.current)
218
218
  with_advisory_lock do
219
- raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
219
+ execution = head_execution(reload: true)
220
+
221
+ raise ActionForStateMismatchError if execution.finished_at.present?
220
222
 
221
223
  execution = head_execution(reload: true)
222
224
  execution.update(scheduled_at: scheduled_at)
@@ -82,6 +82,16 @@ module GoodJob
82
82
  joins(sanitize_sql_for_conditions([join_sql, { table_name: table_name }]))
83
83
  end)
84
84
 
85
+ # Joins the current query with Postgres's +pg_locks+ table AND SELECTs the resulting columns
86
+ # @!method joins_advisory_locks(column: _advisory_lockable_column)
87
+ # @!scope class
88
+ # @param column [String, Symbol] column values to Advisory Lock against
89
+ # @return [ActiveRecord::Relation]
90
+ scope :includes_advisory_locks, (lambda do |column: _advisory_lockable_column|
91
+ owns_advisory_lock_sql = "#{connection.quote_table_name('pg_locks')}.#{connection.quote_column_name('pid')} = pg_backend_pid() AS owns_advisory_lock"
92
+ joins_advisory_locks(column: column).select("#{quoted_table_name}.*, #{connection.quote_table_name('pg_locks')}.locktype, #{owns_advisory_lock_sql}")
93
+ end)
94
+
85
95
  # Find records that do not have an advisory lock on them.
86
96
  # @!method advisory_unlocked(column: _advisory_lockable_column)
87
97
  # @!scope class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.13.1'
4
+ VERSION = '2.14.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.1
4
+ version: 2.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-22 00:00:00.000000000 Z
11
+ date: 2022-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -360,18 +360,19 @@ files:
360
360
  - CHANGELOG.md
361
361
  - LICENSE.txt
362
362
  - README.md
363
- - engine/app/assets/modules/application.js
364
- - engine/app/assets/modules/charts.js
365
- - engine/app/assets/modules/document_ready.js
366
- - engine/app/assets/modules/poller.js
367
- - engine/app/assets/modules/toasts.js
368
- - engine/app/assets/scripts.js
369
- - engine/app/assets/style.css
370
- - engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js
371
- - engine/app/assets/vendor/bootstrap/bootstrap.min.css
372
- - engine/app/assets/vendor/chartjs/chart.min.js
373
- - engine/app/assets/vendor/es_module_shims.js
374
- - engine/app/assets/vendor/rails_ujs.js
363
+ - engine/app/assets/good_job/modules/application.js
364
+ - engine/app/assets/good_job/modules/charts.js
365
+ - engine/app/assets/good_job/modules/checkbox_toggle.js
366
+ - engine/app/assets/good_job/modules/document_ready.js
367
+ - engine/app/assets/good_job/modules/poller.js
368
+ - engine/app/assets/good_job/modules/toasts.js
369
+ - engine/app/assets/good_job/scripts.js
370
+ - engine/app/assets/good_job/style.css
371
+ - engine/app/assets/good_job/vendor/bootstrap/bootstrap.bundle.min.js
372
+ - engine/app/assets/good_job/vendor/bootstrap/bootstrap.min.css
373
+ - engine/app/assets/good_job/vendor/chartjs/chart.min.js
374
+ - engine/app/assets/good_job/vendor/es_module_shims.js
375
+ - engine/app/assets/good_job/vendor/rails_ujs.js
375
376
  - engine/app/charts/good_job/scheduled_by_queue_chart.rb
376
377
  - engine/app/controllers/good_job/application_controller.rb
377
378
  - engine/app/controllers/good_job/assets_controller.rb
@@ -1,3 +0,0 @@
1
- /*jshint esversion: 6, strict: false */
2
-
3
- import "application"; // ./modules/application.js