good_job 3.24.0 → 3.25.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: 47f288c089d005a2b97e5fe6d7bd8fb7094d48fb52565bacf0493b5e547f6516
4
+ data.tar.gz: cf8cd69a7a1f64fee5172b7fd3b184d774eed0cd48d97ba0d01971af0d721646
5
5
  SHA512:
6
- metadata.gz: ae2eb90d2b35711d3d4ade8ed678bba89e36fa32eb36b6bb291a4b99fbe0d60b4ea2a143521f9b20b6d386b745d690f8e6b99467b0ec1b05d9e44709f71a6b7f
7
- data.tar.gz: a695f55188543d9ea6d53f1f25c4e3cfb311d4a4d7dddbed342b8034e5d09cf911f212e57fe4fba0ab64c0cc8ce0f9dcb2529756dc9c701403e5e6ee1b8113e7
6
+ metadata.gz: 3c74189b7315da1fcb5747b285cb4d7837550dcf1456899f2d6f023065a893bd99c93a7e292415b6796a64232335a732666a7783fc04f2319968fff4bfaf7dcc
7
+ data.tar.gz: 67fd833bfa126355ef0853263c7e72b5bfa5d013e1061bc128480408a52b49eddf9724686e3dc47293f8713696074f8029452c035159aee50940f0b320ff36ab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.25.0](https://github.com/bensheldon/good_job/tree/v3.25.0) (2024-02-22)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.24.0...v3.25.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Allow disabling of Dashboard Live Polling configuration [\#1235](https://github.com/bensheldon/good_job/pull/1235) ([erick-tmr](https://github.com/erick-tmr))
10
+ - Add customizable extension partials to good\_job/jobs\#show view [\#1200](https://github.com/bensheldon/good_job/pull/1200) ([grncdr](https://github.com/grncdr))
11
+
12
+ **Fixed bugs:**
13
+
14
+ - Fix default engine cron value [\#1258](https://github.com/bensheldon/good_job/pull/1258) ([hss-mateus](https://github.com/hss-mateus))
15
+ - Print an error when daemon pidfile dir doesn't exist [\#1252](https://github.com/bensheldon/good_job/pull/1252) ([thepry](https://github.com/thepry))
16
+
17
+ **Closed issues:**
18
+
19
+ - Production deployment question [\#1257](https://github.com/bensheldon/good_job/issues/1257)
20
+ - Daemon and App not connecting to secondary database [\#1254](https://github.com/bensheldon/good_job/issues/1254)
21
+ - Logging with logger.warn in classes is suppressed by good job? \(semantic\_logger\) [\#1250](https://github.com/bensheldon/good_job/issues/1250)
22
+
23
+ **Merged pull requests:**
24
+
25
+ - Fix Active Record connection changes on Rails head [\#1259](https://github.com/bensheldon/good_job/pull/1259) ([bensheldon](https://github.com/bensheldon))
26
+ - \[Docs\] Bulk.enqueue takes an array of jobs [\#1256](https://github.com/bensheldon/good_job/pull/1256) ([jpcamara](https://github.com/jpcamara))
27
+ - Clean up icon helpers for less noisy view rendering [\#1248](https://github.com/bensheldon/good_job/pull/1248) ([bensheldon](https://github.com/bensheldon))
28
+ - Use dotenv-rails instead of dotenv [\#1247](https://github.com/bensheldon/good_job/pull/1247) ([bensheldon](https://github.com/bensheldon))
29
+ - Perform inline retries iteratively instead of recursively [\#1246](https://github.com/bensheldon/good_job/pull/1246) ([bensheldon](https://github.com/bensheldon))
30
+
3
31
  ## [v3.24.0](https://github.com/bensheldon/good_job/tree/v3.24.0) (2024-02-12)
4
32
 
5
33
  [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)
@@ -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
@@ -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>
@@ -1,5 +1,5 @@
1
1
  <!-- https://icons.getbootstrap.com/icons/exclamation-circle/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-circle" viewBox="0 0 16 16">
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-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="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
5
5
  </svg>
@@ -96,6 +96,13 @@ module GoodJob
96
96
  begin
97
97
  inline_execution = inline_executions.shift
98
98
  inline_result = inline_execution.perform
99
+
100
+ retried_execution = inline_result.retried
101
+ while retried_execution && retried_execution.scheduled_at <= Time.current
102
+ inline_execution = retried_execution
103
+ inline_result = inline_execution.perform
104
+ retried_execution = inline_result.retried
105
+ end
99
106
  ensure
100
107
  inline_execution.advisory_unlock
101
108
  inline_execution.run_callbacks(:perform_unlocked)
@@ -141,26 +148,43 @@ module GoodJob
141
148
 
142
149
  Rails.application.executor.wrap do
143
150
  will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
144
- execution = GoodJob::Execution.enqueue(
145
- active_job,
146
- scheduled_at: scheduled_at,
147
- create_with_advisory_lock: will_execute_inline
148
- )
149
-
150
- if will_execute_inline
151
+ will_retry_inline = will_execute_inline && CurrentThread.execution&.active_job_id == active_job.job_id && !CurrentThread.retry_now
152
+
153
+ if will_retry_inline
154
+ execution = GoodJob::Execution.enqueue(
155
+ active_job,
156
+ scheduled_at: scheduled_at
157
+ )
158
+ elsif will_execute_inline
159
+ execution = GoodJob::Execution.enqueue(
160
+ active_job,
161
+ scheduled_at: scheduled_at,
162
+ create_with_advisory_lock: true
163
+ )
151
164
  begin
152
165
  result = execution.perform
166
+
167
+ retried_execution = result.retried
168
+ while retried_execution && (retried_execution.scheduled_at.nil? || retried_execution.scheduled_at <= Time.current)
169
+ execution = retried_execution
170
+ result = execution.perform
171
+ retried_execution = result.retried
172
+ end
173
+
174
+ Notifier.notify(retried_execution.job_state) if retried_execution&.scheduled_at && retried_execution.scheduled_at > Time.current && send_notify?(active_job)
153
175
  ensure
154
176
  execution.advisory_unlock
155
177
  execution.run_callbacks(:perform_unlocked)
156
178
  end
157
179
  raise result.unhandled_error if result.unhandled_error
158
180
  else
159
- job_state = { queue_name: execution.queue_name }
160
- job_state[:scheduled_at] = execution.scheduled_at if execution.scheduled_at
181
+ execution = GoodJob::Execution.enqueue(
182
+ active_job,
183
+ scheduled_at: scheduled_at
184
+ )
161
185
 
162
- executed_locally = execute_async? && @capsule&.create_thread(job_state)
163
- Notifier.notify(job_state) if !executed_locally && send_notify?(active_job)
186
+ executed_locally = execute_async? && @capsule&.create_thread(execution.job_state)
187
+ Notifier.notify(execution.job_state) if !executed_locally && send_notify?(active_job)
164
188
  end
165
189
 
166
190
  execution
@@ -31,6 +31,8 @@ module GoodJob
31
31
  DEFAULT_ENABLE_LISTEN_NOTIFY = true
32
32
  # Default Dashboard I18n locale
33
33
  DEFAULT_DASHBOARD_DEFAULT_LOCALE = :en
34
+ # Default Dashboard Live Poll button enabled
35
+ DEFAULT_DASHBOARD_LIVE_POLL_ENABLED = true
34
36
 
35
37
  def self.validate_execution_mode(execution_mode)
36
38
  raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
@@ -204,9 +206,10 @@ module GoodJob
204
206
 
205
207
  def cron
206
208
  env_cron = JSON.parse(ENV.fetch('GOOD_JOB_CRON'), symbolize_names: true) if ENV['GOOD_JOB_CRON'].present?
209
+ rails_config_cron = rails_config[:cron].presence
207
210
 
208
211
  options[:cron] ||
209
- rails_config[:cron] ||
212
+ rails_config_cron ||
210
213
  env_cron ||
211
214
  {}
212
215
  end
@@ -380,6 +383,12 @@ module GoodJob
380
383
  rails_config[:dashboard_default_locale] || DEFAULT_DASHBOARD_DEFAULT_LOCALE
381
384
  end
382
385
 
386
+ def dashboard_live_poll_enabled
387
+ return rails_config[:dashboard_live_poll_enabled] unless rails_config[:dashboard_live_poll_enabled].nil?
388
+
389
+ DEFAULT_DASHBOARD_LIVE_POLL_ENABLED
390
+ end
391
+
383
392
  # Whether running in a web server process.
384
393
  # @return [Boolean, nil]
385
394
  def in_webserver?
@@ -16,6 +16,7 @@ module GoodJob
16
16
  execution
17
17
  execution_interrupted
18
18
  execution_retried
19
+ retry_now
19
20
  ].freeze
20
21
 
21
22
  # @!attribute [rw] cron_at
@@ -66,6 +67,12 @@ module GoodJob
66
67
  # @return [Boolean, nil]
67
68
  thread_mattr_accessor :execution_retried
68
69
 
70
+ # @!attribute [rw] retry_now
71
+ # @!scope class
72
+ # Execution Retried
73
+ # @return [Boolean, nil]
74
+ thread_mattr_accessor :retry_now
75
+
69
76
  # Resets attributes
70
77
  # @param [Hash] values to assign
71
78
  # @return [void]
@@ -17,6 +17,7 @@ module GoodJob
17
17
  # Daemonizes the current process and writes out a pidfile.
18
18
  # @return [void]
19
19
  def daemonize
20
+ check_pid_dir
20
21
  check_pid
21
22
  ::Process.daemon
22
23
  write_pid
@@ -38,6 +39,14 @@ module GoodJob
38
39
  File.delete(pidfile) if File.exist?(pidfile) # rubocop:disable Lint/NonAtomicFileOperation
39
40
  end
40
41
 
42
+ # @return [void]
43
+ def check_pid_dir
44
+ dirname = File.dirname(pidfile)
45
+ return if Dir.exist?(dirname)
46
+
47
+ abort "Pidfile directory \"#{dirname}\" doesn't exist. Aborting..."
48
+ end
49
+
41
50
  # @return [void]
42
51
  def check_pid
43
52
  case pid_status(pidfile)
@@ -14,7 +14,7 @@ module GoodJob # :nodoc:
14
14
 
15
15
  # Registers the current process.
16
16
  def register_process
17
- GoodJob::Process.with_connection(connection) do
17
+ GoodJob::Process.override_connection(connection) do
18
18
  GoodJob::Process.cleanup
19
19
  @process = GoodJob::Process.register
20
20
  end
@@ -22,7 +22,7 @@ module GoodJob # :nodoc:
22
22
 
23
23
  def refresh_process
24
24
  Rails.application.executor.wrap do
25
- GoodJob::Process.with_connection(connection) do
25
+ GoodJob::Process.override_connection(connection) do
26
26
  GoodJob::Process.with_logger_silenced do
27
27
  @process&.refresh_if_stale(cleanup: true)
28
28
  end
@@ -32,7 +32,7 @@ module GoodJob # :nodoc:
32
32
 
33
33
  # Deregisters the current process.
34
34
  def deregister_process
35
- GoodJob::Process.with_connection(connection) do
35
+ GoodJob::Process.override_connection(connection) do
36
36
  @process&.deregister
37
37
  end
38
38
  end
@@ -3,36 +3,29 @@
3
3
  module GoodJob # :nodoc:
4
4
  # Extends an ActiveRecord odel to override the connection and use
5
5
  # an explicit connection that has been removed from the pool.
6
- module AssignableConnection
6
+ module OverridableConnection
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- thread_cattr_accessor :_connection
10
+ thread_cattr_accessor :_overridden_connection
11
11
  end
12
12
 
13
13
  class_methods do
14
- # Assigns a connection to the model.
15
- # @param conn [ActiveRecord::ConnectionAdapters::AbstractAdapter]
16
- # @return [void]
17
- def connection=(conn)
18
- self._connection = conn
19
- end
20
-
21
14
  # Overrides the existing connection method to use the assigned connection
22
15
  # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
23
16
  def connection
24
- _connection || super
17
+ _overridden_connection || super
25
18
  end
26
19
 
27
20
  # Block interface to assign the connection, yield, then unassign the connection.
28
21
  # @param conn [ActiveRecord::ConnectionAdapters::AbstractAdapter]
29
22
  # @return [void]
30
- def with_connection(conn)
31
- original_conn = _connection
32
- self.connection = conn
23
+ def override_connection(conn)
24
+ original_conn = _overridden_connection
25
+ self._overridden_connection = conn
33
26
  yield
34
27
  ensure
35
- self._connection = original_conn
28
+ self._overridden_connection = original_conn
36
29
  end
37
30
  end
38
31
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '3.24.0'
5
+ VERSION = '3.25.0'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -15,7 +15,7 @@ require "good_job/active_job_extensions/interrupt_errors"
15
15
  require "good_job/active_job_extensions/labels"
16
16
  require "good_job/active_job_extensions/notify_options"
17
17
 
18
- require "good_job/assignable_connection"
18
+ require "good_job/overridable_connection"
19
19
  require "good_job/bulk"
20
20
  require "good_job/callable"
21
21
  require "good_job/capsule"
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.24.0
4
+ version: 3.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-12 00:00:00.000000000 Z
11
+ date: 2024-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -288,6 +288,7 @@ files:
288
288
  - app/frontend/good_job/vendor/rails_ujs.js
289
289
  - app/frontend/good_job/vendor/stimulus.js
290
290
  - app/helpers/good_job/application_helper.rb
291
+ - app/helpers/good_job/icons_helper.rb
291
292
  - app/models/concerns/good_job/advisory_lockable.rb
292
293
  - app/models/concerns/good_job/error_events.rb
293
294
  - app/models/concerns/good_job/filterable.rb
@@ -305,6 +306,8 @@ files:
305
306
  - app/models/good_job/job.rb
306
307
  - app/models/good_job/process.rb
307
308
  - app/models/good_job/setting.rb
309
+ - app/views/good_job/_custom_execution_details.html.erb
310
+ - app/views/good_job/_custom_job_details.html.erb
308
311
  - app/views/good_job/batches/_jobs.erb
309
312
  - app/views/good_job/batches/_table.erb
310
313
  - app/views/good_job/batches/index.html.erb
@@ -375,7 +378,6 @@ files:
375
378
  - lib/good_job/active_job_extensions/labels.rb
376
379
  - lib/good_job/active_job_extensions/notify_options.rb
377
380
  - lib/good_job/adapter.rb
378
- - lib/good_job/assignable_connection.rb
379
381
  - lib/good_job/bulk.rb
380
382
  - lib/good_job/callable.rb
381
383
  - lib/good_job/capsule.rb
@@ -394,6 +396,7 @@ files:
394
396
  - lib/good_job/multi_scheduler.rb
395
397
  - lib/good_job/notifier.rb
396
398
  - lib/good_job/notifier/process_heartbeat.rb
399
+ - lib/good_job/overridable_connection.rb
397
400
  - lib/good_job/poller.rb
398
401
  - lib/good_job/probe_server.rb
399
402
  - lib/good_job/probe_server/healthcheck_middleware.rb