good_job 3.26.1 → 3.27.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: eedb4c5defcfa2e25d62147f5021bb868ee0a93b74ae680c46ea4e852212d0dc
4
- data.tar.gz: d0124daaa3d93e7ace5b0764031c44cc06b03559286ebf1e5a36936e414bb50d
3
+ metadata.gz: 2b3817133850acc5176086af47b774bcb80a6ead4ee11e1121a10b23b5c7adb0
4
+ data.tar.gz: 0d8d3412835f37b23122a821d7f1ea1f20120a1e1893d29fadcd6e6b8697bce5
5
5
  SHA512:
6
- metadata.gz: 15c9fbd5228a60caa7a1d13ec9e6165c1a78ea488a9e7dd992cf60339dfb9523ca8cc68efa0588dddf16f491bf065a878837d943355a7bcae8ba80ec998615d4
7
- data.tar.gz: aea808fb1eb1ce99c9de4b15c429dc8128d7eee29445a1ca5172498d9f919f8596f28416e2967c41d4bcb6e389e098db90f865e9dce1870229131aaa28212930
6
+ metadata.gz: 984697d136c831db59efdd76f8ca3fc820263de06bdcfe7650f685d75ee0ef64cc3e4c39aa2919b84273b15bb15689bc1fb47afce99a597035266c70372a7c65
7
+ data.tar.gz: f6f5942330d621fd89cc951a0f6731aa87578725f5959c9dfcf13c99252952e9c610385afcb03cc17a8bdeeb6a126d607e18c910b66353c01c941eb8e0a6c515
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
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
+
29
+ ## [v3.26.2](https://github.com/bensheldon/good_job/tree/v3.26.2) (2024-03-15)
30
+
31
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.26.1...v3.26.2)
32
+
33
+ **Closed issues:**
34
+
35
+ - Async mode blocks ActionDispatch::Reloader [\#1274](https://github.com/bensheldon/good_job/issues/1274)
36
+
37
+ **Merged pull requests:**
38
+
39
+ - Update dependencies and their Sorbet / Tapioca files [\#1284](https://github.com/bensheldon/good_job/pull/1284) ([bensheldon](https://github.com/bensheldon))
40
+ - Use require\_relative and do not modify $LOAD\_PATH in gemspec [\#1283](https://github.com/bensheldon/good_job/pull/1283) ([bensheldon](https://github.com/bensheldon))
41
+ - Tweak rbtrace script [\#1279](https://github.com/bensheldon/good_job/pull/1279) ([bensheldon](https://github.com/bensheldon))
42
+ - Fix for Rails head: Don't try to override connection on connection checkin in tests [\#1277](https://github.com/bensheldon/good_job/pull/1277) ([bensheldon](https://github.com/bensheldon))
43
+ - Tiny improvements to french translation [\#1273](https://github.com/bensheldon/good_job/pull/1273) ([francois-ferrandis](https://github.com/francois-ferrandis))
44
+ - Load metrics for top nav asynchronously [\#1231](https://github.com/bensheldon/good_job/pull/1231) ([binarygit](https://github.com/binarygit))
45
+
3
46
  ## [v3.26.1](https://github.com/bensheldon/good_job/tree/v3.26.1) (2024-03-01)
4
47
 
5
48
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.26.0...v3.26.1)
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",
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ class MetricsController < ApplicationController
5
+ def primary_nav
6
+ jobs_count = GoodJob::Job.count
7
+ batches_count = GoodJob::BatchRecord.migrated? ? GoodJob::BatchRecord.all.size : 0
8
+ cron_entries_count = GoodJob::CronEntry.all.size
9
+ processes_count = GoodJob::Process.active.count
10
+
11
+ render json: {
12
+ jobs_count: number_to_human(jobs_count),
13
+ batches_count: number_to_human(batches_count),
14
+ cron_entries_count: number_to_human(cron_entries_count),
15
+ processes_count: number_to_human(processes_count),
16
+ }
17
+ end
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
+
25
+ private
26
+
27
+ def number_to_human(count)
28
+ helpers.number_to_human(count, **helpers.translate_hash("good_job.number.human.decimal_units"))
29
+ end
30
+
31
+ def number_with_delimiter(count)
32
+ helpers.number_with_delimiter(count, **helpers.translate_hash('good_job.number.format'))
33
+ end
34
+ end
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))
@@ -9,8 +9,10 @@ import LivePoll from "live_poll";
9
9
 
10
10
  import { Application } from "stimulus";
11
11
  import ThemeController from "theme_controller";
12
+ import AsyncValuesController from "async_values_controller";
12
13
  window.Stimulus = Application.start();
13
14
  Stimulus.register("theme", ThemeController)
15
+ Stimulus.register("async-values", AsyncValuesController)
14
16
 
15
17
  documentReady(function() {
16
18
  renderCharts();
@@ -0,0 +1,31 @@
1
+ import {Controller} from "stimulus"
2
+
3
+ // Fetches JSON values from the server and updates the targets with the response.
4
+ export default class extends Controller {
5
+ static values = {
6
+ url: String,
7
+ }
8
+ static targets = ["value"]
9
+
10
+ connect() {
11
+ this.#fetch();
12
+ }
13
+
14
+ async #fetch() {
15
+ const data = await fetch(this.urlValue).then(response => response.json())
16
+ this.valueTargets.forEach((target) => {
17
+ target.textContent = data[target.dataset['asyncValuesKey']];
18
+ target.classList.remove('d-none');
19
+
20
+ // When `data-async-values-zero-class="css-class"` is set, add `css-class` to the target if the value is "0"
21
+ if (target.dataset['asyncValuesZeroClass']) {
22
+ const className = target.dataset['asyncValuesZeroClass'];
23
+ if (data[target.dataset['asyncValuesKey']] === "0") {
24
+ target.classList.add(className);
25
+ } else {
26
+ target.classList.remove(className);
27
+ }
28
+ }
29
+ });
30
+ }
31
+ }
@@ -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 %>
@@ -14,33 +14,29 @@
14
14
  </button>
15
15
 
16
16
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
17
- <ul class="navbar-nav me-auto">
17
+ <ul class="navbar-nav me-auto" data-controller="async-values" data-async-values-url-value="<%= metrics_primary_nav_path %>">
18
18
  <li class="nav-item">
19
19
  <%= link_to jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] do %>
20
20
  <%= t(".jobs") %>
21
- <% jobs_count = GoodJob::Job.count %>
22
- <span class="badge bg-secondary rounded-pill"><%= number_to_human(jobs_count, **translate_hash("good_job.number.human.decimal_units")) %></span>
21
+ <span data-async-values-target="value" data-async-values-key="jobs_count" class="badge bg-secondary rounded-pill d-none"></span>
23
22
  <% end %>
24
23
  </li>
25
24
  <li class="nav-item">
26
25
  <%= link_to batches_path, class: ["nav-link", ("active" if controller_name == 'batches')] do %>
27
26
  <%=t ".batches" %>
28
- <% batches_count = GoodJob::BatchRecord.migrated? ? GoodJob::BatchRecord.all.size : 0 %>
29
- <span class="badge bg-secondary rounded-pill"><%= number_to_human(batches_count, **translate_hash("good_job.number.human.decimal_units")) %></span>
27
+ <span data-async-values-target="value" data-async-values-key="batches_count" class="badge bg-secondary rounded-pill d-none"></span>
30
28
  <% end %>
31
29
  </li>
32
30
  <li class="nav-item">
33
31
  <%= link_to cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] do %>
34
32
  <%= t(".cron_schedules") %>
35
- <% cron_entries_count = GoodJob::CronEntry.all.size %>
36
- <span class="badge bg-secondary rounded-pill"><%= cron_entries_count %></span>
33
+ <span data-async-values-target="value" data-async-values-key="cron_entries_count" class="badge bg-secondary rounded-pill d-none"></span>
37
34
  <% end %>
38
35
  </li>
39
36
  <li class="nav-item">
40
37
  <%= link_to processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] do %>
41
38
  <%= t(".processes") %>
42
- <% processes_count = GoodJob::Process.active.count %>
43
- <span class="badge bg-secondary rounded-pill <%= "bg-danger" if processes_count == 0 %>"><%= processes_count %></span>
39
+ <span data-async-values-target="value" data-async-values-key="processes_count" data-async-values-zero-class="bg-danger" class="badge bg-secondary rounded-pill d-none"></span>
44
40
  <% end %>
45
41
  </li>
46
42
  </ul>
@@ -77,7 +73,7 @@
77
73
  <%= I18n.locale %>
78
74
  </a>
79
75
 
80
- <ul class="dropdown-menu" aria-labelledby="localeOptions">
76
+ <ul class="dropdown-menu dropdown-menu-end min-w-auto" aria-labelledby="localeOptions">
81
77
  <% possible_locales = I18n.available_locales %>
82
78
  <% possible_locales.reject { |locale| locale == I18n.locale }.each do |locale| %>
83
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
@@ -214,11 +214,11 @@ fr:
214
214
  all: Tous
215
215
  all_jobs: Tous les jobs
216
216
  all_queues: Toutes les files d'attente
217
- clear: Clair
217
+ clear: Réinitialiser
218
218
  job_name: Nom du job
219
219
  placeholder: Recherche par classe, ID de job, paramètres de job et texte d'erreur.
220
220
  queue_name: Nom de la file d'attente
221
- search: Recherche
221
+ search: Rechercher
222
222
  navbar:
223
223
  batches: Lots
224
224
  cron_schedules: Cron
@@ -232,12 +232,12 @@ fr:
232
232
  light: Lumière
233
233
  theme: Thème
234
234
  secondary_navbar:
235
- inspiration: N'oublie pas, toi aussi tu fais du bon boulot !
235
+ inspiration: N'oublie pas, toi aussi tu fais du bon boulot !
236
236
  last_updated: Dernière mise à jour
237
237
  status:
238
238
  discarded: Mis au rebut
239
239
  queued: À la file
240
240
  retried: Réessayés
241
241
  running: En cours
242
- scheduled: Replanifiés
242
+ scheduled: Planifiés
243
243
  succeeded: Réussis
@@ -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
@@ -16,6 +16,8 @@ GoodJob::Engine.routes.draw do
16
16
  put :retry
17
17
  end
18
18
  end
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
19
21
 
20
22
  resources :batches, only: %i[index show]
21
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.1'
5
+ VERSION = '3.27.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
@@ -3,44 +3,44 @@
3
3
  require "active_job"
4
4
  require "active_job/queue_adapters"
5
5
 
6
- require "good_job/version"
7
- require "good_job/engine"
8
-
9
- require "good_job/adapter"
10
- require "active_job/queue_adapters/good_job_adapter"
11
- require "good_job/active_job_extensions/batches"
12
- require "good_job/active_job_extensions/concurrency"
13
- require "good_job/interrupt_error"
14
- require "good_job/active_job_extensions/interrupt_errors"
15
- require "good_job/active_job_extensions/labels"
16
- require "good_job/active_job_extensions/notify_options"
17
-
18
- require "good_job/overridable_connection"
19
- require "good_job/bulk"
20
- require "good_job/callable"
21
- require "good_job/capsule"
22
- require "good_job/cleanup_tracker"
23
- require "good_job/cli"
24
- require "good_job/configuration"
25
- require "good_job/cron_manager"
26
- require "good_job/current_thread"
27
- require "good_job/daemon"
28
- require "good_job/dependencies"
29
- require "good_job/job_performer"
30
- require "good_job/job_performer/metrics"
31
- require "good_job/log_subscriber"
32
- require "good_job/multi_scheduler"
33
- require "good_job/notifier"
34
- require "good_job/poller"
35
- require "good_job/probe_server"
36
- require "good_job/probe_server/healthcheck_middleware"
37
- require "good_job/probe_server/not_found_app"
38
- require "good_job/probe_server/simple_handler"
39
- require "good_job/probe_server/webrick_handler"
40
- require "good_job/scheduler"
41
- require "good_job/shared_executor"
42
- require "good_job/systemd_service"
43
- require "good_job/thread_status"
6
+ require_relative "good_job/version"
7
+ require_relative "good_job/engine"
8
+
9
+ require_relative "good_job/adapter"
10
+ require_relative "active_job/queue_adapters/good_job_adapter"
11
+ require_relative "good_job/active_job_extensions/batches"
12
+ require_relative "good_job/active_job_extensions/concurrency"
13
+ require_relative "good_job/interrupt_error"
14
+ require_relative "good_job/active_job_extensions/interrupt_errors"
15
+ require_relative "good_job/active_job_extensions/labels"
16
+ require_relative "good_job/active_job_extensions/notify_options"
17
+
18
+ require_relative "good_job/overridable_connection"
19
+ require_relative "good_job/bulk"
20
+ require_relative "good_job/callable"
21
+ require_relative "good_job/capsule"
22
+ require_relative "good_job/cleanup_tracker"
23
+ require_relative "good_job/cli"
24
+ require_relative "good_job/configuration"
25
+ require_relative "good_job/cron_manager"
26
+ require_relative "good_job/current_thread"
27
+ require_relative "good_job/daemon"
28
+ require_relative "good_job/dependencies"
29
+ require_relative "good_job/job_performer"
30
+ require_relative "good_job/job_performer/metrics"
31
+ require_relative "good_job/log_subscriber"
32
+ require_relative "good_job/multi_scheduler"
33
+ require_relative "good_job/notifier"
34
+ require_relative "good_job/poller"
35
+ require_relative "good_job/probe_server"
36
+ require_relative "good_job/probe_server/healthcheck_middleware"
37
+ require_relative "good_job/probe_server/not_found_app"
38
+ require_relative "good_job/probe_server/simple_handler"
39
+ require_relative "good_job/probe_server/webrick_handler"
40
+ require_relative "good_job/scheduler"
41
+ require_relative "good_job/shared_executor"
42
+ require_relative "good_job/systemd_service"
43
+ require_relative "good_job/thread_status"
44
44
 
45
45
  # GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
46
46
  #
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.1
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-01 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
@@ -168,16 +168,16 @@ dependencies:
168
168
  name: puma
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - "~>"
171
+ - - ">="
172
172
  - !ruby/object:Gem::Version
173
- version: '5.6'
173
+ version: '0'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - "~>"
178
+ - - ">="
179
179
  - !ruby/object:Gem::Version
180
- version: '5.6'
180
+ version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: rspec-rails
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -268,11 +282,13 @@ files:
268
282
  - app/controllers/good_job/cron_entries_controller.rb
269
283
  - app/controllers/good_job/frontends_controller.rb
270
284
  - app/controllers/good_job/jobs_controller.rb
285
+ - app/controllers/good_job/metrics_controller.rb
271
286
  - app/controllers/good_job/processes_controller.rb
272
287
  - app/filters/good_job/base_filter.rb
273
288
  - app/filters/good_job/batches_filter.rb
274
289
  - app/filters/good_job/jobs_filter.rb
275
290
  - app/frontend/good_job/application.js
291
+ - app/frontend/good_job/modules/async_values_controller.js
276
292
  - app/frontend/good_job/modules/charts.js
277
293
  - app/frontend/good_job/modules/checkbox_toggle.js
278
294
  - app/frontend/good_job/modules/document_ready.js
@@ -442,7 +458,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
442
458
  - !ruby/object:Gem::Version
443
459
  version: '0'
444
460
  requirements: []
445
- rubygems_version: 3.5.3
461
+ rubygems_version: 3.5.4
446
462
  signing_key:
447
463
  specification_version: 4
448
464
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails