good_job 2.13.2 → 2.14.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 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