good_job 3.23.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: c4b22e7bf97af388b110d33742f0e1ce762570a509c85b8abd8425d668f691c4
4
- data.tar.gz: 4f5ab8ec5fef5a2996f917851cea61a967fc73ac08afcd61edc248a5f121606e
3
+ metadata.gz: 47f288c089d005a2b97e5fe6d7bd8fb7094d48fb52565bacf0493b5e547f6516
4
+ data.tar.gz: cf8cd69a7a1f64fee5172b7fd3b184d774eed0cd48d97ba0d01971af0d721646
5
5
  SHA512:
6
- metadata.gz: b20eabb8bd33b6c1223a8d60244a75681c939193ba22904531cf4c5d4f92ca2266665e2feb60dd7ee9db8b6ba017d3980bd789b4d90c7c9806b5db42ce08bdb9
7
- data.tar.gz: da0c451f930fb9226a74ce6d03aea3dd5bdb61cfff4e00d3e9f0664032105e357eea653d86c79759f3b67a999e59306f4cc33f9684d7edcdea48fee322ad2e27
6
+ metadata.gz: 3c74189b7315da1fcb5747b285cb4d7837550dcf1456899f2d6f023065a893bd99c93a7e292415b6796a64232335a732666a7783fc04f2319968fff4bfaf7dcc
7
+ data.tar.gz: 67fd833bfa126355ef0853263c7e72b5bfa5d013e1061bc128480408a52b49eddf9724686e3dc47293f8713696074f8029452c035159aee50940f0b320ff36ab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
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
+
31
+ ## [v3.24.0](https://github.com/bensheldon/good_job/tree/v3.24.0) (2024-02-12)
32
+
33
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.23.0...v3.24.0)
34
+
35
+ **Fixed bugs:**
36
+
37
+ - Fix batches so that retried-and-successful jobs leave the batch succeeded [\#1243](https://github.com/bensheldon/good_job/pull/1243) ([bensheldon](https://github.com/bensheldon))
38
+ - Use the job class as the default concurrency key if none is provided [\#1145](https://github.com/bensheldon/good_job/pull/1145) ([Earlopain](https://github.com/Earlopain))
39
+
40
+ **Closed issues:**
41
+
42
+ - Batch callbacks not run when job fails, then succeeds [\#1239](https://github.com/bensheldon/good_job/issues/1239)
43
+ - Broken pipe @ io\_writev - \<STDERR\> \(Errno::EPIPE\) [\#1233](https://github.com/bensheldon/good_job/issues/1233)
44
+ - PG::UniqueViolation unique constraint "index\_good\_jobs\_on\_cron\_key\_and\_cron\_at\_cond" [\#1230](https://github.com/bensheldon/good_job/issues/1230)
45
+ - Default concurrency key [\#1110](https://github.com/bensheldon/good_job/issues/1110)
46
+
47
+ **Merged pull requests:**
48
+
49
+ - Use Ruby 3.3 for development; add Bootsnap; update to Rails 7.1.3 [\#1240](https://github.com/bensheldon/good_job/pull/1240) ([bensheldon](https://github.com/bensheldon))
50
+ - Tweak docs for new concurrency default [\#1229](https://github.com/bensheldon/good_job/pull/1229) ([Earlopain](https://github.com/Earlopain))
51
+ - Brazilian Portuguese locale [\#1226](https://github.com/bensheldon/good_job/pull/1226) ([hss-mateus](https://github.com/hss-mateus))
52
+
3
53
  ## [v3.23.0](https://github.com/bensheldon/good_job/tree/v3.23.0) (2024-01-23)
4
54
 
5
55
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.22.0...v3.23.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`.
@@ -511,12 +548,25 @@ class MyJob < ApplicationJob
511
548
 
512
549
  # A unique key to be globally locked against.
513
550
  # Can be String or Lambda/Proc that is invoked in the context of the job.
551
+ #
552
+ # If a key is not provided GoodJob will use the job class name.
553
+ #
554
+ # To disable concurrency control, for example in a subclass, set the
555
+ # key explicitly to nil (e.g. `key: nil` or `key: -> { nil }`)
556
+ #
557
+ # If you provide a custom concurrency key (for example, if concurrency is supposed
558
+ # to be controlled by the first job argument) make sure that it is sufficiently unique across
559
+ # jobs and queues by adding the job class or queue to the key yourself, if needed.
560
+ #
561
+ # Note: When using a model instance as part of your custom concurrency key, make sure
562
+ # to explicitly use its `id` or `to_global_id` because otherwise it will not stringify as expected.
563
+ #
514
564
  # Note: Arguments passed to #perform_later can be accessed through Active Job's `arguments` method
515
565
  # which is an array containing positional arguments and, optionally, a kwarg hash.
516
- key: -> { "MyJob-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "MyJob-Alice-v2"
566
+ key: -> { "#{self.class.name}-#{queue_name}-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "MyJob-default-Alice-v2"
517
567
  )
518
568
 
519
- def perform(first_name)
569
+ def perform(first_name, version:)
520
570
  # do work
521
571
  end
522
572
  end
@@ -525,8 +575,8 @@ end
525
575
  When testing, the resulting concurrency key value can be inspected:
526
576
 
527
577
  ```ruby
528
- job = MyJob.perform_later("Alice")
529
- job.good_job_concurrency_key #=> "MyJob-Alice"
578
+ job = MyJob.perform_later("Alice", version: 'v1')
579
+ job.good_job_concurrency_key #=> "MyJob-default-Alice-v1"
530
580
  ```
531
581
 
532
582
  #### How concurrency controls work
@@ -596,7 +646,7 @@ end
596
646
  active_jobs.all?(&:provider_job_id)
597
647
 
598
648
  # Bulk enqueue Active Job instances directly without using `.perform_later`:
599
- GoodJob::Bulk.enqueue(MyJob.new, AnotherJob.new)
649
+ GoodJob::Bulk.enqueue([MyJob.new, AnotherJob.new])
600
650
  ```
601
651
 
602
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
@@ -46,7 +46,7 @@ module GoodJob
46
46
  end
47
47
 
48
48
  def _continue_discard_or_finish(execution = nil, lock: true)
49
- execution_discarded = execution && execution.error.present? && execution.retried_good_job_id.nil?
49
+ execution_discarded = execution && execution.error.present? && execution.finished_at && execution.retried_good_job_id.nil?
50
50
  take_advisory_lock(lock) do
51
51
  Batch.within_thread(batch_id: nil, batch_callback_id: id) do
52
52
  reload
@@ -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>
@@ -0,0 +1,243 @@
1
+ ---
2
+ pt-BR:
3
+ good_job:
4
+ actions:
5
+ destroy: Excluir
6
+ discard: Descartar
7
+ force_discard: Forçar descarte
8
+ inspect: Inspecionar
9
+ reschedule: Reagendar
10
+ retry: Tentar novamente
11
+ batches:
12
+ index:
13
+ older_batches: Lotes antigos
14
+ pending_migrations: O GoodJob tem migrações pendentes no banco de dados.
15
+ title: Lotes
16
+ jobs:
17
+ actions:
18
+ confirm_destroy: Tem certeza de que deseja excluir esta tarefa?
19
+ confirm_discard: Tem certeza de que deseja descartar esta tarefa?
20
+ confirm_reschedule: Tem certeza de que deseja reagendar esta tarefa?
21
+ confirm_retry: Tem certeza de que deseja tentar novamente esta tarefa?
22
+ destroy: Excluir Tarefa
23
+ discard: Descartar Tarefa
24
+ reschedule: Reagendar Tarefa
25
+ retry: Tentar Tarefa Novamente
26
+ title: Ações
27
+ no_jobs_found: Nenhuma tarefa encontrada.
28
+ show:
29
+ attributes: Atributos
30
+ batched_jobs: Tarefas em Lote
31
+ callback_jobs: Tarefas de Callback
32
+ table:
33
+ no_batches_found: Nenhum lote encontrado.
34
+ cron_entries:
35
+ actions:
36
+ confirm_disable: Tem certeza de que deseja desativar esta tarefa programada?
37
+ confirm_enable: Tem certeza de que deseja ativar esta tarefa programada?
38
+ confirm_enqueue: Tem certeza de que deseja enfileirar esta tarefa programada agora?
39
+ disable: Desativar tarefa programada
40
+ enable: Ativar tarefa programada
41
+ enqueue: Enfileirar tarefa programada agora
42
+ disable:
43
+ notice: A tarefa programada foi desativada.
44
+ enable:
45
+ notice: A tarefa programada foi ativada.
46
+ enqueue:
47
+ notice: A tarefa programada foi enfileirada.
48
+ index:
49
+ no_cron_schedules_found: Nenhuma tarefa programada encontrada.
50
+ title: Tarefas Programadas
51
+ pending_migrations: Requer migração pendente do GoodJob no banco de dados.
52
+ show:
53
+ cron_entry_key: Chave da Tarefa Programada
54
+ datetime:
55
+ distance_in_words:
56
+ about_x_hours:
57
+ one: cerca de 1 hora
58
+ other: cerca de %{count} horas
59
+ about_x_months:
60
+ one: cerca de 1 mês
61
+ other: cerca de %{count} meses
62
+ about_x_years:
63
+ one: cerca de 1 ano
64
+ other: cerca de %{count} anos
65
+ almost_x_years:
66
+ one: quase 1 ano
67
+ other: quase %{count} anos
68
+ half_a_minute: meio minuto
69
+ less_than_x_minutes:
70
+ one: menos de um minuto
71
+ other: menos de %{count} minutos
72
+ less_than_x_seconds:
73
+ one: menos de 1 segundo
74
+ other: menos de %{count} segundos
75
+ over_x_years:
76
+ one: mais de 1 ano
77
+ other: mais de %{count} anos
78
+ x_days:
79
+ one: 1 dia
80
+ other: "%{count} dias"
81
+ x_minutes:
82
+ one: 1 minuto
83
+ other: "%{count} minutos"
84
+ x_months:
85
+ one: 1 mês
86
+ other: "%{count} meses"
87
+ x_seconds:
88
+ one: 1 segundo
89
+ other: "%{count} segundos"
90
+ x_years:
91
+ one: 1 ano
92
+ other: "%{count} anos"
93
+ duration:
94
+ hours: "%{hour}h %{min}m"
95
+ less_than_10_seconds: "%{sec}s"
96
+ milliseconds: "%{ms}ms"
97
+ minutes: "%{min}m %{sec}s"
98
+ seconds: "%{sec}s"
99
+ error_event:
100
+ discarded: Descartado
101
+ handled: Tratado
102
+ interrupted: Interrompido
103
+ retried: Tentado novamente
104
+ retry_stopped: Tentativa de reexecução interrompida
105
+ unhandled: Não tratado
106
+ helpers:
107
+ relative_time:
108
+ future: em %{time}
109
+ past: "%{time} atrás"
110
+ jobs:
111
+ actions:
112
+ confirm_destroy: Tem certeza de que deseja excluir a tarefa?
113
+ confirm_discard: Tem certeza de que deseja descartar a tarefa?
114
+ confirm_force_discard: 'Tem certeza de que deseja forçar o descarte desta tarefa? A tarefa será marcada como descartada, mas a tarefa em execução não será interrompida - no entanto, ela não será tentada novamente em falhas.
115
+
116
+ '
117
+ confirm_reschedule: Tem certeza de que deseja reagendar a tarefa?
118
+ confirm_retry: Tem certeza de que deseja tentar novamente a tarefa?
119
+ destroy: Excluir tarefa
120
+ discard: Descartar tarefa
121
+ force_discard: Forçar descarte de tarefa
122
+ reschedule: Reagendar tarefa
123
+ retry: Tentar tarefa novamente
124
+ destroy:
125
+ notice: A tarefa foi excluida.
126
+ discard:
127
+ notice: A tarefa foi descartada.
128
+ executions:
129
+ in_queue: na fila
130
+ runtime: tempo de execução
131
+ title: Execuções
132
+ force_discard:
133
+ notice: A tarefa foi forçadamente descartada. Ela continuará a ser executada, mas não será tentada novamente em caso de falhas.
134
+ index:
135
+ job_pagination: Paginação de Tarefa
136
+ older_jobs: Tarefas antigas
137
+ reschedule:
138
+ notice: A tarefa foi reagendada.
139
+ retry:
140
+ notice: A tarefa foi tentada novamente.
141
+ show:
142
+ jobs: Tarefas
143
+ table:
144
+ actions:
145
+ apply_to_all:
146
+ one: Aplicar a todas 1 tarefa.
147
+ other: Aplicar a todas %{count} tarefas.
148
+ confirm_destroy_all: Tem certeza de que deseja excluir as tarefas selecionadas?
149
+ confirm_discard_all: Tem certeza de que deseja descartar as tarefas selecionadas?
150
+ confirm_reschedule_all: Tem certeza de que deseja reagendar as tarefas selecionadas?
151
+ confirm_retry_all: Tem certeza de que deseja tentar novamente as tarefas selecionadas?
152
+ destroy_all: Excluir tudo
153
+ discard_all: Descartar tudo
154
+ reschedule_all: Reagendar tudo
155
+ retry_all: Tentar novamente tudo
156
+ title: Ações
157
+ no_jobs_found: Nenhuma tarefa encontrada.
158
+ toggle_actions: Alternar Ações
159
+ toggle_all_jobs: Alternar todas as tarefas
160
+ models:
161
+ batch:
162
+ created: Criado
163
+ created_at: Criado em
164
+ discarded: Descartado
165
+ discarded_at: Descartado em
166
+ enqueued: Enfileirado
167
+ enqueued_at: Enfileirado em
168
+ finished: Concluído
169
+ finished_at: Concluído em
170
+ jobs: Tarefas
171
+ name: Nome
172
+ cron:
173
+ class: Classe
174
+ last_run: Última execução
175
+ next_scheduled: Próximo agendamento
176
+ schedule: Agendamento
177
+ job:
178
+ arguments: Argumentos
179
+ attempts: Tentativas
180
+ priority: Prioridade
181
+ queue: Fila
182
+ number:
183
+ format:
184
+ delimiter: "."
185
+ separator: ","
186
+ human:
187
+ decimal_units:
188
+ delimiter: "."
189
+ format: "%n%u"
190
+ precision: 3
191
+ separator: ","
192
+ units:
193
+ billion: B
194
+ million: M
195
+ quadrillion: Q
196
+ thousand: K
197
+ trillion: T
198
+ unit: ''
199
+ processes:
200
+ index:
201
+ cron_enabled: Agendamento ativado
202
+ no_good_job_processes_found: Nenhum processo do GoodJob encontrado.
203
+ process: Processo
204
+ schedulers: Agendadores
205
+ started: Iniciado
206
+ title: Processos
207
+ updated: Atualizado
208
+ shared:
209
+ boolean:
210
+ 'false': Não
211
+ 'true': Sim
212
+ error: Erro
213
+ filter:
214
+ all: Tudo
215
+ all_jobs: Todas as tarefas
216
+ all_queues: Todas as filas
217
+ clear: Limpar
218
+ job_name: Nome da Tarefa
219
+ placeholder: Pesquisar por classe, ID da tarefa, parâmetros da tarefa e texto de erro.
220
+ queue_name: Nome da Fila
221
+ search: Pesquisar
222
+ navbar:
223
+ batches: Lotes
224
+ cron_schedules: Agendamentos
225
+ jobs: Tarefas
226
+ live_poll: Acompanhamento ao Vivo
227
+ name: "GoodJob 👍"
228
+ processes: Processos
229
+ theme:
230
+ auto: Automático
231
+ dark: Escuro
232
+ light: Claro
233
+ theme: Tema
234
+ secondary_navbar:
235
+ inspiration: Lembre-se, você também está fazendo uma Boa Tarefa!
236
+ last_updated: Última atualização
237
+ status:
238
+ discarded: Descartado
239
+ queued: Enfileirado
240
+ retried: Tentado novamente
241
+ running: Em execução
242
+ scheduled: Agendado
243
+ succeeded: Concluído com sucesso
@@ -96,6 +96,8 @@ module GoodJob
96
96
  # Generates the concurrency key from the configuration
97
97
  # @return [Object] concurrency key
98
98
  def _good_job_concurrency_key
99
+ return _good_job_default_concurrency_key unless self.class.good_job_concurrency_config.key?(:key)
100
+
99
101
  key = self.class.good_job_concurrency_config[:key]
100
102
  return if key.blank?
101
103
 
@@ -105,6 +107,12 @@ module GoodJob
105
107
  key
106
108
  end
107
109
 
110
+ # Generates the default concurrency key when the configuration doesn't provide one
111
+ # @return [String] concurrency key
112
+ def _good_job_default_concurrency_key
113
+ self.class.name.to_s
114
+ end
115
+
108
116
  private
109
117
 
110
118
  def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
@@ -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.23.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.23.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-01-23 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
@@ -347,6 +350,7 @@ files:
347
350
  - config/locales/ja.yml
348
351
  - config/locales/ko.yml
349
352
  - config/locales/nl.yml
353
+ - config/locales/pt-BR.yml
350
354
  - config/locales/ru.yml
351
355
  - config/locales/tr.yml
352
356
  - config/locales/uk.yml
@@ -374,7 +378,6 @@ files:
374
378
  - lib/good_job/active_job_extensions/labels.rb
375
379
  - lib/good_job/active_job_extensions/notify_options.rb
376
380
  - lib/good_job/adapter.rb
377
- - lib/good_job/assignable_connection.rb
378
381
  - lib/good_job/bulk.rb
379
382
  - lib/good_job/callable.rb
380
383
  - lib/good_job/capsule.rb
@@ -393,6 +396,7 @@ files:
393
396
  - lib/good_job/multi_scheduler.rb
394
397
  - lib/good_job/notifier.rb
395
398
  - lib/good_job/notifier/process_heartbeat.rb
399
+ - lib/good_job/overridable_connection.rb
396
400
  - lib/good_job/poller.rb
397
401
  - lib/good_job/probe_server.rb
398
402
  - lib/good_job/probe_server/healthcheck_middleware.rb
@@ -436,7 +440,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
436
440
  - !ruby/object:Gem::Version
437
441
  version: '0'
438
442
  requirements: []
439
- rubygems_version: 3.4.10
443
+ rubygems_version: 3.5.3
440
444
  signing_key:
441
445
  specification_version: 4
442
446
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails