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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +14 -3
- data/app/controllers/good_job/metrics_controller.rb +35 -0
- data/app/filters/good_job/base_filter.rb +4 -0
- data/app/filters/good_job/jobs_filter.rb +4 -0
- data/app/frontend/good_job/application.js +2 -0
- data/app/frontend/good_job/modules/async_values_controller.js +31 -0
- data/app/frontend/good_job/style.css +4 -0
- data/app/models/concerns/good_job/advisory_lockable.rb +2 -7
- data/app/models/good_job/batch_record.rb +1 -0
- data/app/models/good_job/cron_entry.rb +6 -1
- data/app/models/good_job/discrete_execution.rb +1 -0
- data/app/models/good_job/execution.rb +2 -1
- data/app/models/good_job/job.rb +1 -0
- data/app/models/good_job/process.rb +1 -0
- data/app/models/good_job/setting.rb +29 -10
- data/app/views/good_job/shared/_filter.erb +4 -4
- data/app/views/good_job/shared/_navbar.erb +6 -10
- data/config/locales/de.yml +1 -1
- data/config/locales/en.yml +1 -1
- data/config/locales/es.yml +1 -1
- data/config/locales/fr.yml +7 -7
- data/config/locales/it.yml +1 -1
- data/config/locales/nl.yml +1 -1
- data/config/locales/ru.yml +1 -1
- data/config/routes.rb +2 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +66 -17
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +38 -38
- metadata +23 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b3817133850acc5176086af47b774bcb80a6ead4ee11e1121a10b23b5c7adb0
|
4
|
+
data.tar.gz: 0d8d3412835f37b23122a821d7f1ea1f20120a1e1893d29fadcd6e6b8697bce5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
630
|
+
production_task: {
|
621
631
|
cron: "0 0,12 * * *",
|
622
|
-
class: "
|
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
|
@@ -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
|
+
}
|
@@ -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 =
|
48
|
-
|
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
|
data/app/models/good_job/job.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
11
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
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.
|
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'
|
59
|
-
<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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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>
|
data/config/locales/de.yml
CHANGED
@@ -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
|
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
|
data/config/locales/en.yml
CHANGED
@@ -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
|
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
|
data/config/locales/es.yml
CHANGED
@@ -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
|
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
|
data/config/locales/fr.yml
CHANGED
@@ -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:
|
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:
|
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:
|
242
|
+
scheduled: Planifiés
|
243
243
|
succeeded: Réussis
|
data/config/locales/it.yml
CHANGED
@@ -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
|
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
|
data/config/locales/nl.yml
CHANGED
@@ -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
|
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
|
data/config/locales/ru.yml
CHANGED
@@ -34,7 +34,7 @@ ru:
|
|
34
34
|
cron_entries:
|
35
35
|
actions:
|
36
36
|
confirm_disable: Вы уверены, что хотите отключить эту задачу cron?
|
37
|
-
confirm_enable: Вы уверены, что хотите
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
data/lib/good_job/version.rb
CHANGED
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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.
|
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-
|
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: '
|
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: '
|
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.
|
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
|