good_job 4.9.0 → 4.9.2

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: e647f9e6432f6972951bdf902f2de80d9cc0001ee37c9bb355fedbfaae2c0dd6
4
- data.tar.gz: 432488ed94165af462f1702aa08cdf26b771fa2815ba5f345d7acc48fa040a42
3
+ metadata.gz: eebb74f90c823f1b2dd3dbeeecffc6279b072d5929182c7734357ce98ebd3a1a
4
+ data.tar.gz: f0df3c322938cb34d9a5686a344db8e412a2878473ff778928006855ab8339a1
5
5
  SHA512:
6
- metadata.gz: 68824597423a7149136168819e25d75f2ae427a574188352a793a0e8405eda879e032c4cfde00decab70e509c353dd3ea89752619099d1f4ddc80771ef3a7035
7
- data.tar.gz: 90c3b1c09665ca5c2cbb3dcda3067949e69e02edb68dfea4ffb42c3a128a9603bde4ffb8ee9082fddab98651b9eea0c7096af71f5653c9af1588df7377e05d6b
6
+ metadata.gz: a91aae2a4768af8c89c8b0e2b04898d70dbe145b8808f2e1cc60fd18dd63faafd3679ca7444abd9e6863b7991b778ee709ba8949ac791362ff850c7aca3dcb5c
7
+ data.tar.gz: e3df10dbd134f6e335938578c0050e838d2b0073a6a3d7f64a90313e70afd214e76307df146be840dc8fecfcfca32bcb4ea9671f7549573c2e0e32adb8006a7a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.9.2](https://github.com/bensheldon/good_job/tree/v4.9.2) (2025-03-09)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.9.1...v4.9.2)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Quiet duplicate cron enqueue logging by pretending it was halted at before\_enqueue [\#1615](https://github.com/bensheldon/good_job/pull/1615) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ ## [v4.9.1](https://github.com/bensheldon/good_job/tree/v4.9.1) (2025-03-09)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.9.0...v4.9.1)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - Order Dashboard jobs in more "natural" order [\#1604](https://github.com/bensheldon/good_job/pull/1604) ([francois](https://github.com/francois))
18
+
19
+ **Fixed bugs:**
20
+
21
+ - \[dashboard\] Scheduled tasks are shown "backwards" [\#1580](https://github.com/bensheldon/good_job/issues/1580)
22
+ - Update `form_with` calls to be compatible with Rails 8 [\#1610](https://github.com/bensheldon/good_job/pull/1610) ([sallyhall](https://github.com/sallyhall))
23
+ - Add index on good\_jobs: \[:concurrency\_key, :created\_at\] to improve performance of throttling \(\#1603\) [\#1605](https://github.com/bensheldon/good_job/pull/1605) ([Intrepidd](https://github.com/Intrepidd))
24
+
25
+ **Closed issues:**
26
+
27
+ - Cron did not enqueue jobs [\#1600](https://github.com/bensheldon/good_job/issues/1600)
28
+ - Same job performed by all threads [\#1599](https://github.com/bensheldon/good_job/issues/1599)
29
+ - Option for bin/rails g good\_job:install to purge Solid gems [\#1593](https://github.com/bensheldon/good_job/issues/1593)
30
+ - Jobs are not being picked up at the expected rate [\#1578](https://github.com/bensheldon/good_job/issues/1578)
31
+ - Question about GoodJob Batches in tests [\#1479](https://github.com/bensheldon/good_job/issues/1479)
32
+
3
33
  ## [v4.9.0](https://github.com/bensheldon/good_job/tree/v4.9.0) (2025-02-07)
4
34
 
5
35
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.8.2...v4.9.0)
data/README.md CHANGED
@@ -1032,7 +1032,7 @@ To detect the start of a graceful shutdown from within a performing job, for exa
1032
1032
  ```ruby
1033
1033
  def perform(lots_of_records)
1034
1034
  lots_of_records.each do |record|
1035
- break if GoodJob.current_thread_shutting_down? # or `unless GoodJob.current_thread.running?`
1035
+ break if GoodJob.current_thread_shutting_down? # or `unless GoodJob.current_thread_running?`
1036
1036
  # process record ...
1037
1037
  end
1038
1038
  end
@@ -13,12 +13,15 @@ module GoodJob
13
13
  end
14
14
 
15
15
  def records
16
- after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
16
+ after_at = params[:after_at].present? ? Time.zone.parse(params[:after_at]) : nil
17
+ after_id = params[:after_id] if after_at
18
+ limit = params.fetch(:limit, DEFAULT_LIMIT)
17
19
 
18
20
  query_for_records.display_all(
19
- after_scheduled_at: after_scheduled_at,
20
- after_id: params[:after_id]
21
- ).limit(params.fetch(:limit, DEFAULT_LIMIT))
21
+ ordered_by: ordered_by,
22
+ after_at: after_at,
23
+ after_id: after_id
24
+ ).limit(limit)
22
25
  end
23
26
 
24
27
  def last
@@ -66,6 +69,19 @@ module GoodJob
66
69
  filtered_query.count
67
70
  end
68
71
 
72
+ def ordered_by
73
+ %w[created_at desc]
74
+ end
75
+
76
+ def next_page_params
77
+ order_column = ordered_by.first
78
+
79
+ {
80
+ after_at: records.last&.send(order_column),
81
+ after_id: records.last&.id,
82
+ }.merge(to_params)
83
+ end
84
+
69
85
  private
70
86
 
71
87
  def query_for_records
@@ -2,19 +2,14 @@
2
2
 
3
3
  module GoodJob
4
4
  class BatchesFilter < BaseFilter
5
- def records
6
- after_created_at = params[:after_created_at].present? ? Time.zone.parse(params[:after_created_at]) : nil
7
-
8
- filtered_query.display_all(
9
- after_created_at: after_created_at,
10
- after_id: params[:after_id]
11
- ).limit(params.fetch(:limit, DEFAULT_LIMIT))
12
- end
13
-
14
5
  def filtered_query(_filtered_params = params)
15
6
  base_query
16
7
  end
17
8
 
9
+ def query_for_records
10
+ default_base_query
11
+ end
12
+
18
13
  def default_base_query
19
14
  GoodJob::BatchRecord.includes(:jobs)
20
15
  end
@@ -53,6 +53,19 @@ module GoodJob
53
53
  @_filtered_count ||= filtered_query.unscope(:select).count
54
54
  end
55
55
 
56
+ def ordered_by
57
+ case params[:state]
58
+ when "scheduled", "retried", "pending", "queued"
59
+ %w[scheduled_at asc]
60
+ when "running"
61
+ %w[performed_at desc]
62
+ when "finished", "discarded"
63
+ %w[finished_at desc]
64
+ else
65
+ %w[created_at desc]
66
+ end
67
+ end
68
+
56
69
  private
57
70
 
58
71
  def query_for_records
@@ -7,21 +7,27 @@ module GoodJob
7
7
 
8
8
  included do
9
9
  # Get records in display order with optional keyset pagination.
10
- # @!method display_all(after_scheduled_at: nil, after_id: nil)
10
+ # @!method display_all(ordered_by: ["created_at", "desc"], after_at: nil, after_id: nil)
11
11
  # @!scope class
12
- # @param after_scheduled_at [DateTime, String, nil]
13
- # Display records scheduled after this time for keyset pagination
12
+ # @param ordered_by [Array<String>]
13
+ # Order to display records, from Filter#ordered_by
14
+ # @param after_at [DateTime, String, nil]
15
+ # Display records after this time for keyset pagination
14
16
  # @param after_id [Numeric, String, nil]
15
17
  # Display records after this ID for keyset pagination
16
18
  # @return [ActiveRecord::Relation]
17
- scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
18
- query = order(Arel.sql('scheduled_at DESC, id DESC'))
19
- if after_scheduled_at.present? && after_id.present?
20
- query = query.where Arel::Nodes::Grouping.new([arel_table["scheduled_at"], arel_table["id"]]).lt(Arel::Nodes::Grouping.new([bind_value('scheduled_at', after_scheduled_at, ActiveRecord::Type::DateTime), bind_value('id', after_id, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid)]))
21
- elsif after_scheduled_at.present?
22
- query = query.where arel_table["scheduled_at"].lt(bind_value('scheduled_at', after_scheduled_at, ActiveRecord::Type::DateTime))
19
+ scope :display_all, (lambda do |ordered_by: %w[created_at desc], after_at: nil, after_id: nil|
20
+ order_column, order_direction = ordered_by
21
+ query = self
22
+
23
+ if after_at.present? && after_id.present?
24
+ query = query.where Arel::Nodes::Grouping.new([arel_table[order_column], arel_table[primary_key]]).send(
25
+ order_direction == 'asc' ? :gteq : :lt,
26
+ Arel::Nodes::Grouping.new([bind_value(order_column, after_at, ActiveRecord::Type::DateTime), bind_value(primary_key, after_id, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid)])
27
+ )
23
28
  end
24
- query
29
+
30
+ query.order Arel.sql("#{order_column} #{order_direction}, #{primary_key} #{order_direction}")
25
31
  end)
26
32
 
27
33
  # Search records by text query.
@@ -5,6 +5,7 @@ require 'active_job/arguments'
5
5
  module GoodJob
6
6
  class BatchRecord < BaseRecord
7
7
  include AdvisoryLockable
8
+ include Filterable
8
9
 
9
10
  self.table_name = 'good_job_batches'
10
11
  self.implicit_order_column = 'created_at'
@@ -24,20 +25,6 @@ module GoodJob
24
25
  alias_attribute :discarded?, :discarded_at
25
26
  alias_attribute :finished?, :finished_at
26
27
 
27
- scope :display_all, (lambda do |after_created_at: nil, after_id: nil|
28
- query = order(created_at: :desc, id: :desc)
29
- if after_created_at.present? && after_id.present?
30
- query = if Gem::Version.new(Rails.version) < Gem::Version.new('7.0.0.a') || Concurrent.on_jruby?
31
- query.where(Arel.sql('(created_at, id) < (:after_created_at, :after_id)'), after_created_at: after_created_at, after_id: after_id)
32
- else
33
- query.where Arel::Nodes::Grouping.new([arel_table["created_at"], arel_table["id"]]).lt(Arel::Nodes::Grouping.new([bind_value('created_at', after_created_at, ActiveRecord::Type::DateTime), bind_value('id', after_id, ActiveRecord::Type::String)]))
34
- end
35
- elsif after_created_at.present?
36
- query = query.where arel_table["created_at"].lt(bind_value('created_at', after_created_at, ActiveRecord::Type::DateTime))
37
- end
38
- query
39
- end)
40
-
41
28
  def self.jobs_finished_at_migrated?
42
29
  column_names.include?('jobs_finished_at')
43
30
  end
@@ -110,8 +110,6 @@ module GoodJob # :nodoc:
110
110
  kwargs_value.present? ? configured_job.perform_later(*args_value, **kwargs_value) : configured_job.perform_later(*args_value)
111
111
  end
112
112
  end
113
- rescue ActiveRecord::RecordNotUnique
114
- false
115
113
  end
116
114
 
117
115
  def display_properties
@@ -387,7 +387,16 @@ module GoodJob
387
387
  end
388
388
 
389
389
  instrument_payload[:job] = job
390
- job.save!
390
+ begin
391
+ job.save!
392
+ rescue ActiveRecord::RecordNotUnique
393
+ raise unless job.cron_key
394
+
395
+ # Active Job doesn't have a clean way to cancel an enqueue for unexceptional reasons
396
+ # This is a workaround to mark it as having been halted in before_enqueue
397
+ active_job.send(:halted_callback_hook, "duplicate_cron_key", "good_job")
398
+ return false
399
+ end
391
400
 
392
401
  CurrentThread.retried_job = job if retried
393
402
 
@@ -7,7 +7,7 @@
7
7
  <nav aria-label="Batch pagination" class="mt-3">
8
8
  <ul class="pagination">
9
9
  <li class="page-item">
10
- <%= link_to(@filter.to_params(after_created_at: @filter.last.created_at, after_id: @filter.last.id), class: "page-link") do %>
10
+ <%= link_to(@filter.next_page_params, class: "page-link") do %>
11
11
  <%= t ".older_batches" %> <span aria-hidden="true">&raquo;</span>
12
12
  <% end %>
13
13
  </li>
@@ -1,4 +1,4 @@
1
- <%= form_with(url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
1
+ <%= form_with(model: false, url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
2
2
  <div class="my-3 card" data-gj-poll-replace id="jobs-table">
3
3
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
4
4
  <header class="list-group-item bg-body-tertiary">
@@ -7,7 +7,7 @@
7
7
  <nav aria-label="<%= t ".job_pagination" %>" class="mt-3">
8
8
  <ul class="pagination">
9
9
  <li class="page-item">
10
- <%= link_to(@filter.to_params(after_scheduled_at: @filter.last.scheduled_at || @filter.last.created_at, after_id: @filter.last.id), class: "page-link") do %>
10
+ <%= link_to(@filter.next_page_params, class: "page-link") do %>
11
11
  <%= t ".older_jobs" %> <span aria-hidden="true">&raquo;</span>
12
12
  <% end %>
13
13
  </li>
@@ -4,7 +4,7 @@
4
4
  <h2 class="pt-3 pb-2"><%= title %></h2>
5
5
  </div>
6
6
 
7
- <%= form_with(url: "", method: :get, local: true, id: "filter_form", class: "") do |form| %>
7
+ <%= form_with(model: false, url: "", method: :get, local: true, id: "filter_form", class: "") do |form| %>
8
8
  <%= hidden_field_tag :poll, params[:poll] %>
9
9
  <%= hidden_field_tag :state, params[:state] %>
10
10
  <%= hidden_field_tag :locale, params[:locale] if params[:locale] %>
@@ -82,6 +82,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
82
82
  add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)", name: :index_good_jobs_on_queue_name_and_scheduled_at
83
83
  add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at
84
84
  add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished
85
+ add_index :good_jobs, [:concurrency_key, :created_at], name: :index_good_jobs_on_concurrency_key_and_created_at
85
86
  add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)", name: :index_good_jobs_on_cron_key_and_created_at_cond
86
87
  add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true, name: :index_good_jobs_on_cron_key_and_cron_at_cond
87
88
  add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddIndexGoodJobsConcurrencyKeyCreatedAt < ActiveRecord::Migration<%= migration_version %>
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ # Ensure this incremental update migration is idempotent
10
+ # with monolithic install migration.
11
+ return if connection.index_exists? :good_jobs, [:concurrency_key, :created_at]
12
+ end
13
+ end
14
+
15
+ add_index :good_jobs, [:concurrency_key, :created_at], algorithm: :concurrently
16
+ end
17
+ end
@@ -163,8 +163,10 @@ module GoodJob
163
163
  scheduled_at: scheduled_at
164
164
  )
165
165
 
166
- executed_locally = execute_async? && @capsule&.create_thread(job.job_state)
167
- Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
166
+ if job
167
+ executed_locally = execute_async? && @capsule&.create_thread(job.job_state)
168
+ Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
169
+ end
168
170
  end
169
171
 
170
172
  job
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.9.0'
5
+ VERSION = '4.9.2'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.9.0
4
+ version: 4.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-07 00:00:00.000000000 Z
10
+ date: 2025-03-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activejob
@@ -348,6 +348,7 @@ files:
348
348
  - lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb
349
349
  - lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb
350
350
  - lib/generators/good_job/templates/update/migrations/02_add_jobs_finished_at_to_good_job_batches.rb.erb
351
+ - lib/generators/good_job/templates/update/migrations/03_add_index_good_jobs_concurrency_key_created_at.rb.erb
351
352
  - lib/generators/good_job/update_generator.rb
352
353
  - lib/good_job.rb
353
354
  - lib/good_job/active_job_extensions/batches.rb