good_job 3.26.1 → 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: 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