good_job 3.26.2 → 3.27.1

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: cd8ee28330829da54e9a3aa54e26b346c5a36749cadbe957871e800d7cd896fd
4
- data.tar.gz: 42fd6406fafafff7a8635908db73c8a4159db1e86178551e1245d81c6485e943
3
+ metadata.gz: 6f6e4655250d166696f48733e756d69164f1c1b6371d925fa5333fd538c7850c
4
+ data.tar.gz: c65cd8ace06f23e8647ff6747eb754151a39a732a7ac9cb79534573016f20b87
5
5
  SHA512:
6
- metadata.gz: 1b63f85473716cc66d12ff3886c3968606c0e6eff11d43b9ea49524419ba6f55841d3662660169712a5aca1b7715c9c50a9927626bc3416d495634864082effd
7
- data.tar.gz: 83d2f99ca7aa3357af7625adbf05ab970b64fa9d6145d865ae045c737e7c8637e31b2da878e710db29feb2cf04a9dc63388663067bb096a6e2495e3d16b03367
6
+ metadata.gz: 1bb8b51381b2e61dcb024d15fd555f2cc36a5dbd1abb0439b20e8db182c332f136be433108a07cc0fdfcc7a2321035f1136676833a81387783694db5a9c6ea32
7
+ data.tar.gz: 1054014eb0dea19141fde93ef3a99b6533804f9f5714a97be43bb9bf57183297371df458f7ac55c0c77fd3bf7d8504d49c980d9ed5f5b94f378f1468356d6c68
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.27.1](https://github.com/bensheldon/good_job/tree/v3.27.1) (2024-03-24)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.27.0...v3.27.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Start async adapters `after_initialize` instead of once Active Job and Active Record are loaded and Rails initialized? [\#1297](https://github.com/bensheldon/good_job/pull/1297) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ ## [v3.27.0](https://github.com/bensheldon/good_job/tree/v3.27.0) (2024-03-24)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.26.2...v3.27.0)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - Add `enabled_by_default: false` as option for cron configuration [\#1289](https://github.com/bensheldon/good_job/pull/1289) ([bensheldon](https://github.com/bensheldon))
18
+ - Load metrics for job statuses asynchronously [\#1286](https://github.com/bensheldon/good_job/pull/1286) ([binarygit](https://github.com/binarygit))
19
+ - Implement throttling options in concurrency extension [\#1270](https://github.com/bensheldon/good_job/pull/1270) ([marckohlbrugge](https://github.com/marckohlbrugge))
20
+
21
+ **Fixed bugs:**
22
+
23
+ - fix\(ui-dropdown\): use dropdown-end on locales dropdown [\#1296](https://github.com/bensheldon/good_job/pull/1296) ([WailanTirajoh](https://github.com/WailanTirajoh))
24
+
25
+ **Closed issues:**
26
+
27
+ - Disabling probe [\#1290](https://github.com/bensheldon/good_job/issues/1290)
28
+ - Set an implicit order on models [\#1242](https://github.com/bensheldon/good_job/issues/1242)
29
+
30
+ **Merged pull requests:**
31
+
32
+ - docs\(readme\): remove double "using" [\#1295](https://github.com/bensheldon/good_job/pull/1295) ([WailanTirajoh](https://github.com/WailanTirajoh))
33
+ - Set an implicit order on models [\#1293](https://github.com/bensheldon/good_job/pull/1293) ([mec](https://github.com/mec))
34
+ - CI: install gems after loading cache, not before [\#1288](https://github.com/bensheldon/good_job/pull/1288) ([bensheldon](https://github.com/bensheldon))
35
+ - Ensure job execution Advisory Lock query uses bind parameters [\#1287](https://github.com/bensheldon/good_job/pull/1287) ([bensheldon](https://github.com/bensheldon))
36
+
3
37
  ## [v3.26.2](https://github.com/bensheldon/good_job/tree/v3.26.2) (2024-03-15)
4
38
 
5
39
  [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
@@ -30,7 +30,7 @@ module GoodJob
30
30
  GoodJob::Configuration.validate_execution_mode(@_execution_mode_override) if @_execution_mode_override
31
31
  @capsule = _capsule
32
32
 
33
- start_async if GoodJob.async_ready?
33
+ start_async if GoodJob._async_ready?
34
34
  self.class.instances << self
35
35
  end
36
36
 
@@ -49,22 +49,17 @@ module GoodJob
49
49
  end
50
50
 
51
51
  initializer "good_job.start_async" do
52
- # This hooks into the hookable places during Rails boot, which is unfortunately not Rails.application.initialized?
53
- # If an Adapter is initialized during boot, we want to want to start async executors once the framework dependencies have loaded.
54
- # When exactly that happens is out of our control because gems or application code may touch things earlier than expected.
55
- # For example, as of Rails 6.1, if an ActiveRecord model is touched during boot, that triggers ActiveRecord to load,
56
- # which touches DestroyAssociationAsyncJob, which loads ActiveJob, which may initialize a GoodJob::Adapter, all of which
57
- # happens _before_ ActiveRecord finishes loading. GoodJob will deadlock if an async executor is started in the middle of
58
- # ActiveRecord loading.
59
52
  config.after_initialize do
60
- ActiveSupport.on_load(:active_record) do
61
- ActiveSupport.on_load(:active_job) do
62
- GoodJob._framework_ready = true
63
- GoodJob._start_async_adapters
64
- end
65
- GoodJob._start_async_adapters
66
- end
67
- GoodJob._start_async_adapters
53
+ GoodJob._async_ready = true
54
+
55
+ # Ensure Active Record and Active Job are fully loaded
56
+ ActiveRecord::Base # rubocop:disable Lint/Void
57
+ ActiveJob::Base.queue_adapter
58
+
59
+ GoodJob::Adapter.instances
60
+ .select(&:execute_async?)
61
+ .reject(&:async_started?)
62
+ .each(&:start_async)
68
63
  end
69
64
  end
70
65
  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.1'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -25,7 +25,6 @@ require_relative "good_job/configuration"
25
25
  require_relative "good_job/cron_manager"
26
26
  require_relative "good_job/current_thread"
27
27
  require_relative "good_job/daemon"
28
- require_relative "good_job/dependencies"
29
28
  require_relative "good_job/job_performer"
30
29
  require_relative "good_job/job_performer/metrics"
31
30
  require_relative "good_job/log_subscriber"
@@ -46,7 +45,6 @@ require_relative "good_job/thread_status"
46
45
  #
47
46
  # +GoodJob+ is the top-level namespace and exposes configuration attributes.
48
47
  module GoodJob
49
- include GoodJob::Dependencies
50
48
  include GoodJob::ThreadStatus
51
49
 
52
50
  # Default, null, blank value placeholder.
@@ -114,6 +112,11 @@ module GoodJob
114
112
  # @return [GoodJob::Capsule, nil]
115
113
  mattr_accessor :capsule, default: GoodJob::Capsule.new(configuration: configuration)
116
114
 
115
+ mattr_accessor :_async_ready, default: false
116
+ def self._async_ready?
117
+ _async_ready
118
+ end
119
+
117
120
  # Called with exception when a GoodJob thread raises an exception
118
121
  # @param exception [Exception] Exception that was raised
119
122
  # @return [void]
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.1
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
@@ -390,7 +404,6 @@ files:
390
404
  - lib/good_job/cron_manager.rb
391
405
  - lib/good_job/current_thread.rb
392
406
  - lib/good_job/daemon.rb
393
- - lib/good_job/dependencies.rb
394
407
  - lib/good_job/engine.rb
395
408
  - lib/good_job/interrupt_error.rb
396
409
  - lib/good_job/job_performer.rb
@@ -444,7 +457,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
444
457
  - !ruby/object:Gem::Version
445
458
  version: '0'
446
459
  requirements: []
447
- rubygems_version: 3.5.3
460
+ rubygems_version: 3.5.4
448
461
  signing_key:
449
462
  specification_version: 4
450
463
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GoodJob # :nodoc:
4
- # Extends GoodJob module to track Rails boot dependencies.
5
- module Dependencies
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- mattr_accessor :_framework_ready, default: false
10
- end
11
-
12
- class_methods do
13
- # Whether Rails framework has sufficiently initialized to enable Async execution.
14
- def async_ready?
15
- Rails.application.initialized? || _framework_ready
16
- end
17
-
18
- def _start_async_adapters
19
- return unless async_ready?
20
-
21
- ActiveJob::Base.queue_adapter # Ensure Active Job is initialized
22
- GoodJob::Adapter.instances
23
- .select(&:execute_async?)
24
- .reject(&:async_started?)
25
- .each(&:start_async)
26
- end
27
- end
28
- end
29
- end