good_job 3.11.1 → 3.12.1

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: 65cb545f93b489c6ee487cbd07924e7a9d4ad2df7d7039391c66f704b38a767d
4
- data.tar.gz: 6495c9a4b8da725051e44eb05e0b4c35b6aaa7363075ebb540316ec5a7d0b1b0
3
+ metadata.gz: 9f74b72eb186eb125e6a130824094f5629124f4a99bcfecbc7e24e3e8b6b0efa
4
+ data.tar.gz: 31c6b7d98d2d33146794b3c41b25a7ed434f3f7610a427658bc51aa958815057
5
5
  SHA512:
6
- metadata.gz: 698fbb528da9f58d766ccf47880de2e270f30e2f2226e23adafc00ea7de8920194773fd1769eb40aaff96b2ff3d666c20b080f2ddda801e0979817714bcc93b6
7
- data.tar.gz: 42b54be4f42c185531a490d7eae1bc26701c341c1d708167f385505141f62cd860119f52fc32a2a1dab0c6f94817518af51cb629cdff26f5cabfff3a1c61227e
6
+ metadata.gz: '08d92a2c384a0950749e191bacaa495f120f52a4ce9dfba8e07100d494323dd5692e784565fa301623bb2ec7d378a75a84a76414022a1feeb22cb1362f38fc04'
7
+ data.tar.gz: e7942e4dbd92be5635d38a6f77722317ee9fa5b1f4f6fac53cf3029230192fa6fc928e97d98f86653dc2f0a41d75223806e1b66492d44f64731ebb48016c6715
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.12.1](https://github.com/bensheldon/good_job/tree/v3.12.1) (2023-02-09)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.12.0...v3.12.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Fix "NoMethodError: private method `\_good\_job\_concurrency\_key' if key is nil" [\#836](https://github.com/bensheldon/good_job/pull/836) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ **Closed issues:**
12
+
13
+ - NoMethodError: private method `\_good\_job\_concurrency\_key' if key is nil [\#835](https://github.com/bensheldon/good_job/issues/835)
14
+ - Jobs go back into the queued state when a worker is killed [\#821](https://github.com/bensheldon/good_job/issues/821)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - Dashboard's tables update [\#834](https://github.com/bensheldon/good_job/pull/834) ([Ajmal](https://github.com/Ajmal))
19
+
20
+ ## [v3.12.0](https://github.com/bensheldon/good_job/tree/v3.12.0) (2023-02-07)
21
+
22
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.11.1...v3.12.0)
23
+
24
+ **Implemented enhancements:**
25
+
26
+ - Create `InterruptErrors` extension to raise an exception when an interrupted job is retried [\#830](https://github.com/bensheldon/good_job/pull/830) ([bensheldon](https://github.com/bensheldon))
27
+
3
28
  ## [v3.11.1](https://github.com/bensheldon/good_job/tree/v3.11.1) (2023-02-06)
4
29
 
5
30
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.11.0...v3.11.1)
data/README.md CHANGED
@@ -55,6 +55,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
55
55
  - [Exceptions](#exceptions)
56
56
  - [Retries](#retries)
57
57
  - [ActionMailer retries](#actionmailer-retries)
58
+ - [Interrupts](#interrupts)
58
59
  - [Timeouts](#timeouts)
59
60
  - [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)
60
61
  - [Database connections](#database-connections)
@@ -829,6 +830,23 @@ end
829
830
  Note, that `ActionMailer::MailDeliveryJob` is a default since Rails 6.0. Be sure that your app is using that class, as it
830
831
  might also be configured to use (deprecated now) `ActionMailer::DeliveryJob`.
831
832
 
833
+ ### Interrupts
834
+
835
+ Jobs will be automatically retried if the process is interrupted while performing a job, for example as the result of a `SIGKILL` or power failure.
836
+
837
+ If you need more control over interrupt-caused retries, include the `GoodJob::ActiveJobExtensions::InterruptErrors` extension in your job closs. When an interrupted job is retried, the extension will raise a `GoodJob::InterruptError` exception within the job, which allows you to use ActiveJob's `retry_on` and `discard_on` to control the behavior of the job.
838
+
839
+ ```ruby
840
+ class MyJob < ApplicationJob
841
+ # The extension must be included before other extensions
842
+ include GoodJob::ActiveJobExtensions::InterruptErrors
843
+ # Discard the job if it is interrupted
844
+ discard_on InterruptError
845
+ # Retry the job if it is interrupted
846
+ retry_on InterruptError, wait: 0, attempts: Float::INFINITY
847
+ end
848
+ ```
849
+
832
850
  ### Timeouts
833
851
 
834
852
  Job timeouts can be configured with an `around_perform`:
@@ -315,10 +315,27 @@ module GoodJob
315
315
  run_callbacks(:perform) do
316
316
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
317
317
 
318
- self.performed_at = Time.current
319
- save! if GoodJob.preserve_job_records
318
+ result = GoodJob::CurrentThread.within do |current_thread|
319
+ current_thread.reset
320
+ current_thread.execution = self
320
321
 
321
- result = execute
322
+ current_thread.execution_interrupted = performed_at if performed_at
323
+ update!(performed_at: Time.current)
324
+
325
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
326
+ value = ActiveJob::Base.execute(active_job_data)
327
+
328
+ if value.is_a?(Exception)
329
+ handled_error = value
330
+ value = nil
331
+ end
332
+ handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
333
+
334
+ ExecutionResult.new(value: value, handled_error: handled_error, retried: current_thread.error_on_retry.present?)
335
+ rescue StandardError => e
336
+ ExecutionResult.new(value: nil, unhandled_error: e)
337
+ end
338
+ end
322
339
 
323
340
  job_error = result.handled_error || result.unhandled_error
324
341
  self.error = [job_error.class, ERROR_MESSAGE_SEPARATOR, job_error.message].join if job_error
@@ -408,28 +425,6 @@ module GoodJob
408
425
  end
409
426
  end
410
427
 
411
- # @return [ExecutionResult]
412
- def execute
413
- GoodJob::CurrentThread.within do |current_thread|
414
- current_thread.reset
415
- current_thread.execution = self
416
-
417
- ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
418
- value = ActiveJob::Base.execute(active_job_data)
419
-
420
- if value.is_a?(Exception)
421
- handled_error = value
422
- value = nil
423
- end
424
- handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
425
-
426
- ExecutionResult.new(value: value, handled_error: handled_error, retried: current_thread.error_on_retry.present?)
427
- rescue StandardError => e
428
- ExecutionResult.new(value: nil, unhandled_error: e)
429
- end
430
- end
431
- end
432
-
433
428
  def reset_batch_values(&block)
434
429
  GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
435
430
  end
@@ -3,9 +3,9 @@
3
3
  <header class="list-group-item bg-light">
4
4
  <div class="row small text-muted text-uppercase align-items-center">
5
5
  <div class="col-4">Jobs</div>
6
- <div class="d-none d-md-block col-md-1">Queue</div>
7
- <div class="d-none d-md-block col-md-1">Priority</div>
8
- <div class="d-none d-md-block col-md-1 text-end">Attempts</div>
6
+ <div class="d-none d-lg-block col-lg-1 text-lg-center">Queue</div>
7
+ <div class="d-none d-lg-block col-lg-1 text-lg-end">Priority</div>
8
+ <div class="d-none d-lg-block col-lg-1 text-lg-end">Attempts</div>
9
9
  <div class="col text-end">
10
10
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
11
11
  data: { bs_toggle: "collapse", bs_target: ".job-params" },
@@ -21,18 +21,20 @@
21
21
  <% jobs.each do |job| %>
22
22
  <div role="row" class="list-group-item list-group-item-action py-3">
23
23
  <div class="row align-items-center">
24
- <div class="col-md-4">
24
+ <div class="col-lg-4">
25
25
  <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
26
26
  <%= tag.h5 tag.code(link_to(job.job_class, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
27
27
  </div>
28
- <div class="col-md-1">
28
+ <div class="col-4 col-lg-1 text-lg-center">
29
+ <div class="d-lg-none small text-muted mt-1">Queue</div>
29
30
  <span class="badge bg-primary bg-opacity-25 text-dark font-monospace"><%= job.queue_name %></span>
30
31
  </div>
31
- <div class="col-md-1 small text-md-center">
32
+ <div class="col-4 col-lg-1 text-lg-end">
33
+ <div class="d-lg-none small text-muted mt-1">Priority</div>
32
34
  <span class="font-monospace fw-bold"><%= job.priority %></span>
33
- <span class="d-md-none">Priority</span>
34
35
  </div>
35
- <div class="col-md-1 text-md-center">
36
+ <div class="col-4 col-lg-1 text-lg-end">
37
+ <div class="d-lg-none small text-muted mt-1">Attempts</div>
36
38
  <% if job.executions_count > 0 && job.status != :finished %>
37
39
  <%= tag.span job.executions_count, class: "badge rounded-pill bg-danger", data: {
38
40
  bs_toggle: "popover",
@@ -43,13 +45,11 @@
43
45
  <% else %>
44
46
  <span class="badge bg-secondary bg-opacity-50 rounded-pill"><%= job.executions_count %></span>
45
47
  <% end %>
46
- <span class="d-md-none small">Attemp</span>
47
48
  </div>
48
- <div class="col d-flex gap-3 align-items-center justify-content-end">
49
+ <div class="mt-3 mt-lg-0 col d-flex gap-3 align-items-center justify-content-end">
49
50
  <%= tag.span relative_time(job.last_status_at), class: "small" %>
50
51
  <%= status_badge job.status %>
51
- </div>
52
- <div class="col-auto">
52
+
53
53
  <div class="dropdown float-end">
54
54
  <button class="d-flex align-items-center btn btn-sm" type="button" id="<%= dom_id(job, :actions) %>" data-bs-toggle="dropdown" aria-expanded="false">
55
55
  <%= render "good_job/shared/icons/dots" %>
@@ -106,5 +106,5 @@
106
106
  No jobs found.
107
107
  </div>
108
108
  <% end %>
109
- </>
109
+ </div>
110
110
  </div>
@@ -3,11 +3,11 @@
3
3
  <header class="list-group-item bg-light">
4
4
  <div class="row small text-muted text-uppercase align-items-center">
5
5
  <div class="col-4">Name</div>
6
- <div class="col-md-1 d-none d-md-block">Created</div>
7
- <div class="col-md-1 d-none d-md-block">Enqueued</div>
8
- <div class="col-md-1 d-none d-md-block">Discarded</div>
9
- <div class="col-md-1 d-none d-md-block">Finished</div>
10
- <div class="col-md-1 d-none d-md-block">Jobs</div>
6
+ <div class="col-lg-1 d-none d-lg-block">Created</div>
7
+ <div class="col-lg-1 d-none d-lg-block">Enqueued</div>
8
+ <div class="col-lg-1 d-none d-lg-block">Discarded</div>
9
+ <div class="col-lg-1 d-none d-lg-block">Finished</div>
10
+ <div class="col-lg-1 d-none d-lg-block">Jobs</div>
11
11
  <div class="col text-end">
12
12
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
13
13
  data: { bs_toggle: "collapse", bs_target: ".batch-properties" },
@@ -23,7 +23,7 @@
23
23
  <% batches.each do |batch| %>
24
24
  <div id="<%= dom_id(batch) %>" class="list-group-item py-3" role="row">
25
25
  <div class="row align-items-center">
26
- <div class="col-4">
26
+ <div class="col-12 col-lg-4">
27
27
  <%= link_to batch_path(batch), class: "text-decoration-none" do %>
28
28
  <code class="small text-muted">
29
29
  <%= batch.id %>
@@ -32,34 +32,34 @@
32
32
  <div class="text-muted"><%= batch.description %></div>
33
33
  <% end %>
34
34
  </div>
35
- <div class="col-md-1 text-wrap">
36
- <div class="d-md-none small text-muted mt-1">Created at</div>
35
+ <div class="col-6 col-lg-1 text-wrap">
36
+ <div class="d-lg-none small text-muted mt-1">Created at</div>
37
37
  <%= relative_time(batch.created_at) %>
38
38
  </div>
39
- <div class="col-md-1 text-wrap">
39
+ <div class="col-6 col-lg-1 text-wrap">
40
40
  <% if batch.enqueued_at %>
41
- <div class="d-md-none small text-muted mt-1">Enqueued at</div>
41
+ <div class="d-lg-none small text-muted mt-1">Enqueued at</div>
42
42
  <%= relative_time(batch.enqueued_at) %>
43
43
  <% end %>
44
44
  </div>
45
- <div class="col-md-1 text-wrap">
45
+ <div class="col-6 col-lg-1 text-wrap">
46
46
  <% if batch.discarded_at %>
47
- <div class="d-md-none small text-muted mt-1">Discarded at</div>
47
+ <div class="d-lg-none small text-muted mt-1">Discarded at</div>
48
48
  <%= relative_time(batch.discarded_at) %>
49
49
  <% end %>
50
50
  </div>
51
- <div class="col-md-1 text-wrap">
51
+ <div class="col-6 col-lg-1 text-wrap">
52
52
  <% if batch.finished_at %>
53
- <div class="d-md-none small text-muted mt-1">Finished at</div>
53
+ <div class="d-lg-none small text-muted mt-1">Finished at</div>
54
54
  <%= relative_time(batch.finished_at) %>
55
55
  <% end %>
56
56
  </div>
57
- <div class="col">
58
- <div class="d-md-none small text-muted mt-1">Jobs</div>
57
+ <div class="col-6 col-lg-1">
58
+ <div class="d-lg-none small text-muted mt-1">Jobs</div>
59
59
  <%= batch.jobs.count %>
60
60
  </div>
61
61
  <div class="col text-end">
62
- <%= tag.button type: "button", class: "btn btn-sm text-muted ms-auto", role: "button",
62
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
63
63
  title: "Inspect",
64
64
  data: { bs_toggle: "collapse", bs_target: "##{dom_id(batch, 'properties')}" },
65
65
  aria: { expanded: false, controls: dom_id(batch, "state") } do %>
@@ -6,11 +6,11 @@
6
6
  <div class="list-group list-group-flush text-nowrap" role="table">
7
7
  <header class="list-group-item bg-light">
8
8
  <div class="row small text-muted text-uppercase align-items-center">
9
- <div class="col"></div>
10
- <div class="col">Class</div>
11
- <div class="col">Schedule</div>
12
- <div class="col">Next scheduled</div>
13
- <div class="col">Last run</div>
9
+ <div class="col-12 col-lg-2"></div>
10
+ <div class="col-6 col-lg-2 d-none d-lg-block">Class</div>
11
+ <div class="col-6 col-lg-2 d-none d-lg-block">Schedule</div>
12
+ <div class="col-6 col-lg-2 d-none d-lg-block">Next scheduled</div>
13
+ <div class="col-6 col-lg-2 d-none d-lg-block">Last run</div>
14
14
  <div class="col text-end">
15
15
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
16
16
  data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
@@ -24,15 +24,22 @@
24
24
  <% @cron_entries.each do |cron_entry| %>
25
25
  <div id="<%= dom_id(cron_entry) %>" class="list-group-item py-3" role="row">
26
26
  <div class="row align-items-center">
27
- <div class="col">
27
+ <div class="col-12 col-lg-2">
28
28
  <div class="small font-monospace"><%= cron_entry.key %></div>
29
- <div class="small text-muted"><%= cron_entry.description %></div>
29
+ <div class="small text-muted text-wrap"><%= cron_entry.description %></div>
30
30
  </div>
31
- <div class="col"><%= tag.span tag.code(cron_entry.job_class), class: "fs-5 mb-0" %></div>
32
- <div class="col font-monospace fw-bold"><%= cron_entry.schedule %></div>
33
- <div class="col small"><%= relative_time cron_entry.next_at %></div>
34
- <div class="col small">
31
+ <div class="col-12 col-lg-2 text-wrap"><%= tag.span tag.code(cron_entry.job_class), class: "fs-5 mb-0" %></div>
32
+ <div class="col-6 col-lg-2 text-wrap">
33
+ <div class="d-lg-none small text-muted mt-1">Schedule</div>
34
+ <span class="font-monospace fw-bold"><%= cron_entry.schedule %></span>
35
+ </div>
36
+ <div class="col-6 col-lg-2 text-wrap small">
37
+ <div class="d-lg-none small text-muted mt-1">Next scheduled</div>
38
+ <%= relative_time cron_entry.next_at %>
39
+ </div>
40
+ <div class="col-6 col-lg-2 text-wrap small">
35
41
  <% if cron_entry.last_job.present? %>
42
+ <div class="d-lg-none small text-muted mt-1">Last run</div>
36
43
  <%= link_to relative_time(cron_entry.last_at), cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
37
44
  <% end %>
38
45
  </div>
@@ -23,7 +23,7 @@
23
23
  <%= tag.span relative_time(execution.last_status_at, include_seconds: true), class: "small" %>
24
24
  <%= status_badge execution.status %>
25
25
 
26
- <%= tag.button type: "button", class: "btn btn-sm text-muted ms-auto", role: "button",
26
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
27
27
  title: "Inspect",
28
28
  data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
29
29
  aria: { expanded: false, controls: dom_id(execution, "params") } do %>
@@ -3,12 +3,11 @@
3
3
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
4
4
  <header class="list-group-item bg-light">
5
5
  <div class="row small text-muted text-uppercase align-items-center">
6
- <div class="col-auto">
6
+ <div class="col-lg-4 d-flex gap-2 flex-wrap">
7
7
  <%= label_tag('toggle_job_ids', "Toggle all jobs", class: "visually-hidden") %>
8
8
  <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
9
- </div>
10
- <div class="col-md-4">
11
- <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'btn btn-sm btn-outline-secondary', title: "Reschedule all", data: { confirm: "Are you sure you want to reschedule the selected jobs?", disable: true } do %>
9
+
10
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: "Reschedule all", data: { confirm: "Are you sure you want to reschedule the selected jobs?", disable: true } do %>
12
11
  <span class="me-1"><%= render_icon "skip_forward" %></span> Reschedule
13
12
  <% end %>
14
13
 
@@ -33,9 +32,9 @@
33
32
  </div>
34
33
 
35
34
  </div>
36
- <div class="d-none d-md-block col-md-1">Queue</div>
37
- <div class="d-none d-md-block col-md-1">Priority</div>
38
- <div class="d-none d-md-block col-md-1 text-end">Attempts</div>
35
+ <div class="d-none d-lg-block col-lg-1 text-lg-center">Queue</div>
36
+ <div class="d-none d-lg-block col-lg-1 text-lg-end">Priority</div>
37
+ <div class="d-none d-lg-block col-lg-1 text-lg-end">Attempts</div>
39
38
  <div class="col text-end">
40
39
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
41
40
  data: { bs_toggle: "collapse", bs_target: ".job-params" },
@@ -61,21 +60,23 @@
61
60
  <% jobs.each do |job| %>
62
61
  <%= label_tag dom_id(job, :checkbox), id: dom_id(job), role: "row", class: "list-group-item list-group-item-action py-3" do %>
63
62
  <div class="row align-items-center">
64
- <div class="col-auto">
63
+ <div class="col-lg-4 d-flex">
65
64
  <%= check_box_tag 'job_ids[]', job.id, false, id: dom_id(job, :checkbox), data: { "checkbox-toggle-each": "job_ids" } %>
65
+ <div class="ms-2">
66
+ <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
67
+ <%= tag.h5 tag.code(link_to(job.job_class, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
68
+ </div>
66
69
  </div>
67
- <div class="col-md-4">
68
- <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
69
- <%= tag.h5 tag.code(link_to(job.job_class, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
70
- </div>
71
- <div class="col-md-1">
70
+ <div class="col-4 col-lg-1 text-lg-center">
71
+ <div class="d-lg-none small text-muted mt-1">Queue</div>
72
72
  <span class="badge bg-primary bg-opacity-25 text-dark font-monospace"><%= job.queue_name %></span>
73
73
  </div>
74
- <div class="col-md-1 small text-md-center">
75
- <span class="font-monospace fw-bold"><%= job.priority %></span>
76
- <span class="d-md-none">Priority</span>
74
+ <div class="col-4 col-lg-1 text-lg-end">
75
+ <div class="d-lg-none small text-muted mt-1">Priority</div>
76
+ <%= job.priority %>
77
77
  </div>
78
- <div class="col-md-1 text-md-center">
78
+ <div class="col-4 col-lg-1 text-lg-end">
79
+ <div class="d-lg-none small text-muted mt-1">Attempts</div>
79
80
  <% if job.executions_count > 0 && job.status != :succeeded %>
80
81
  <%= tag.span job.executions_count, class: "badge rounded-pill bg-danger", data: {
81
82
  bs_toggle: "popover",
@@ -86,56 +87,56 @@
86
87
  <% else %>
87
88
  <span class="badge bg-secondary bg-opacity-50 rounded-pill"><%= job.executions_count %></span>
88
89
  <% end %>
89
- <span class="d-md-none small">Attemp</span>
90
- </div>
91
- <div class="col d-flex gap-3 align-items-center justify-content-end">
92
- <%= tag.span relative_time(job.last_status_at), class: "small" %>
93
- <%= status_badge job.status %>
94
90
  </div>
95
- <div class="col-auto">
96
- <div class="dropdown float-end">
97
- <button class="d-flex align-items-center btn btn-sm" type="button" id="<%= dom_id(job, :actions) %>" data-bs-toggle="dropdown" aria-expanded="false">
98
- <%= render "good_job/shared/icons/dots" %>
99
- <span class="visually-hidden">Actions</span>
100
- </button>
101
- <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
102
- <li>
103
- <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
104
- <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
105
- <%= render "good_job/shared/icons/skip_forward" %>
106
- Reschedule
107
- <% end %>
108
- </li>
109
- <li>
110
- <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
111
- <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
112
- <%= render "good_job/shared/icons/stop" %>
113
- Discard
114
- <% end %>
115
- </li>
116
- <li>
117
- <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
118
- <%= render "good_job/shared/icons/arrow_clockwise" %>
119
- Retry
120
- <% end %>
121
- </li>
122
- <li>
123
- <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: "Destroy job", data: { confirm: "Confirm destroy", disable: true } do %>
124
- <%= render_icon "trash" %>
125
- Destroy
126
- <% end %>
127
- </li>
91
+ <div class="mt-3 mt-lg-0 col">
92
+ <div class="d-flex gap-3 align-items-center justify-content-end">
93
+ <%= tag.span relative_time(job.last_status_at), class: "small" %>
94
+ <%= status_badge job.status %>
95
+
96
+ <div class="dropdown float-end">
97
+ <button class="d-flex align-items-center btn btn-sm" type="button" id="<%= dom_id(job, :actions) %>" data-bs-toggle="dropdown" aria-expanded="false">
98
+ <%= render "good_job/shared/icons/dots" %>
99
+ <span class="visually-hidden">Actions</span>
100
+ </button>
101
+ <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
102
+ <li>
103
+ <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
104
+ <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
105
+ <%= render "good_job/shared/icons/skip_forward" %>
106
+ Reschedule
107
+ <% end %>
108
+ </li>
109
+ <li>
110
+ <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
111
+ <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
112
+ <%= render "good_job/shared/icons/stop" %>
113
+ Discard
114
+ <% end %>
115
+ </li>
116
+ <li>
117
+ <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
118
+ <%= render "good_job/shared/icons/arrow_clockwise" %>
119
+ Retry
120
+ <% end %>
121
+ </li>
122
+ <li>
123
+ <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: "Destroy job", data: { confirm: "Confirm destroy", disable: true } do %>
124
+ <%= render_icon "trash" %>
125
+ Destroy
126
+ <% end %>
127
+ </li>
128
128
 
129
- <li>
130
- <%= link_to "##{dom_id(job, 'params')}",
131
- class: "dropdown-item",
132
- data: { bs_toggle: "collapse" },
133
- aria: { expanded: false, controls: dom_id(job, "params") } do %>
134
- <%= render_icon "info" %>
135
- Inspect
136
- <% end %>
137
- </li>
138
- </ul>
129
+ <li>
130
+ <%= link_to "##{dom_id(job, 'params')}",
131
+ class: "dropdown-item",
132
+ data: { bs_toggle: "collapse" },
133
+ aria: { expanded: false, controls: dom_id(job, "params") } do %>
134
+ <%= render_icon "info" %>
135
+ Inspect
136
+ <% end %>
137
+ </li>
138
+ </ul>
139
+ </div>
139
140
  </div>
140
141
  </div>
141
142
  </div>
@@ -86,6 +86,18 @@ module GoodJob
86
86
  @good_job_concurrency_key || _good_job_concurrency_key
87
87
  end
88
88
 
89
+ # Generates the concurrency key from the configuration
90
+ # @return [Object] concurrency key
91
+ def _good_job_concurrency_key
92
+ key = self.class.good_job_concurrency_config[:key]
93
+ return if key.blank?
94
+
95
+ key = key.respond_to?(:call) ? instance_exec(&key) : key
96
+ raise TypeError, "Concurrency key must be a String; was a #{key.class}" unless VALID_TYPES.any? { |type| key.is_a?(type) }
97
+
98
+ key
99
+ end
100
+
89
101
  private
90
102
 
91
103
  def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
@@ -129,18 +141,6 @@ module GoodJob
129
141
  end
130
142
  end
131
143
  end
132
-
133
- # Generates the concurrency key from the configuration
134
- # @return [Object] concurrency key
135
- def _good_job_concurrency_key
136
- key = self.class.good_job_concurrency_config[:key]
137
- return if key.blank?
138
-
139
- key = key.respond_to?(:call) ? instance_exec(&key) : key
140
- raise TypeError, "Concurrency key must be a String; was a #{key.class}" unless VALID_TYPES.any? { |type| key.is_a?(type) }
141
-
142
- key
143
- end
144
144
  end
145
145
  end
146
146
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ module ActiveJobExtensions
4
+ module InterruptErrors
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ around_perform do |_job, block|
9
+ raise InterruptError, "Interrupted after starting perform at '#{CurrentThread.execution_interrupted}'" if CurrentThread.execution_interrupted.present?
10
+
11
+ block.call
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -12,6 +12,7 @@ module GoodJob
12
12
  error_on_discard
13
13
  error_on_retry
14
14
  execution
15
+ execution_interrupted
15
16
  ].freeze
16
17
 
17
18
  # @!attribute [rw] cron_at
@@ -44,6 +45,12 @@ module GoodJob
44
45
  # @return [GoodJob::Execution, nil]
45
46
  thread_mattr_accessor :execution
46
47
 
48
+ # @!attribute [rw] execution_interrupted
49
+ # @!scope class
50
+ # Execution Interrupted
51
+ # @return [Boolean, nil]
52
+ thread_mattr_accessor :execution_interrupted
53
+
47
54
  # Resets attributes
48
55
  # @param [Hash] values to assign
49
56
  # @return [void]
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ # Exception raised when a job is interrupted by a SIGKILL or power failure.
4
+ class InterruptError < StandardError
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.11.1'
4
+ VERSION = '3.12.1'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -9,6 +9,8 @@ require "good_job/adapter"
9
9
  require "active_job/queue_adapters/good_job_adapter"
10
10
  require "good_job/active_job_extensions/batches"
11
11
  require "good_job/active_job_extensions/concurrency"
12
+ require "good_job/interrupt_error"
13
+ require "good_job/active_job_extensions/interrupt_errors"
12
14
  require "good_job/active_job_extensions/notify_options"
13
15
 
14
16
  require "good_job/assignable_connection"
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: 3.11.1
4
+ version: 3.12.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: 2023-02-06 00:00:00.000000000 Z
11
+ date: 2023-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -403,6 +403,7 @@ files:
403
403
  - lib/good_job.rb
404
404
  - lib/good_job/active_job_extensions/batches.rb
405
405
  - lib/good_job/active_job_extensions/concurrency.rb
406
+ - lib/good_job/active_job_extensions/interrupt_errors.rb
406
407
  - lib/good_job/active_job_extensions/notify_options.rb
407
408
  - lib/good_job/adapter.rb
408
409
  - lib/good_job/assignable_connection.rb
@@ -415,6 +416,7 @@ files:
415
416
  - lib/good_job/daemon.rb
416
417
  - lib/good_job/dependencies.rb
417
418
  - lib/good_job/engine.rb
419
+ - lib/good_job/interrupt_error.rb
418
420
  - lib/good_job/job_performer.rb
419
421
  - lib/good_job/log_subscriber.rb
420
422
  - lib/good_job/multi_scheduler.rb