good_job 3.24.0 → 3.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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