good_job 3.26.2 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd8ee28330829da54e9a3aa54e26b346c5a36749cadbe957871e800d7cd896fd
4
- data.tar.gz: 42fd6406fafafff7a8635908db73c8a4159db1e86178551e1245d81c6485e943
3
+ metadata.gz: 2b3817133850acc5176086af47b774bcb80a6ead4ee11e1121a10b23b5c7adb0
4
+ data.tar.gz: 0d8d3412835f37b23122a821d7f1ea1f20120a1e1893d29fadcd6e6b8697bce5
5
5
  SHA512:
6
- metadata.gz: 1b63f85473716cc66d12ff3886c3968606c0e6eff11d43b9ea49524419ba6f55841d3662660169712a5aca1b7715c9c50a9927626bc3416d495634864082effd
7
- data.tar.gz: 83d2f99ca7aa3357af7625adbf05ab970b64fa9d6145d865ae045c737e7c8637e31b2da878e710db29feb2cf04a9dc63388663067bb096a6e2495e3d16b03367
6
+ metadata.gz: 984697d136c831db59efdd76f8ca3fc820263de06bdcfe7650f685d75ee0ef64cc3e4c39aa2919b84273b15bb15689bc1fb47afce99a597035266c70372a7c65
7
+ data.tar.gz: f6f5942330d621fd89cc951a0f6731aa87578725f5959c9dfcf13c99252952e9c610385afcb03cc17a8bdeeb6a126d607e18c910b66353c01c941eb8e0a6c515
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.27.0](https://github.com/bensheldon/good_job/tree/v3.27.0) (2024-03-24)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.26.2...v3.27.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add `enabled_by_default: false` as option for cron configuration [\#1289](https://github.com/bensheldon/good_job/pull/1289) ([bensheldon](https://github.com/bensheldon))
10
+ - Load metrics for job statuses asynchronously [\#1286](https://github.com/bensheldon/good_job/pull/1286) ([binarygit](https://github.com/binarygit))
11
+ - Implement throttling options in concurrency extension [\#1270](https://github.com/bensheldon/good_job/pull/1270) ([marckohlbrugge](https://github.com/marckohlbrugge))
12
+
13
+ **Fixed bugs:**
14
+
15
+ - fix\(ui-dropdown\): use dropdown-end on locales dropdown [\#1296](https://github.com/bensheldon/good_job/pull/1296) ([WailanTirajoh](https://github.com/WailanTirajoh))
16
+
17
+ **Closed issues:**
18
+
19
+ - Disabling probe [\#1290](https://github.com/bensheldon/good_job/issues/1290)
20
+ - Set an implicit order on models [\#1242](https://github.com/bensheldon/good_job/issues/1242)
21
+
22
+ **Merged pull requests:**
23
+
24
+ - docs\(readme\): remove double "using" [\#1295](https://github.com/bensheldon/good_job/pull/1295) ([WailanTirajoh](https://github.com/WailanTirajoh))
25
+ - Set an implicit order on models [\#1293](https://github.com/bensheldon/good_job/pull/1293) ([mec](https://github.com/mec))
26
+ - CI: install gems after loading cache, not before [\#1288](https://github.com/bensheldon/good_job/pull/1288) ([bensheldon](https://github.com/bensheldon))
27
+ - Ensure job execution Advisory Lock query uses bind parameters [\#1287](https://github.com/bensheldon/good_job/pull/1287) ([bensheldon](https://github.com/bensheldon))
28
+
3
29
  ## [v3.26.2](https://github.com/bensheldon/good_job/tree/v3.26.2) (2024-03-15)
4
30
 
5
31
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.26.1...v3.26.2)
data/README.md CHANGED
@@ -127,7 +127,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
127
127
  - Because of Rails deferred autoloading, jobs enqueued via the `rails console` may not begin executing on a separate server process until the Rails application is fully initialized by loading a web page once.
128
128
  - Remember, only Active Job's `perform_later` sends jobs to the queue adapter; Active Job's `perform_now` executes the job immediately and does not invoke the queue adapter. GoodJob is not involved in `perform_now` jobs.
129
129
  1. **In Rails' test environment**, by default, GoodJob's Adapter executes jobs `inline` immediately in the current thread.
130
- - Future-scheduled jobs can be executed with `GoodJob.perform_inline` using using a tool like Timecop or `ActiveSupport::Testing::TimeHelpers`.
130
+ - Future-scheduled jobs can be executed with `GoodJob.perform_inline` using a tool like Timecop or `ActiveSupport::Testing::TimeHelpers`.
131
131
  - Note that Active Job's TestAdapter, which powers test helpers (e.g. `assert_enqueued_with`), may override GoodJob's Adapter in [some configurations](https://github.com/rails/rails/issues/37270).
132
132
  1. **In Rails' production environment**, by default, GoodJob's Adapter enqueues jobs in `external` mode to be executed by a separate execution process:
133
133
  - By default, GoodJob separates job enqueuing from job execution so that jobs can be scaled independently of the web server. Use the GoodJob command-line tool to execute jobs:
@@ -541,6 +541,16 @@ class MyJob < ApplicationJob
541
541
  # Can be an Integer or Lambda/Proc that is invoked in the context of the job
542
542
  perform_limit: 1,
543
543
 
544
+ # Maximum number of jobs with the concurrency key to be enqueued within
545
+ # the time period, looking backwards from the current time. Must be an array
546
+ # with two elements: the number of jobs and the time period.
547
+ enqueue_throttle: [10, 1.minute],
548
+
549
+ # Maximum number of jobs with the concurrency key to be performed within
550
+ # the time period, looking backwards from the current time. Must be an array
551
+ # with two elements: the number of jobs and the time period.
552
+ perform_throttle: [100, 1.hour],
553
+
544
554
  # Note: Under heavy load, the total number of jobs may exceed the
545
555
  # sum of `enqueue_limit` and `perform_limit` because of race conditions
546
556
  # caused by imperfectly disjunctive states. If you need to constrain
@@ -617,9 +627,10 @@ config.good_job.cron = {
617
627
  set: { priority: -10 }, # additional Active Job properties; can also be a lambda/proc e.g. `-> { { priority: [1,2].sample } }`
618
628
  description: "Something helpful", # optional description that appears in Dashboard
619
629
  },
620
- another_task: {
630
+ production_task: {
621
631
  cron: "0 0,12 * * *",
622
- class: "AnotherJob",
632
+ class: "ProductionJob",
633
+ enabled_by_default: -> { Rails.env.production? } # Only enable in production, otherwise can be enabled manually through Dashboard
623
634
  },
624
635
  complex_schedule: {
625
636
  class: "ComplexScheduleJob",
@@ -16,10 +16,20 @@ module GoodJob
16
16
  }
17
17
  end
18
18
 
19
+ def job_status
20
+ @filter = JobsFilter.new(params)
21
+
22
+ render json: @filter.states.transform_values { |count| number_with_delimiter(count) }
23
+ end
24
+
19
25
  private
20
26
 
21
27
  def number_to_human(count)
22
28
  helpers.number_to_human(count, **helpers.translate_hash("good_job.number.human.decimal_units"))
23
29
  end
30
+
31
+ def number_with_delimiter(count)
32
+ helpers.number_with_delimiter(count, **helpers.translate_hash('good_job.number.format'))
33
+ end
24
34
  end
25
35
  end
@@ -42,6 +42,10 @@ module GoodJob
42
42
  raise NotImplementedError
43
43
  end
44
44
 
45
+ def state_names
46
+ raise NotImplementedError
47
+ end
48
+
45
49
  def to_params(override = {})
46
50
  {
47
51
  job_class: params[:job_class],
@@ -2,6 +2,10 @@
2
2
 
3
3
  module GoodJob
4
4
  class JobsFilter < BaseFilter
5
+ def state_names
6
+ %w[scheduled retried queued running succeeded discarded]
7
+ end
8
+
5
9
  def states
6
10
  @_states ||= begin
7
11
  query = filtered_query(params.except(:state))
@@ -38,3 +38,7 @@
38
38
  .btn-outline-secondary {
39
39
  border-color: #ced4da; /* $gray-400 */
40
40
  }
41
+
42
+ .min-w-auto {
43
+ min-width: auto;
44
+ }
@@ -44,13 +44,8 @@ module GoodJob
44
44
  cte_table = Arel::Table.new(:rows)
45
45
  cte_query = original_query.select(primary_key, column).except(:limit)
46
46
  cte_query = cte_query.limit(select_limit) if select_limit
47
- cte_type = if supports_cte_materialization_specifiers?
48
- 'MATERIALIZED'
49
- else
50
- ''
51
- end
52
-
53
- composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::SqlLiteral.new([cte_type, "(", cte_query.to_sql, ")"].join(' ')))
47
+ cte_type = supports_cte_materialization_specifiers? ? :MATERIALIZED : :""
48
+ composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::UnaryOperation.new(cte_type, cte_query.arel))
54
49
  query = cte_table.project(cte_table[:id])
55
50
  .with(composed_cte)
56
51
  .where(Arel.sql("#{function}(('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"))
@@ -7,6 +7,7 @@ module GoodJob
7
7
  include AdvisoryLockable
8
8
 
9
9
  self.table_name = 'good_job_batches'
10
+ self.implicit_order_column = 'created_at'
10
11
 
11
12
  has_many :jobs, class_name: 'GoodJob::Job', inverse_of: :batch, foreign_key: :batch_id, dependent: nil
12
13
  has_many :executions, class_name: 'GoodJob::Execution', foreign_key: :batch_id, inverse_of: :batch, dependent: nil
@@ -74,7 +74,7 @@ module GoodJob # :nodoc:
74
74
  def enabled?
75
75
  return true unless GoodJob::Setting.migrated?
76
76
 
77
- GoodJob::Setting.cron_key_enabled?(key)
77
+ GoodJob::Setting.cron_key_enabled?(key, default: enabled_by_default?)
78
78
  end
79
79
 
80
80
  def enable
@@ -132,6 +132,11 @@ module GoodJob # :nodoc:
132
132
 
133
133
  private
134
134
 
135
+ def enabled_by_default?
136
+ value = params.fetch(:enabled_by_default, true)
137
+ value.respond_to?(:call) ? value.call : value
138
+ end
139
+
135
140
  def cron
136
141
  params.fetch(:cron)
137
142
  end
@@ -5,6 +5,7 @@ module GoodJob # :nodoc:
5
5
  include ErrorEvents
6
6
 
7
7
  self.table_name = 'good_job_executions'
8
+ self.implicit_order_column = 'created_at'
8
9
 
9
10
  belongs_to :execution, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
10
11
  belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
@@ -16,6 +16,7 @@ module GoodJob
16
16
 
17
17
  self.table_name = 'good_jobs'
18
18
  self.advisory_lockable_column = 'active_job_id'
19
+ self.implicit_order_column = 'created_at'
19
20
 
20
21
  define_model_callbacks :perform
21
22
  define_model_callbacks :perform_unlocked, only: :after
@@ -94,7 +95,7 @@ module GoodJob
94
95
  # @!method only_scheduled
95
96
  # @!scope class
96
97
  # @return [ActiveRecord::Relation]
97
- scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
98
+ scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Arel::Nodes::BindParam.new(ActiveModel::Attribute.with_cast_value("scheduled_at", Time.current, ActiveModel::Type::DateTime.new)))).or(where(scheduled_at: nil)) }
98
99
 
99
100
  # Order executions by priority (highest priority first).
100
101
  # @!method priority_ordered
@@ -26,6 +26,7 @@ module GoodJob
26
26
 
27
27
  self.primary_key = 'active_job_id'
28
28
  self.advisory_lockable_column = 'active_job_id'
29
+ self.implicit_order_column = 'created_at'
29
30
 
30
31
  belongs_to :batch, class_name: 'GoodJob::BatchRecord', inverse_of: :jobs, optional: true
31
32
  has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
@@ -14,6 +14,7 @@ module GoodJob # :nodoc:
14
14
  EXPIRED_INTERVAL = 5.minutes
15
15
 
16
16
  self.table_name = 'good_job_processes'
17
+ self.implicit_order_column = 'created_at'
17
18
 
18
19
  cattr_reader :mutex, default: Mutex.new
19
20
  cattr_accessor :_current_id, default: nil
@@ -2,29 +2,48 @@
2
2
 
3
3
  module GoodJob
4
4
  class Setting < BaseRecord
5
+ CRON_KEYS_ENABLED = "cron_keys_enabled"
5
6
  CRON_KEYS_DISABLED = "cron_keys_disabled"
6
7
 
7
8
  self.table_name = 'good_job_settings'
9
+ self.implicit_order_column = 'created_at'
8
10
 
9
- def self.cron_key_enabled?(key)
10
- cron_disabled = find_by(key: CRON_KEYS_DISABLED)&.value || []
11
- cron_disabled.exclude?(key.to_s)
11
+ def self.cron_key_enabled?(key, default: true)
12
+ if default
13
+ cron_disabled = find_by(key: CRON_KEYS_DISABLED)&.value || []
14
+ cron_disabled.exclude?(key.to_s)
15
+ else
16
+ cron_enabled = find_by(key: CRON_KEYS_ENABLED)&.value || []
17
+ cron_enabled.include?(key.to_s)
18
+ end
12
19
  end
13
20
 
14
21
  def self.cron_key_enable(key)
15
- setting = GoodJob::Setting.find_by(key: CRON_KEYS_DISABLED)
16
- return unless setting&.value&.include?(key.to_s)
22
+ enabled_setting = find_or_initialize_by(key: CRON_KEYS_ENABLED) do |record|
23
+ record.value = []
24
+ end
25
+ enabled_setting.value << key unless enabled_setting.value.include?(key)
26
+ enabled_setting.save!
17
27
 
18
- setting.value.delete(key.to_s)
19
- setting.save!
28
+ disabled_setting = GoodJob::Setting.find_by(key: CRON_KEYS_DISABLED)
29
+ return unless disabled_setting&.value&.include?(key.to_s)
30
+
31
+ disabled_setting.value.delete(key.to_s)
32
+ disabled_setting.save!
20
33
  end
21
34
 
22
35
  def self.cron_key_disable(key)
23
- setting = find_or_initialize_by(key: CRON_KEYS_DISABLED) do |record|
36
+ enabled_setting = GoodJob::Setting.find_by(key: CRON_KEYS_ENABLED)
37
+ if enabled_setting&.value&.include?(key.to_s)
38
+ enabled_setting.value.delete(key.to_s)
39
+ enabled_setting.save!
40
+ end
41
+
42
+ disabled_setting = find_or_initialize_by(key: CRON_KEYS_DISABLED) do |record|
24
43
  record.value = []
25
44
  end
26
- setting.value << key
27
- setting.save!
45
+ disabled_setting.value << key unless disabled_setting.value.include?(key)
46
+ disabled_setting.save!
28
47
  end
29
48
  end
30
49
  end
@@ -47,16 +47,16 @@
47
47
  </div>
48
48
  <% end %>
49
49
 
50
- <ul data-live-poll-region="filter-tabs" class="nav nav-tabs my-3">
50
+ <ul data-controller="async-values" data-async-values-url-value="<%= metrics_job_status_path %>" data-live-poll-region="filter-tabs" class="nav nav-tabs my-3">
51
51
  <li class="nav-item">
52
52
  <%= link_to t(".all"), filter.to_params(state: nil), class: "nav-link #{"active" unless params[:state].present?}" %>
53
53
  </li>
54
54
 
55
- <% filter.states.each do |name, count| %>
55
+ <% filter.state_names.each do |name| %>
56
56
  <li class="nav-item">
57
57
  <%= link_to filter.to_params(state: name), class: "nav-link #{"active" if params[:state] == name}" do %>
58
- <%= t(name, scope: 'good_job.status', count: count) %>
59
- <span class="badge bg-primary rounded-pill <%= "bg-secondary" if count == 0 %>"><%= number_with_delimiter(count, t('good_job.number.format')) %></span>
58
+ <%= t(name, scope: 'good_job.status') %>
59
+ <span data-async-values-target="value", data-async-values-key="<%= name %>" data-async-values-zero-class="bg-secondary" class="badge bg-primary rounded-pill d-none"></span>
60
60
  <% end %>
61
61
  </li>
62
62
  <% end %>
@@ -73,7 +73,7 @@
73
73
  <%= I18n.locale %>
74
74
  </a>
75
75
 
76
- <ul class="dropdown-menu" aria-labelledby="localeOptions">
76
+ <ul class="dropdown-menu dropdown-menu-end min-w-auto" aria-labelledby="localeOptions">
77
77
  <% possible_locales = I18n.available_locales %>
78
78
  <% possible_locales.reject { |locale| locale == I18n.locale }.each do |locale| %>
79
79
  <li><%= link_to locale, url_for(locale: locale), class: "dropdown-item" %></li>
@@ -34,7 +34,7 @@ de:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: Möchten Sie diesen Cron-Eintrag wirklich deaktivieren?
37
- confirm_enable: Möchten Sie diesen Cron-Eintrag wirklich bestätigen?
37
+ confirm_enable: Möchten Sie diesen Cron-Eintrag wirklich aktivieren?
38
38
  confirm_enqueue: Möchten Sie diesen Cron-Eintrag wirklich in die Warteschlange stellen?
39
39
  disable: Cron-Eintrag deaktivieren
40
40
  enable: Cron-Eintrag aktivieren
@@ -34,7 +34,7 @@ en:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: Are you sure you want to disable this cron entry?
37
- confirm_enable: Are you sure you want to confirm this cron entry?
37
+ confirm_enable: Are you sure you want to enable this cron entry?
38
38
  confirm_enqueue: Are you sure you want to enqueue this cron entry?
39
39
  disable: Disable cron entry
40
40
  enable: Enable cron entry
@@ -34,7 +34,7 @@ es:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: "¿Estás seguro que querés deshabilitar esta tarea programada?"
37
- confirm_enable: "¿Estás seguro que querés confirmar esta tarea programada?"
37
+ confirm_enable: "¿Estás seguro que querés habilitar esta tarea programada?"
38
38
  confirm_enqueue: "¿Estás seguro que querés encolar esta tarea programada?"
39
39
  disable: Deshabilitar tarea programada
40
40
  enable: Habilitar tarea programada
@@ -33,9 +33,9 @@ fr:
33
33
  no_batches_found: Aucun lot trouvé.
34
34
  cron_entries:
35
35
  actions:
36
- confirm_disable: Voulez-vous vraiment désactiver cette entrée cron ?
37
- confirm_enable: Voulez-vous vraiment activer cette entrée cron ?
38
- confirm_enqueue: Voulez-vous vraiment mettre en file d'attente cette entrée cron ?
36
+ confirm_disable: Voulez-vous vraiment désactiver cette entrée cron?
37
+ confirm_enable: Voulez-vous vraiment activer cette entrée cron?
38
+ confirm_enqueue: Voulez-vous vraiment mettre en file d'attente cette entrée cron?
39
39
  disable: Désactiver l'entrée cron
40
40
  enable: Activer l'entrée cron
41
41
  enqueue: Mettre en file d'attente l'entrée cron maintenant
@@ -34,7 +34,7 @@ it:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: Sei sicuro di voler disabilitare questa voce cron?
37
- confirm_enable: Sei sicuro di voler confermare questa voce cron?
37
+ confirm_enable: Sei sicuro di voler abilita questa voce cron?
38
38
  confirm_enqueue: Sei sicuro di voler mettere in coda questa voce cron?
39
39
  disable: Disabilita voce cron
40
40
  enable: Abilita voce cron
@@ -34,7 +34,7 @@ nl:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: Weet u zeker dat u deze cron-vermelding wilt uitschakelen?
37
- confirm_enable: Weet u zeker dat u deze cron-invoer wilt bevestigen?
37
+ confirm_enable: Weet u zeker dat u deze cron-invoer wilt inschakelen?
38
38
  confirm_enqueue: Weet u zeker dat u deze cron-vermelding in de wachtrij wilt plaatsen?
39
39
  disable: Schakel cron-invoer uit
40
40
  enable: Schakel cron-invoer in
@@ -34,7 +34,7 @@ ru:
34
34
  cron_entries:
35
35
  actions:
36
36
  confirm_disable: Вы уверены, что хотите отключить эту задачу cron?
37
- confirm_enable: Вы уверены, что хотите подтвердить эту задачу cron?
37
+ confirm_enable: Вы уверены, что хотите включить это задание cron?
38
38
  confirm_enqueue: Вы уверены, что хотите поставить эту задачу cron в очередь?
39
39
  disable: Отключить задачу cron
40
40
  enable: Включить задачу cron
data/config/routes.rb CHANGED
@@ -17,6 +17,7 @@ GoodJob::Engine.routes.draw do
17
17
  end
18
18
  end
19
19
  get 'jobs/metrics/primary_nav', to: 'metrics#primary_nav', as: :metrics_primary_nav
20
+ get 'jobs/metrics/job_status', to: 'metrics#job_status', as: :metrics_job_status
20
21
 
21
22
  resources :batches, only: %i[index show]
22
23
 
@@ -13,6 +13,8 @@ module GoodJob
13
13
  end
14
14
  end
15
15
 
16
+ ThrottleExceededError = Class.new(ConcurrencyExceededError)
17
+
16
18
  module Prepends
17
19
  def deserialize(job_data)
18
20
  super
@@ -62,8 +64,13 @@ module GoodJob
62
64
  total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
63
65
  end
64
66
 
67
+ perform_throttle = job.class.good_job_concurrency_config[:perform_throttle]
68
+ perform_throttle = instance_exec(&perform_throttle) if perform_throttle.respond_to?(:call)
69
+ perform_throttle = nil unless GoodJob::DiscreteExecution.migrated? && perform_throttle.present? && perform_throttle.is_a?(Array) && perform_throttle.size == 2
70
+
65
71
  limit = perform_limit || total_limit
66
- next unless limit
72
+ throttle = perform_throttle
73
+ next unless limit || throttle
67
74
 
68
75
  key = job.good_job_concurrency_key
69
76
  next if key.blank?
@@ -74,9 +81,29 @@ module GoodJob
74
81
  end
75
82
 
76
83
  GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
77
- allowed_active_job_ids = GoodJob::Execution.unfinished.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(limit).pluck(:active_job_id)
78
- # The current job has already been locked and will appear in the previous query
79
- raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
84
+ if limit
85
+ allowed_active_job_ids = GoodJob::Execution.unfinished.where(concurrency_key: key)
86
+ .advisory_locked
87
+ .order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC"))
88
+ .limit(limit).pluck(:active_job_id)
89
+ # The current job has already been locked and will appear in the previous query
90
+ raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include?(job.job_id)
91
+ end
92
+
93
+ if throttle
94
+ throttle_limit = throttle[0]
95
+ throttle_period = throttle[1]
96
+
97
+ query = DiscreteExecution.joins(:job)
98
+ .where(GoodJob::Job.table_name => { concurrency_key: key })
99
+ .where(DiscreteExecution.arel_table[:created_at].gt(throttle_period.ago))
100
+ allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
101
+ .order(created_at: :asc)
102
+ .limit(throttle_limit)
103
+ .pluck(:active_job_id)
104
+
105
+ raise ThrottleExceededError unless allowed_active_job_ids.include?(job.job_id)
106
+ end
80
107
  end
81
108
  end
82
109
  end
@@ -137,23 +164,45 @@ module GoodJob
137
164
  total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
138
165
  end
139
166
 
167
+ enqueue_throttle = job.class.good_job_concurrency_config[:enqueue_throttle]
168
+ enqueue_throttle = instance_exec(&enqueue_throttle) if enqueue_throttle.respond_to?(:call)
169
+ enqueue_throttle = nil unless enqueue_throttle.present? && enqueue_throttle.is_a?(Array) && enqueue_throttle.size == 2
170
+
140
171
  limit = enqueue_limit || total_limit
141
- return on_enqueue&.call unless limit
172
+ throttle = enqueue_throttle
173
+ return on_enqueue&.call unless limit || throttle
142
174
 
143
175
  GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
144
- enqueue_concurrency = if enqueue_limit
145
- GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
146
- else
147
- GoodJob::Execution.where(concurrency_key: key).unfinished.count
148
- end
149
-
150
- # The job has not yet been enqueued, so check if adding it will go over the limit
151
- if (enqueue_concurrency + 1) > limit
152
- logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its limit of #{limit} #{'job'.pluralize(limit)}"
153
- on_abort&.call
154
- else
155
- on_enqueue&.call
176
+ if limit
177
+ enqueue_concurrency = if enqueue_limit
178
+ GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
179
+ else
180
+ GoodJob::Execution.where(concurrency_key: key).unfinished.count
181
+ end
182
+
183
+ # The job has not yet been enqueued, so check if adding it will go over the limit
184
+ if (enqueue_concurrency + 1) > limit
185
+ logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
186
+ on_abort&.call
187
+ break
188
+ end
156
189
  end
190
+
191
+ if throttle
192
+ throttle_limit = throttle[0]
193
+ throttle_period = throttle[1]
194
+ enqueued_within_period = GoodJob::Job.where(concurrency_key: key)
195
+ .where(GoodJob::Job.arel_table[:created_at].gt(throttle_period.ago))
196
+ .count
197
+
198
+ if (enqueued_within_period + 1) > throttle_limit
199
+ logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
200
+ on_abort&.call
201
+ break
202
+ end
203
+ end
204
+
205
+ on_enqueue&.call
157
206
  end
158
207
  end
159
208
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '3.26.2'
5
+ VERSION = '3.27.0'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
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.26.2
4
+ version: 3.27.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-03-15 00:00:00.000000000 Z
11
+ date: 2024-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -206,6 +206,20 @@ dependencies:
206
206
  - - ">="
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: timecop
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
209
223
  - !ruby/object:Gem::Dependency
210
224
  name: webrick
211
225
  requirement: !ruby/object:Gem::Requirement
@@ -444,7 +458,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
444
458
  - !ruby/object:Gem::Version
445
459
  version: '0'
446
460
  requirements: []
447
- rubygems_version: 3.5.3
461
+ rubygems_version: 3.5.4
448
462
  signing_key:
449
463
  specification_version: 4
450
464
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails