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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b464ea5a307aa615b5800649e4ff4083276900fe42bfb32edfc9ed66c470914
4
- data.tar.gz: 7ff522ebb69f55a222a089ddc7c2b1fc2e0835603bcf515a630b2b27a2e90b7a
3
+ metadata.gz: a51ff44544ae7fb381fce50f13d3196d6a561bcdc6a5af7f37ad1e8fb0d6b258
4
+ data.tar.gz: 3703fe631d3f549bb152999203e5d67ac6d305c4ac77bba5b47621f3419cef21
5
5
  SHA512:
6
- metadata.gz: ae2eb90d2b35711d3d4ade8ed678bba89e36fa32eb36b6bb291a4b99fbe0d60b4ea2a143521f9b20b6d386b745d690f8e6b99467b0ec1b05d9e44709f71a6b7f
7
- data.tar.gz: a695f55188543d9ea6d53f1f25c4e3cfb311d4a4d7dddbed342b8034e5d09cf911f212e57fe4fba0ab64c0cc8ce0f9dcb2529756dc9c701403e5e6ee1b8113e7
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](#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, for example as the result of a `SIGKILL` or power failure.
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 = true
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
- if performed_at
376
- current_thread.execution_interrupted = performed_at
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 '#{performed_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: Time.current,
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: now
403
+ created_at: job_performed_at
401
404
  )
402
- update!(performed_at: now, executions_count: ((executions_count || 0) + 1))
405
+ update!(performed_at: job_performed_at, executions_count: ((executions_count || 0) + 1))
403
406
  end
404
407
  else
405
- update!(performed_at: Time.current)
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
- job_error = result.handled_error || result.unhandled_error
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
- self.error = error_string
453
- self.error_event = result.error_event if self.class.error_event_migrated?
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 if discrete_execution.class.error_event_migrated?
461
+ discrete_execution.error_event = result.error_event
457
462
  end
458
463
  else
459
- self.error = nil
460
- self.error_event = nil if self.class.error_event_migrated?
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
- reenqueued = result.retried? || retried_good_job_id.present?
464
- if result.unhandled_error && GoodJob.retry_on_unhandled_error
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
- transaction do
467
- discrete_execution.update!(finished_at: Time.current)
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
- save!
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
- elsif GoodJob.preserve_job_records == true || reenqueued || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error) || cron_key.present?
474
- now = Time.current
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
- save!
490
+ update!(job_attributes)
485
491
  end
486
492
  else
487
- self.finished_at = now
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 [Boolean, nil]
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: false)
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
@@ -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: execution.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 AssignableConnection
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
- <%= render "good_job/shared/icons/dots" %>
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
- <%= render "good_job/shared/icons/skip_forward" %>
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
- <%= render "good_job/shared/icons/stop" %>
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
- <%= render "good_job/shared/icons/arrow_clockwise" %>
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
- <%= render "good_job/shared/icons/dots" %>
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
- <%= render "good_job/shared/icons/skip_forward" %>
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
- <%= render "good_job/shared/icons/stop" %>
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
- <%= render "good_job/shared/icons/eject" %>
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
- <%= render "good_job/shared/icons/arrow_clockwise" %>
139
+ <%= render_icon "arrow_clockwise" %>
140
140
  <%=t "good_job.actions.retry" %>
141
141
  <% end %>
142
142
  </li>
@@ -67,6 +67,8 @@
67
67
  </div>
68
68
  </div>
69
69
 
70
+ <%= render 'good_job/custom_job_details', job: @job %>
71
+
70
72
  <div class="my-4">
71
73
  <h5>
72
74
  <%= t "good_job.models.job.arguments" %>
@@ -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
- <%= render "good_job/shared/icons/check", class: "flex-shrink-0 text-success" %>
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
- <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 text-danger" %>
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?, role: "switch", class: "form-check-input" %>
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>