good_job 3.24.0 → 3.26.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +56 -4
- data/app/helpers/good_job/application_helper.rb +5 -36
- data/app/helpers/good_job/icons_helper.rb +41 -0
- data/app/models/good_job/execution.rb +45 -34
- data/app/models/good_job/execution_result.rb +8 -4
- data/app/models/good_job/job.rb +9 -1
- data/app/models/good_job/process.rb +1 -1
- data/app/views/good_job/_custom_execution_details.html.erb +11 -0
- data/app/views/good_job/_custom_job_details.html.erb +10 -0
- data/app/views/good_job/batches/_jobs.erb +4 -4
- data/app/views/good_job/jobs/_executions.erb +1 -0
- data/app/views/good_job/jobs/_table.erb +5 -5
- data/app/views/good_job/jobs/show.html.erb +2 -0
- data/app/views/good_job/shared/_alert.erb +2 -2
- data/app/views/good_job/shared/_navbar.erb +1 -1
- data/app/views/good_job/shared/icons/_check.html.erb +1 -1
- data/app/views/good_job/shared/icons/_exclamation.html.erb +1 -1
- data/config/locales/it.yml +243 -0
- data/lib/good_job/adapter.rb +35 -11
- data/lib/good_job/configuration.rb +10 -1
- data/lib/good_job/current_thread.rb +8 -1
- data/lib/good_job/daemon.rb +9 -0
- data/lib/good_job/notifier/process_heartbeat.rb +3 -3
- data/lib/good_job/notifier.rb +3 -3
- data/lib/good_job/{assignable_connection.rb → overridable_connection.rb} +7 -14
- data/lib/good_job/scheduler.rb +11 -7
- data/lib/good_job/shared_executor.rb +8 -7
- data/lib/good_job/thread_status.rb +27 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +3 -1
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a51ff44544ae7fb381fce50f13d3196d6a561bcdc6a5af7f37ad1e8fb0d6b258
|
|
4
|
+
data.tar.gz: 3703fe631d3f549bb152999203e5d67ac6d305c4ac77bba5b47621f3419cef21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba6647af970ee04455977ccdfb835ac03648bc544ae3606e69e2007d04bf13634f0767c1263738d04101a6839949affd3227a19b1b573d27a6cb8bb4ea56a5b6
|
|
7
|
+
data.tar.gz: 3c8e79ae13f69d050f35aea95ebaf4c035a97138a89d24c57430776909d44c9fbbd9a5ab4460ca30efd65c2c16ec231780957c555f43a77dc100388dbfffb341
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v3.26.0](https://github.com/bensheldon/good_job/tree/v3.26.0) (2024-03-01)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.25.0...v3.26.0)
|
|
6
|
+
|
|
7
|
+
**Implemented enhancements:**
|
|
8
|
+
|
|
9
|
+
- Add `GoodJob.current_thread_running?` and `GoodJob.current_thread_shutting_down?` for graceful shutdowns [\#1253](https://github.com/bensheldon/good_job/pull/1253) ([bensheldon](https://github.com/bensheldon))
|
|
10
|
+
|
|
11
|
+
**Fixed bugs:**
|
|
12
|
+
|
|
13
|
+
- Ensure "shutdown?" behavior is consistent between J Ruby and C Ruby [\#1267](https://github.com/bensheldon/good_job/pull/1267) ([bensheldon](https://github.com/bensheldon))
|
|
14
|
+
|
|
15
|
+
**Closed issues:**
|
|
16
|
+
|
|
17
|
+
- PG Good Job rows add up [\#1262](https://github.com/bensheldon/good_job/issues/1262)
|
|
18
|
+
- Bulk operations do not work for Batches [\#1255](https://github.com/bensheldon/good_job/issues/1255)
|
|
19
|
+
- What's the difference between 'reschedule' and 'retry' in the dashboard? [\#1241](https://github.com/bensheldon/good_job/issues/1241)
|
|
20
|
+
|
|
21
|
+
**Merged pull requests:**
|
|
22
|
+
|
|
23
|
+
- feat: add italian locale [\#1268](https://github.com/bensheldon/good_job/pull/1268) ([metalelf0](https://github.com/metalelf0))
|
|
24
|
+
|
|
25
|
+
## [v3.25.0](https://github.com/bensheldon/good_job/tree/v3.25.0) (2024-02-22)
|
|
26
|
+
|
|
27
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.24.0...v3.25.0)
|
|
28
|
+
|
|
29
|
+
**Implemented enhancements:**
|
|
30
|
+
|
|
31
|
+
- Allow disabling of Dashboard Live Polling configuration [\#1235](https://github.com/bensheldon/good_job/pull/1235) ([erick-tmr](https://github.com/erick-tmr))
|
|
32
|
+
- Add customizable extension partials to good\_job/jobs\#show view [\#1200](https://github.com/bensheldon/good_job/pull/1200) ([grncdr](https://github.com/grncdr))
|
|
33
|
+
|
|
34
|
+
**Fixed bugs:**
|
|
35
|
+
|
|
36
|
+
- Fix default engine cron value [\#1258](https://github.com/bensheldon/good_job/pull/1258) ([hss-mateus](https://github.com/hss-mateus))
|
|
37
|
+
- Print an error when daemon pidfile dir doesn't exist [\#1252](https://github.com/bensheldon/good_job/pull/1252) ([thepry](https://github.com/thepry))
|
|
38
|
+
|
|
39
|
+
**Closed issues:**
|
|
40
|
+
|
|
41
|
+
- Production deployment question [\#1257](https://github.com/bensheldon/good_job/issues/1257)
|
|
42
|
+
- Daemon and App not connecting to secondary database [\#1254](https://github.com/bensheldon/good_job/issues/1254)
|
|
43
|
+
- Logging with logger.warn in classes is suppressed by good job? \(semantic\_logger\) [\#1250](https://github.com/bensheldon/good_job/issues/1250)
|
|
44
|
+
|
|
45
|
+
**Merged pull requests:**
|
|
46
|
+
|
|
47
|
+
- Fix Active Record connection changes on Rails head [\#1259](https://github.com/bensheldon/good_job/pull/1259) ([bensheldon](https://github.com/bensheldon))
|
|
48
|
+
- \[Docs\] Bulk.enqueue takes an array of jobs [\#1256](https://github.com/bensheldon/good_job/pull/1256) ([jpcamara](https://github.com/jpcamara))
|
|
49
|
+
- Clean up icon helpers for less noisy view rendering [\#1248](https://github.com/bensheldon/good_job/pull/1248) ([bensheldon](https://github.com/bensheldon))
|
|
50
|
+
- Use dotenv-rails instead of dotenv [\#1247](https://github.com/bensheldon/good_job/pull/1247) ([bensheldon](https://github.com/bensheldon))
|
|
51
|
+
- Perform inline retries iteratively instead of recursively [\#1246](https://github.com/bensheldon/good_job/pull/1246) ([bensheldon](https://github.com/bensheldon))
|
|
52
|
+
|
|
3
53
|
## [v3.24.0](https://github.com/bensheldon/good_job/tree/v3.24.0) (2024-02-12)
|
|
4
54
|
|
|
5
55
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.23.0...v3.24.0)
|
data/README.md
CHANGED
|
@@ -41,6 +41,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
|
41
41
|
- [Dashboard](#dashboard)
|
|
42
42
|
- [API-only Rails applications](#api-only-rails-applications)
|
|
43
43
|
- [Live polling](#live-polling)
|
|
44
|
+
- [Extending dashboard views](#extending-dashboard-views)
|
|
44
45
|
- [Job priority](#job-priority)
|
|
45
46
|
- [Concurrency controls](#concurrency-controls)
|
|
46
47
|
- [How concurrency controls work](#how-concurrency-controls-work)
|
|
@@ -56,7 +57,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
|
56
57
|
- [Exceptions](#exceptions)
|
|
57
58
|
- [Retries](#retries)
|
|
58
59
|
- [Action Mailer retries](#action-mailer-retries)
|
|
59
|
-
- [Interrupts](#
|
|
60
|
+
- [Interrupts, graceful shutdown, and SIGKILL](#Interrupts-graceful-shutdown-and-SIGKILL)
|
|
60
61
|
- [Timeouts](#timeouts)
|
|
61
62
|
- [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)
|
|
62
63
|
- [Database connections](#database-connections)
|
|
@@ -451,6 +452,42 @@ end
|
|
|
451
452
|
|
|
452
453
|
The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
|
|
453
454
|
|
|
455
|
+
#### Extending dashboard views
|
|
456
|
+
|
|
457
|
+
GoodJob exposes some views that are intended to be overriden by placing views in your application:
|
|
458
|
+
|
|
459
|
+
- [`app/views/good_job/jobs/_custom_job_details.html.erb`](app/views/good_job/_custom_job_details.html.erb): content added to this partial will be displayed above the argument list on the good_job/jobs#show page.
|
|
460
|
+
- [`app/views/good_job/jobs/_custom_execution_details.html.erb`](app/views/good_job/_custom_execution_details.html.erb): content added to this partial will be displayed above each execution on the good_job/jobs#show page.
|
|
461
|
+
|
|
462
|
+
**Warning:** these partials expose classes (such as `GoodJob::Job`) that are considered internal implementation details of GoodJob. You should always test your custom partials after upgrading GoodJob.
|
|
463
|
+
|
|
464
|
+
For example, if your app deals with widgets and you want to show a link to the widget a job acted on, you can add the following to `app/views/good_job/_custom_job_details.html.erb`:
|
|
465
|
+
|
|
466
|
+
```erb
|
|
467
|
+
<%# file: app/views/good_job/_custom_job_details.html.erb %>
|
|
468
|
+
<% arguments = job.active_job.arguments rescue [] %>
|
|
469
|
+
<% widgets = arguments.select { |arg| arg.is_a?(Widget) } %>
|
|
470
|
+
<% if widgets.any? %>
|
|
471
|
+
<div class="my-4">
|
|
472
|
+
<h5>Widgets</h5>
|
|
473
|
+
<ul>
|
|
474
|
+
<% widgets.each do |widget| %>
|
|
475
|
+
<li><%= link_to widget.name, main_app.widget_url(widget) %></li>
|
|
476
|
+
<% end %>
|
|
477
|
+
</ul>
|
|
478
|
+
</div>
|
|
479
|
+
<% end %>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
As a second example, you may wish to show a link to a log aggregator next to each job execution. You can do this by adding the following to `app/views/good_job/_custom_execution_details.html.erb`:
|
|
483
|
+
|
|
484
|
+
```erb
|
|
485
|
+
<%# file: app/views/good_job/_custom_execution_details.html.erb %>
|
|
486
|
+
<div class="py-3">
|
|
487
|
+
<%= link_to "Logs", main_app.logs_url(filter: { job_id: job.id }, start_time: execution.performed_at, end_time: execution.finished_at + 1.minute) %>
|
|
488
|
+
</div>
|
|
489
|
+
```
|
|
490
|
+
|
|
454
491
|
### Job priority
|
|
455
492
|
|
|
456
493
|
Higher priority numbers run first in all versions of GoodJob v3.x and below. GoodJob v4.x will change job `priority` to give smaller numbers higher priority (default: `0`), in accordance with Active Job's definition of priority (see #524). To opt-in to this behavior now, set `config.good_job.smaller_number_is_higher_priority = true` in your GoodJob initializer or `application.rb`.
|
|
@@ -609,7 +646,7 @@ end
|
|
|
609
646
|
active_jobs.all?(&:provider_job_id)
|
|
610
647
|
|
|
611
648
|
# Bulk enqueue Active Job instances directly without using `.perform_later`:
|
|
612
|
-
GoodJob::Bulk.enqueue(MyJob.new, AnotherJob.new)
|
|
649
|
+
GoodJob::Bulk.enqueue([MyJob.new, AnotherJob.new])
|
|
613
650
|
```
|
|
614
651
|
|
|
615
652
|
### Batches
|
|
@@ -937,9 +974,24 @@ end
|
|
|
937
974
|
Note, that `ActionMailer::MailDeliveryJob` is a default since Rails 6.0. Be sure that your app is using that class, as it
|
|
938
975
|
might also be configured to use (deprecated now) `ActionMailer::DeliveryJob`.
|
|
939
976
|
|
|
940
|
-
### Interrupts
|
|
977
|
+
### Interrupts, graceful shutdown, and SIGKILL
|
|
978
|
+
|
|
979
|
+
When GoodJob receives an interrupt (SIGINT, SIGTERM) or explicitly with `GoodJob.shutdown`, GoodJob will attempt to gracefully shut down, waiting for all jobs to finish before exiting based on the `shutdown_timeout` configuration.
|
|
980
|
+
|
|
981
|
+
To detect the start of a graceful shutdown from within a performing job, for example while looping/iterating over multiple items, you can call `GoodJob.current_thread_shutting_down?` or `GoodJob.current_thread_running?` from within the job. For example:
|
|
982
|
+
|
|
983
|
+
```ruby
|
|
984
|
+
def perform(lots_of_records)
|
|
985
|
+
lots_of_records.each do |record|
|
|
986
|
+
break if GoodJob.current_thread_shutting_down? # or `unless GoodJob.current_thread.running?`
|
|
987
|
+
# process record ...
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
````
|
|
991
|
+
|
|
992
|
+
Note that when running jobs in `:inline` execution mode, `GoodJob.current_thread_running?` will always be truthy and `GoodJob.current_thread_shutting_down?` will always be falsey.
|
|
941
993
|
|
|
942
|
-
Jobs will be automatically retried if the process is interrupted while performing a job
|
|
994
|
+
Jobs will be automatically retried if the process is interrupted while performing a job and the job is unable to finish before the timeout or as the result of a `SIGKILL` or power failure.
|
|
943
995
|
|
|
944
996
|
If you need more control over interrupt-caused retries, include the `GoodJob::ActiveJobExtensions::InterruptErrors` extension in your job class. When an interrupted job is retried, the extension will raise a `GoodJob::InterruptError` exception within the job, which allows you to use Active Job's `retry_on` and `discard_on` to control the behavior of the job.
|
|
945
997
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module GoodJob
|
|
4
4
|
module ApplicationHelper
|
|
5
|
+
# Explicit helper inclusion because ApplicationController inherits from the host app.
|
|
6
|
+
#
|
|
7
|
+
# We can't rely on +config.action_controller.include_all_helpers = true+ in the host app.
|
|
8
|
+
include IconsHelper
|
|
9
|
+
|
|
5
10
|
def format_duration(sec)
|
|
6
11
|
return unless sec
|
|
7
12
|
|
|
@@ -24,42 +29,6 @@ module GoodJob
|
|
|
24
29
|
tag.time(text, datetime: timestamp, title: timestamp)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
STATUS_ICONS = {
|
|
28
|
-
discarded: "exclamation",
|
|
29
|
-
succeeded: "check",
|
|
30
|
-
queued: "dash_circle",
|
|
31
|
-
retried: "arrow_clockwise",
|
|
32
|
-
running: "play",
|
|
33
|
-
scheduled: "clock",
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
STATUS_COLOR = {
|
|
37
|
-
discarded: "danger",
|
|
38
|
-
succeeded: "success",
|
|
39
|
-
queued: "secondary",
|
|
40
|
-
retried: "warning",
|
|
41
|
-
running: "primary",
|
|
42
|
-
scheduled: "secondary",
|
|
43
|
-
}.freeze
|
|
44
|
-
|
|
45
|
-
def status_badge(status)
|
|
46
|
-
content_tag :span, status_icon(status, class: "text-white") + t(status, scope: 'good_job.status', count: 1),
|
|
47
|
-
class: "badge rounded-pill bg-#{STATUS_COLOR.fetch(status)} d-inline-flex gap-2 ps-1 pe-3 align-items-center"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def status_icon(status, **options)
|
|
51
|
-
options[:class] ||= "text-#{STATUS_COLOR.fetch(status)}"
|
|
52
|
-
icon = render_icon STATUS_ICONS.fetch(status)
|
|
53
|
-
content_tag :span, icon, **options
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def render_icon(name, **options)
|
|
57
|
-
# workaround to render svg icons without all of the log messages
|
|
58
|
-
partial = lookup_context.find_template("good_job/shared/icons/#{name}", [], true)
|
|
59
|
-
options[:class] = Array(options[:class]).join(" ")
|
|
60
|
-
partial.render(self, { class: options[:class] })
|
|
61
|
-
end
|
|
62
|
-
|
|
63
32
|
def translate_hash(key, **options)
|
|
64
33
|
translation_exists?(key, **options) ? translate(key, **options) : {}
|
|
65
34
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
module IconsHelper
|
|
5
|
+
STATUS_ICONS = {
|
|
6
|
+
discarded: "exclamation",
|
|
7
|
+
succeeded: "check",
|
|
8
|
+
queued: "dash_circle",
|
|
9
|
+
retried: "arrow_clockwise",
|
|
10
|
+
running: "play",
|
|
11
|
+
scheduled: "clock",
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
STATUS_COLOR = {
|
|
15
|
+
discarded: "danger",
|
|
16
|
+
succeeded: "success",
|
|
17
|
+
queued: "secondary",
|
|
18
|
+
retried: "warning",
|
|
19
|
+
running: "primary",
|
|
20
|
+
scheduled: "secondary",
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def status_badge(status)
|
|
24
|
+
content_tag :span, status_icon(status, class: "text-white") + t(status, scope: 'good_job.status', count: 1),
|
|
25
|
+
class: "badge rounded-pill bg-#{STATUS_COLOR.fetch(status)} d-inline-flex gap-2 ps-1 pe-3 align-items-center"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def status_icon(status, **options)
|
|
29
|
+
options[:class] ||= "text-#{STATUS_COLOR.fetch(status)}"
|
|
30
|
+
icon = render_icon STATUS_ICONS.fetch(status)
|
|
31
|
+
content_tag :span, icon, **options
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render_icon(name, **options)
|
|
35
|
+
# workaround to render svg icons without all of the log messages
|
|
36
|
+
partial = lookup_context.find_template("good_job/shared/icons/#{name}", [], true)
|
|
37
|
+
options[:class] = Array(options[:class]).join(" ")
|
|
38
|
+
partial.render(self, { class: options[:class] })
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -343,8 +343,10 @@ module GoodJob
|
|
|
343
343
|
execution.save!
|
|
344
344
|
|
|
345
345
|
if retried
|
|
346
|
-
CurrentThread.execution_retried =
|
|
346
|
+
CurrentThread.execution_retried = execution
|
|
347
347
|
CurrentThread.execution.retried_good_job_id = execution.id unless current_execution.discrete?
|
|
348
|
+
else
|
|
349
|
+
CurrentThread.execution_retried = nil
|
|
348
350
|
end
|
|
349
351
|
|
|
350
352
|
active_job.provider_job_id = execution.id
|
|
@@ -367,22 +369,24 @@ module GoodJob
|
|
|
367
369
|
run_callbacks(:perform) do
|
|
368
370
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
|
369
371
|
|
|
372
|
+
job_performed_at = Time.current
|
|
370
373
|
discrete_execution = nil
|
|
371
374
|
result = GoodJob::CurrentThread.within do |current_thread|
|
|
372
375
|
current_thread.reset
|
|
373
376
|
current_thread.execution = self
|
|
374
377
|
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
existing_performed_at = performed_at
|
|
379
|
+
if existing_performed_at
|
|
380
|
+
current_thread.execution_interrupted = existing_performed_at
|
|
377
381
|
|
|
378
382
|
if discrete?
|
|
379
|
-
interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{
|
|
383
|
+
interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{existing_performed_at}'"))
|
|
380
384
|
self.error = interrupt_error_string
|
|
381
385
|
self.error_event = ERROR_EVENT_INTERRUPTED if self.class.error_event_migrated?
|
|
382
386
|
|
|
383
387
|
discrete_execution_attrs = {
|
|
384
388
|
error: interrupt_error_string,
|
|
385
|
-
finished_at:
|
|
389
|
+
finished_at: job_performed_at,
|
|
386
390
|
}
|
|
387
391
|
discrete_execution_attrs[:error_event] = GoodJob::ErrorEvents::ERROR_EVENT_ENUMS[GoodJob::ErrorEvents::ERROR_EVENT_INTERRUPTED] if self.class.error_event_migrated?
|
|
388
392
|
discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
|
|
@@ -391,18 +395,17 @@ module GoodJob
|
|
|
391
395
|
|
|
392
396
|
if discrete?
|
|
393
397
|
transaction do
|
|
394
|
-
now = Time.current
|
|
395
398
|
discrete_execution = discrete_executions.create!(
|
|
396
399
|
job_class: job_class,
|
|
397
400
|
queue_name: queue_name,
|
|
398
401
|
serialized_params: serialized_params,
|
|
399
402
|
scheduled_at: (scheduled_at || created_at),
|
|
400
|
-
created_at:
|
|
403
|
+
created_at: job_performed_at
|
|
401
404
|
)
|
|
402
|
-
update!(performed_at:
|
|
405
|
+
update!(performed_at: job_performed_at, executions_count: ((executions_count || 0) + 1))
|
|
403
406
|
end
|
|
404
407
|
else
|
|
405
|
-
update!(performed_at:
|
|
408
|
+
update!(performed_at: job_performed_at)
|
|
406
409
|
end
|
|
407
410
|
|
|
408
411
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do |instrument_payload|
|
|
@@ -427,7 +430,7 @@ module GoodJob
|
|
|
427
430
|
instrument_payload.merge!(
|
|
428
431
|
value: value,
|
|
429
432
|
handled_error: handled_error,
|
|
430
|
-
retried: current_thread.execution_retried
|
|
433
|
+
retried: current_thread.execution_retried.present?,
|
|
431
434
|
error_event: error_event
|
|
432
435
|
)
|
|
433
436
|
ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
|
|
@@ -445,47 +448,49 @@ module GoodJob
|
|
|
445
448
|
end
|
|
446
449
|
end
|
|
447
450
|
|
|
448
|
-
|
|
451
|
+
job_attributes = {}
|
|
449
452
|
|
|
453
|
+
job_error = result.handled_error || result.unhandled_error
|
|
450
454
|
if job_error
|
|
451
455
|
error_string = self.class.format_error(job_error)
|
|
452
|
-
|
|
453
|
-
|
|
456
|
+
|
|
457
|
+
job_attributes[:error] = error_string
|
|
458
|
+
job_attributes[:error_event] = result.error_event if self.class.error_event_migrated?
|
|
454
459
|
if discrete_execution
|
|
455
460
|
discrete_execution.error = error_string
|
|
456
|
-
discrete_execution.error_event = result.error_event
|
|
461
|
+
discrete_execution.error_event = result.error_event
|
|
457
462
|
end
|
|
458
463
|
else
|
|
459
|
-
|
|
460
|
-
|
|
464
|
+
job_attributes[:error] = nil
|
|
465
|
+
job_attributes[:error_event] = nil
|
|
461
466
|
end
|
|
467
|
+
job_attributes.delete(:error_event) unless self.class.error_event_migrated?
|
|
468
|
+
|
|
469
|
+
job_finished_at = Time.current
|
|
470
|
+
job_attributes[:finished_at] = job_finished_at
|
|
471
|
+
discrete_execution.finished_at = job_finished_at if discrete_execution
|
|
462
472
|
|
|
463
|
-
|
|
464
|
-
|
|
473
|
+
retry_unhandled_error = result.unhandled_error && GoodJob.retry_on_unhandled_error
|
|
474
|
+
reenqueued = result.retried? || retried_good_job_id.present? || retry_unhandled_error
|
|
475
|
+
if reenqueued
|
|
465
476
|
if discrete_execution
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
update!(performed_at: nil, finished_at: nil, retried_good_job_id: nil)
|
|
469
|
-
end
|
|
477
|
+
job_attributes[:performed_at] = nil
|
|
478
|
+
job_attributes[:finished_at] = nil
|
|
470
479
|
else
|
|
471
|
-
|
|
480
|
+
job_attributes[:retried_good_job_id] = retried_good_job_id
|
|
481
|
+
job_attributes[:finished_at] = nil if retry_unhandled_error
|
|
472
482
|
end
|
|
473
|
-
|
|
474
|
-
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
preserve_unhandled = (result.unhandled_error && (GoodJob.retry_on_unhandled_error || GoodJob.preserve_job_records == :on_unhandled_error))
|
|
486
|
+
if GoodJob.preserve_job_records == true || reenqueued || preserve_unhandled || cron_key.present?
|
|
475
487
|
if discrete_execution
|
|
476
|
-
if reenqueued
|
|
477
|
-
self.performed_at = nil
|
|
478
|
-
else
|
|
479
|
-
self.finished_at = now
|
|
480
|
-
end
|
|
481
|
-
discrete_execution.finished_at = now
|
|
482
488
|
transaction do
|
|
483
489
|
discrete_execution.save!
|
|
484
|
-
|
|
490
|
+
update!(job_attributes)
|
|
485
491
|
end
|
|
486
492
|
else
|
|
487
|
-
|
|
488
|
-
save!
|
|
493
|
+
update!(job_attributes)
|
|
489
494
|
end
|
|
490
495
|
else
|
|
491
496
|
destroy_job
|
|
@@ -556,6 +561,12 @@ module GoodJob
|
|
|
556
561
|
@_destroy_job = false
|
|
557
562
|
end
|
|
558
563
|
|
|
564
|
+
def job_state
|
|
565
|
+
state = { queue_name: queue_name }
|
|
566
|
+
state[:scheduled_at] = scheduled_at if scheduled_at
|
|
567
|
+
state
|
|
568
|
+
end
|
|
569
|
+
|
|
559
570
|
private
|
|
560
571
|
|
|
561
572
|
def reset_batch_values(&block)
|
|
@@ -13,9 +13,8 @@ module GoodJob
|
|
|
13
13
|
attr_reader :error_event
|
|
14
14
|
# @return [Boolean, nil]
|
|
15
15
|
attr_reader :unexecutable
|
|
16
|
-
# @return [
|
|
16
|
+
# @return [GoodJob::Execution, nil]
|
|
17
17
|
attr_reader :retried
|
|
18
|
-
alias retried? retried
|
|
19
18
|
|
|
20
19
|
# @param value [Object, nil]
|
|
21
20
|
# @param handled_error [Exception, nil]
|
|
@@ -23,7 +22,7 @@ module GoodJob
|
|
|
23
22
|
# @param error_event [String, nil]
|
|
24
23
|
# @param unexecutable [Boolean, nil]
|
|
25
24
|
# @param retried [Boolean, nil]
|
|
26
|
-
def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried:
|
|
25
|
+
def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried: nil)
|
|
27
26
|
@value = value
|
|
28
27
|
@handled_error = handled_error
|
|
29
28
|
@unhandled_error = unhandled_error
|
|
@@ -34,7 +33,12 @@ module GoodJob
|
|
|
34
33
|
|
|
35
34
|
# @return [Boolean]
|
|
36
35
|
def succeeded?
|
|
37
|
-
!(handled_error || unhandled_error || unexecutable || retried)
|
|
36
|
+
!(handled_error || unhandled_error || unexecutable || retried?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def retried?
|
|
41
|
+
retried.present?
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
end
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -195,12 +195,20 @@ module GoodJob
|
|
|
195
195
|
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
|
196
196
|
active_job.executions = (active_job.executions || 0) + 1
|
|
197
197
|
|
|
198
|
+
begin
|
|
199
|
+
error_class, error_message = execution.error.split(GoodJob::Execution::ERROR_MESSAGE_SEPARATOR).map(&:strip)
|
|
200
|
+
error = error_class.constantize.new(error_message)
|
|
201
|
+
rescue StandardError
|
|
202
|
+
error = StandardError.new(execution.error)
|
|
203
|
+
end
|
|
204
|
+
|
|
198
205
|
new_active_job = nil
|
|
199
206
|
GoodJob::CurrentThread.within do |current_thread|
|
|
200
207
|
current_thread.execution = execution
|
|
208
|
+
current_thread.retry_now = true
|
|
201
209
|
|
|
202
210
|
execution.class.transaction(joinable: false, requires_new: true) do
|
|
203
|
-
new_active_job = active_job.retry_job(wait: 0, error:
|
|
211
|
+
new_active_job = active_job.retry_job(wait: 0, error: error)
|
|
204
212
|
execution.error_event = ERROR_EVENT_RETRIED if execution.error && execution.class.error_event_migrated?
|
|
205
213
|
execution.save!
|
|
206
214
|
end
|
|
@@ -6,7 +6,7 @@ module GoodJob # :nodoc:
|
|
|
6
6
|
# ActiveRecord model that represents an GoodJob process (either async or CLI).
|
|
7
7
|
class Process < BaseRecord
|
|
8
8
|
include AdvisoryLockable
|
|
9
|
-
include
|
|
9
|
+
include OverridableConnection
|
|
10
10
|
|
|
11
11
|
# Interval until the process record being updated
|
|
12
12
|
STALE_INTERVAL = 30.seconds
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Content added to this partial will be displayed above the collapsible JSON representation of each execution on the jobs#show view.
|
|
3
|
+
|
|
4
|
+
You can make use of the following variables:
|
|
5
|
+
|
|
6
|
+
- `job`: The `GoodJob::Job` instance.
|
|
7
|
+
- `execution`: The `GoodJob::DiscreteExecution` instance.
|
|
8
|
+
- `main_app`: Use this to access helpers (e.g. route helpers) from your application.
|
|
9
|
+
|
|
10
|
+
Note: the `GoodJob::Job` and `GoodJob::Execution` classes are considered an internal implementation detail of GoodJob and may change at any time. Please test your view extensions when upgrading.
|
|
11
|
+
%>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Content added to this partial will be displayed above the argument list on the good_job/jobs#show page.
|
|
3
|
+
|
|
4
|
+
You can make use of the following variables:
|
|
5
|
+
|
|
6
|
+
- `job`: The `GoodJob::Job` instance. Calling `job.active_job` will deserialize an instance of your ActiveJob class.
|
|
7
|
+
- `main_app`: Use this to access helpers (e.g. route helpers) from your application.
|
|
8
|
+
|
|
9
|
+
Note: the `GoodJob::Job` class is considered an internal implementation detail and may change at any time. Please test your view extensions when upgrading.
|
|
10
|
+
%>
|
|
@@ -52,27 +52,27 @@
|
|
|
52
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_icon :dots %>
|
|
56
56
|
<span class="visually-hidden"><%=t ".actions.title" %></span>
|
|
57
57
|
</button>
|
|
58
58
|
<ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
|
|
59
59
|
<li>
|
|
60
60
|
<% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
61
61
|
<%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t(".actions.reschedule"), data: { confirm: t(".actions.confirm_reschedule"), disable: true } do %>
|
|
62
|
-
<%=
|
|
62
|
+
<%= render_icon "skip_forward" %>
|
|
63
63
|
<%=t "good_job.actions.reschedule" %>
|
|
64
64
|
<% end %>
|
|
65
65
|
</li>
|
|
66
66
|
<li>
|
|
67
67
|
<% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
68
68
|
<%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t(".actions.discard"), data: { confirm: t(".actions.confirm_discard"), disable: true } do %>
|
|
69
|
-
<%=
|
|
69
|
+
<%= render_icon "stop" %>
|
|
70
70
|
<%=t "good_job.actions.discard" %>
|
|
71
71
|
<% end %>
|
|
72
72
|
</li>
|
|
73
73
|
<li>
|
|
74
74
|
<%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t(".actions.retry"), data: { confirm: t(".actions.confirm_retry"), disable: true } do %>
|
|
75
|
-
<%=
|
|
75
|
+
<%= render_icon "arrow_clockwise" %>
|
|
76
76
|
<%=t "good_job.actions.retry" %>
|
|
77
77
|
<% end %>
|
|
78
78
|
</li>
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
<% end %>
|
|
42
42
|
<% end %>
|
|
43
|
+
<%= render 'good_job/custom_execution_details', execution: execution, job: @job %>
|
|
43
44
|
<%= tag.div id: dom_id(execution, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
|
|
44
45
|
<%= tag.pre JSON.pretty_generate(execution.display_serialized_params) %>
|
|
45
46
|
<% end %>
|
|
@@ -109,34 +109,34 @@
|
|
|
109
109
|
|
|
110
110
|
<div class="dropdown float-end">
|
|
111
111
|
<button class="d-flex align-items-center btn btn-sm" type="button" id="<%= dom_id(job, :actions) %>" data-bs-toggle="dropdown" aria-expanded="false">
|
|
112
|
-
<%=
|
|
112
|
+
<%= render_icon "dots" %>
|
|
113
113
|
<span class="visually-hidden"><%=t ".actions.title" %></span>
|
|
114
114
|
</button>
|
|
115
115
|
<ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
|
|
116
116
|
<li>
|
|
117
117
|
<% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
118
118
|
<%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t("good_job.jobs.actions.reschedule"), data: { confirm: t("good_job.jobs.actions.confirm_reschedule"), disable: true } do %>
|
|
119
|
-
<%=
|
|
119
|
+
<%= render_icon "skip_forward" %>
|
|
120
120
|
<%=t "good_job.actions.reschedule" %>
|
|
121
121
|
<% end %>
|
|
122
122
|
</li>
|
|
123
123
|
<li>
|
|
124
124
|
<% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
125
125
|
<%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t("good_job.jobs.actions.discard"), data: { confirm: t("good_job.jobs.actions.confirm_discard"), disable: true } do %>
|
|
126
|
-
<%=
|
|
126
|
+
<%= render_icon "stop" %>
|
|
127
127
|
<%=t "good_job.actions.discard" %>
|
|
128
128
|
<% end %>
|
|
129
129
|
</li>
|
|
130
130
|
<li>
|
|
131
131
|
<% job_force_discardable = job.status.in? [:running] %>
|
|
132
132
|
<%= link_to force_discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { confirm: t("good_job.jobs.actions.confirm_force_discard"), disable: true } do %>
|
|
133
|
-
<%=
|
|
133
|
+
<%= render_icon "eject" %>
|
|
134
134
|
<%=t "good_job.actions.force_discard" %>
|
|
135
135
|
<% end %>
|
|
136
136
|
</li>
|
|
137
137
|
<li>
|
|
138
138
|
<%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry"), disable: true } do %>
|
|
139
|
-
<%=
|
|
139
|
+
<%= render_icon "arrow_clockwise" %>
|
|
140
140
|
<%=t "good_job.actions.retry" %>
|
|
141
141
|
<% end %>
|
|
142
142
|
</li>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<% if notice %>
|
|
3
3
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
4
4
|
<div class="toast-body d-flex align-items-center gap-2">
|
|
5
|
-
<%=
|
|
5
|
+
<%= render_icon "check", class: "flex-shrink-0 text-success" %>
|
|
6
6
|
<div class="flex-fill"><%= notice %></div>
|
|
7
7
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
8
8
|
</div>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<% if alert %>
|
|
12
12
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
13
13
|
<div class="toast-body d-flex align-items-center gap-2">
|
|
14
|
-
<%=
|
|
14
|
+
<%= render_icon "exclamation", class: "flex-shrink-0 text-danger" %>
|
|
15
15
|
<div class="flex-fill"><%= alert %></div>
|
|
16
16
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
17
17
|
</div>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
<li class="nav-item d-flex flex-column justify-content-center">
|
|
62
62
|
<div class="form-check form-switch m-0">
|
|
63
|
-
<%= check_box_tag "live_poll", params.fetch("poll", 30), params[:poll].present
|
|
63
|
+
<%= check_box_tag "live_poll", params.fetch("poll", 30), (GoodJob.configuration.dashboard_live_poll_enabled && params[:poll].present?), role: "switch", class: "form-check-input", disabled: !GoodJob.configuration.dashboard_live_poll_enabled %>
|
|
64
64
|
<label class="form-check-label navbar-text p-0" for="live_poll">
|
|
65
65
|
<%= t(".live_poll") %>
|
|
66
66
|
</label>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- https://icons.getbootstrap.com/icons/check-circle/ -->
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle <%= local_assigns[:class] %>" viewBox="0 0 16 16">
|
|
3
3
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
|
4
4
|
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
|
|
5
5
|
</svg>
|