good_job 2.13.2 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fe80504394d81f996c4d912ec4dc0b7c6750b5ecfd949af863a2d6087af68e8
4
- data.tar.gz: 4c19aab29e610f5a73fe4682ac9824a6d8894e9713bc2c12e2373153093558b1
3
+ metadata.gz: 6f311db9741be3dc51422a0bc3dd6dc3ec3a63cb2e570a8584d4cd2eaeea2565
4
+ data.tar.gz: 025cb4a2ebc678e3a052edf515d12bfb828dd23ab5d25e71debb9162ff41c34c
5
5
  SHA512:
6
- metadata.gz: da255714957639ca3aafae09d0e8d4bba4e7e36ad1cd370b696b67170d88e16525f4e41751e306b5d671f74fffe36873283ad3f667a3183e532c86e255648353
7
- data.tar.gz: 0fdf220cc5e6856eac99aef51aa5cd906596f6359c688bcb831d594f7a2355c37346bf63637715fd3669351a952f188f89ce24a01a47c561bcdabb28abdc5f86
6
+ metadata.gz: 4ef6d3f5d653481e1307f1c1f50371471fc7387f64b987d856d2b7bdf104b24d435060d9cb2d4d11fcc81e0808fecd564faf7ee2cc52c10c9c910626990818a9
7
+ data.tar.gz: 7c414de9a084fb505e8ff7306af76dfe026fce8427462c8c9bb051a82be2a80219685af1c4c37ec220c86f1dbd10c23f1651f611883348018224d9e62286467d
data/CHANGELOG.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.14.0](https://github.com/bensheldon/good_job/tree/v2.14.0) (2022-04-26)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.2...v2.14.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add mass update operations for jobs to Dashboard [\#578](https://github.com/bensheldon/good_job/pull/578) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ **Closed issues:**
12
+
13
+ - Allow "mass"-actions through Dashboard \(e.g. retry all\) [\#446](https://github.com/bensheldon/good_job/issues/446)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - 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))
18
+
3
19
  ## [v2.13.2](https://github.com/bensheldon/good_job/tree/v2.13.2) (2022-04-25)
4
20
 
5
21
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.1...v2.13.2)
6
22
 
7
- **Merged pull requests:**
23
+ **Fixed bugs:**
8
24
 
9
25
  - Namespaces assets per Rails docs [\#580](https://github.com/bensheldon/good_job/pull/580) ([kylekthompson](https://github.com/kylekthompson))
10
26
 
@@ -12,6 +28,11 @@
12
28
 
13
29
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.0...v2.13.1)
14
30
 
31
+ **Implemented enhancements:**
32
+
33
+ - Dashboard: Use toasts to show notices and alerts [\#577](https://github.com/bensheldon/good_job/pull/577) ([bkeepers](https://github.com/bkeepers))
34
+ - Remove executions from the dashboard [\#576](https://github.com/bensheldon/good_job/pull/576) ([bkeepers](https://github.com/bkeepers))
35
+
15
36
  **Fixed bugs:**
16
37
 
17
38
  - `ActionMailer::MailDeliveryJob` executing twice [\#329](https://github.com/bensheldon/good_job/issues/329)
@@ -26,8 +47,6 @@
26
47
 
27
48
  **Merged pull requests:**
28
49
 
29
- - Dashboard: Use toasts to show notices and alerts [\#577](https://github.com/bensheldon/good_job/pull/577) ([bkeepers](https://github.com/bkeepers))
30
- - Remove executions from the dashboard [\#576](https://github.com/bensheldon/good_job/pull/576) ([bkeepers](https://github.com/bkeepers))
31
50
  - Use javascript importmaps for Dashboard [\#574](https://github.com/bensheldon/good_job/pull/574) ([bensheldon](https://github.com/bensheldon))
32
51
 
33
52
  ## [v2.13.0](https://github.com/bensheldon/good_job/tree/v2.13.0) (2022-04-19)
@@ -50,10 +69,13 @@
50
69
 
51
70
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.12.1...v2.12.2)
52
71
 
72
+ **Fixed bugs:**
73
+
74
+ - Un-deprecate Adapter's `execution_mode` argument [\#567](https://github.com/bensheldon/good_job/pull/567) ([bensheldon](https://github.com/bensheldon))
75
+
53
76
  **Merged pull requests:**
54
77
 
55
78
  - Dashboard: added NL translations [\#568](https://github.com/bensheldon/good_job/pull/568) ([eelcoj](https://github.com/eelcoj))
56
- - Un-deprecate Adapter's `execution_mode` argument [\#567](https://github.com/bensheldon/good_job/pull/567) ([bensheldon](https://github.com/bensheldon))
57
79
 
58
80
  ## [v2.12.1](https://github.com/bensheldon/good_job/tree/v2.12.1) (2022-04-18)
59
81
 
@@ -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
+ }
@@ -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,111 @@
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
+ <tr class="d-none" data-checkbox-toggle-show="job_ids">
40
+ <td class="text-center table-warning" colspan="10">
41
+ <% all_jobs_count = local_assigns[:all_jobs_count] %>
42
+ <label>
43
+ <%= check_box_tag "all_job_ids", 1, false, disabled: true, data: { "checkbox-toggle-show": "job_ids"} %>
44
+ Apply to all <%= all_jobs_count.present? ? number_with_delimiter(all_jobs_count) : "" %> <%= "job".pluralize(all_jobs_count || 99) %>.
45
+ <em>This could be a lot.</em>
46
+ </label>
47
+ </td>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <% if jobs.present? %>
52
+ <% jobs.each do |job| %>
53
+ <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
54
+ <td><%= check_box_tag 'job_ids[]', job.id, false, data: { "checkbox-toggle-each": "job_ids" } %></td>
55
+ <td>
56
+ <%= link_to job_path(job.id) do %>
57
+ <code><%= job.id %></code>
59
58
  <% end %>
60
- </div>
61
- </td>
59
+ </td>
60
+ <td><%= status_badge(job.status) %></td>
61
+ <td><%= job.job_class %></td>
62
+ <td><%= job.queue_name %></td>
63
+ <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
64
+ <td><%= job.executions_count %></td>
65
+ <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
66
+ <td>
67
+ <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
68
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
69
+ aria: { expanded: false, controls: dom_id(job, "params") }
70
+ %>
71
+ <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
72
+ </td>
73
+ <td>
74
+ <div class="text-nowrap">
75
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
76
+ <%= 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 %>
77
+ <%= render_icon "skip_forward" %>
78
+ <% end %>
79
+ <% else %>
80
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "skip_forward" %></button>
81
+ <% end %>
82
+
83
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
84
+ <%= 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 %>
85
+ <%= render_icon "stop" %>
86
+ <% end %>
87
+ <% else %>
88
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "stop" %></button>
89
+ <% end %>
90
+
91
+ <% if job.status == :discarded %>
92
+ <%= 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 %>
93
+ <%= render_icon "arrow_clockwise" %>
94
+ <% end %>
95
+ <% else %>
96
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "arrow_clockwise" %></button>
97
+ <% end %>
98
+ </div>
99
+ </td>
100
+ </tr>
101
+ <% end %>
102
+ <% else %>
103
+ <tr>
104
+ <td colspan="10" class="py-2 text-center text-muted">No jobs found.</td>
62
105
  </tr>
63
106
  <% 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>
107
+ </tbody>
108
+ </table>
109
+ <% end %>
71
110
  </div>
72
111
  </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.2'
4
+ VERSION = '2.14.0'
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.2
4
+ version: 2.14.0
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-25 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
@@ -362,6 +362,7 @@ files:
362
362
  - README.md
363
363
  - engine/app/assets/good_job/modules/application.js
364
364
  - engine/app/assets/good_job/modules/charts.js
365
+ - engine/app/assets/good_job/modules/checkbox_toggle.js
365
366
  - engine/app/assets/good_job/modules/document_ready.js
366
367
  - engine/app/assets/good_job/modules/poller.js
367
368
  - engine/app/assets/good_job/modules/toasts.js