good_job 4.0.3 → 4.1.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 +30 -0
- data/README.md +1 -1
- data/app/charts/good_job/base_chart.rb +25 -0
- data/app/charts/good_job/performance_index_chart.rb +69 -0
- data/app/charts/good_job/performance_show_chart.rb +71 -0
- data/app/charts/good_job/scheduled_by_queue_chart.rb +23 -28
- data/app/controllers/good_job/metrics_controller.rb +5 -15
- data/app/controllers/good_job/performance_controller.rb +5 -0
- data/app/frontend/good_job/modules/charts.js +5 -17
- data/app/helpers/good_job/application_helper.rb +9 -1
- data/app/models/concerns/good_job/error_events.rb +14 -35
- data/app/models/good_job/base_execution.rb +11 -15
- data/app/models/good_job/discrete_execution.rb +0 -1
- data/app/models/good_job/job.rb +2 -2
- data/app/models/good_job/process.rb +13 -29
- data/app/views/good_job/performance/index.html.erb +3 -1
- data/app/views/good_job/performance/show.html.erb +5 -0
- data/app/views/good_job/shared/_filter.erb +2 -2
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/ja.yml +4 -0
- data/config/locales/ko.yml +4 -0
- data/config/locales/nl.yml +4 -0
- data/config/locales/pt-BR.yml +4 -0
- data/config/locales/ru.yml +4 -0
- data/config/locales/tr.yml +4 -0
- data/config/locales/uk.yml +4 -0
- data/config/routes.rb +1 -1
- data/lib/good_job/capsule_tracker.rb +1 -1
- data/lib/good_job/notifier.rb +7 -0
- data/lib/good_job/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aead87131c66c247222ce6733d9fe579b6d61fa0cc166b23ae75c6011e2b52db
|
4
|
+
data.tar.gz: 4158e3be14f1b93f46bb2eeefbeee246b4a2d98347519166b507811d343234ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7c522339ad17886675ac8c97104087a1d132767fe9e90c5a7f62005fced53abc40f29488e431b18e1a3a1ce528202245448617123eabaef1bda478576a9bfde
|
7
|
+
data.tar.gz: ce6fdd69c377f1e834b41640c5307e3bd030ac95d247b7037eab3fc5e806e6fb3030e57cc2de9d1f18a9e1f22e314e6b7e4a5aee709e2551b74b3d47f1e84388
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,35 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v4.1.0](https://github.com/bensheldon/good_job/tree/v4.1.0) (2024-07-16)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.99.1...v4.1.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Add keepalive SQL query to Notifier [\#1423](https://github.com/bensheldon/good_job/pull/1423) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
- Latency charts and histograms for individual job classes [\#1411](https://github.com/bensheldon/good_job/pull/1411) ([Earlopain](https://github.com/Earlopain))
|
11
|
+
|
12
|
+
**Fixed bugs:**
|
13
|
+
|
14
|
+
- Fix nonexistant association error between DiscreteExecution and Execution [\#1425](https://github.com/bensheldon/good_job/pull/1425) ([bensheldon](https://github.com/bensheldon))
|
15
|
+
|
16
|
+
**Closed issues:**
|
17
|
+
|
18
|
+
- Could not find the inverse association for execution \(:discrete\_executions in GoodJob::Execution\) [\#1424](https://github.com/bensheldon/good_job/issues/1424)
|
19
|
+
- 3.99.1 is marked as the latest version, not 4.0.3 [\#1422](https://github.com/bensheldon/good_job/issues/1422)
|
20
|
+
- How to maximise amount of jobs executed in parallel [\#1418](https://github.com/bensheldon/good_job/issues/1418)
|
21
|
+
- Performance Metrics for individual jobs [\#1397](https://github.com/bensheldon/good_job/issues/1397)
|
22
|
+
|
23
|
+
**Merged pull requests:**
|
24
|
+
|
25
|
+
- Remove some now unnecessary checks against `locked_by_id` existence [\#1421](https://github.com/bensheldon/good_job/pull/1421) ([Earlopain](https://github.com/Earlopain))
|
26
|
+
- Use rails enum for `error_event` and `lock_type` [\#1420](https://github.com/bensheldon/good_job/pull/1420) ([Earlopain](https://github.com/Earlopain))
|
27
|
+
- Add a little more wording to the v4 "ready to upgrade" instructions [\#1415](https://github.com/bensheldon/good_job/pull/1415) ([bensheldon](https://github.com/bensheldon))
|
28
|
+
|
29
|
+
## [v3.99.1](https://github.com/bensheldon/good_job/tree/v3.99.1) (2024-07-10)
|
30
|
+
|
31
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.0.3...v3.99.1)
|
32
|
+
|
3
33
|
## [v4.0.3](https://github.com/bensheldon/good_job/tree/v4.0.3) (2024-07-10)
|
4
34
|
|
5
35
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.0.2...v4.0.3)
|
data/README.md
CHANGED
@@ -876,7 +876,7 @@ To perform upgrades to the GoodJob database tables:
|
|
876
876
|
|
877
877
|
#### Upgrading v3 to v4
|
878
878
|
|
879
|
-
GoodJob v4 changes how job and job execution records are stored in the database; moving from job and executions being commingled in the `good_jobs` table to separately and discretely storing job executions in `good_job_executions`. To safely upgrade, all unfinished jobs must use the new format. This change was introduced in GoodJob [v3.15.4 (April 2023)](https://github.com/bensheldon/good_job/releases/tag/v3.15.4), so your application is likely ready-to-upgrade
|
879
|
+
GoodJob v4 changes how job and job execution records are stored in the database; moving from job and executions being commingled in the `good_jobs` table to separately and discretely storing job executions in `good_job_executions`. To safely upgrade, all unfinished jobs must use the new format. This change was introduced in GoodJob [v3.15.4 (April 2023)](https://github.com/bensheldon/good_job/releases/tag/v3.15.4), so your application is likely ready-to-upgrade in this respect if you have kept up with GoodJob updates and applied migrations (`bin/rails g good_job:update`). _Please be sure to doublecheck you are not missing subsequent migrations or deprecations too by following the instructions below._
|
880
880
|
|
881
881
|
To upgrade:
|
882
882
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class BaseChart
|
5
|
+
def start_end_binds
|
6
|
+
end_time = Time.current
|
7
|
+
start_time = end_time - 1.day
|
8
|
+
|
9
|
+
[
|
10
|
+
ActiveRecord::Relation::QueryAttribute.new('start_time', start_time, ActiveRecord::Type::DateTime.new),
|
11
|
+
ActiveRecord::Relation::QueryAttribute.new('end_time', end_time, ActiveRecord::Type::DateTime.new),
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
def string_to_hsl(string)
|
16
|
+
hash_value = string.sum
|
17
|
+
|
18
|
+
hue = hash_value % 360
|
19
|
+
saturation = (hash_value % 50) + 50
|
20
|
+
lightness = '50'
|
21
|
+
|
22
|
+
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class PerformanceIndexChart < BaseChart
|
5
|
+
def data
|
6
|
+
table_name = GoodJob::DiscreteExecution.table_name
|
7
|
+
|
8
|
+
sum_query = Arel.sql(GoodJob::Job.pg_or_jdbc_query(<<~SQL.squish))
|
9
|
+
SELECT *
|
10
|
+
FROM generate_series(
|
11
|
+
date_trunc('hour', $1::timestamp),
|
12
|
+
date_trunc('hour', $2::timestamp),
|
13
|
+
'1 hour'
|
14
|
+
) timestamp
|
15
|
+
LEFT JOIN (
|
16
|
+
SELECT
|
17
|
+
date_trunc('hour', scheduled_at) AS scheduled_at,
|
18
|
+
job_class,
|
19
|
+
SUM(duration) AS sum
|
20
|
+
FROM #{table_name} sources
|
21
|
+
GROUP BY date_trunc('hour', scheduled_at), job_class
|
22
|
+
) sources ON sources.scheduled_at = timestamp
|
23
|
+
ORDER BY timestamp ASC
|
24
|
+
SQL
|
25
|
+
|
26
|
+
executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Chart", start_end_binds)
|
27
|
+
|
28
|
+
job_names = executions_data.reject { |d| d['sum'].nil? }.map { |d| d['job_class'] || BaseFilter::EMPTY }.uniq
|
29
|
+
labels = []
|
30
|
+
jobs_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
31
|
+
labels << timestamp.in_time_zone.strftime('%H:%M')
|
32
|
+
job_names.each do |job_class|
|
33
|
+
sum = values.find { |d| d['job_class'] == job_class }&.[]('sum')
|
34
|
+
duration = sum ? ActiveSupport::Duration.parse(sum).to_f : 0
|
35
|
+
(hash[job_class] ||= []) << duration
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
{
|
40
|
+
type: "line",
|
41
|
+
data: {
|
42
|
+
labels: labels,
|
43
|
+
datasets: jobs_data.map do |job_class, data|
|
44
|
+
label = job_class || '(none)'
|
45
|
+
{
|
46
|
+
label: label,
|
47
|
+
data: data,
|
48
|
+
backgroundColor: string_to_hsl(label),
|
49
|
+
borderColor: string_to_hsl(label),
|
50
|
+
}
|
51
|
+
end,
|
52
|
+
},
|
53
|
+
options: {
|
54
|
+
plugins: {
|
55
|
+
title: {
|
56
|
+
display: true,
|
57
|
+
text: I18n.t("good_job.performance.index.chart_title"),
|
58
|
+
},
|
59
|
+
},
|
60
|
+
scales: {
|
61
|
+
y: {
|
62
|
+
beginAtZero: true,
|
63
|
+
},
|
64
|
+
},
|
65
|
+
},
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class PerformanceShowChart < BaseChart
|
5
|
+
# These numbers are lifted from Sidekiq
|
6
|
+
BUCKET_INTERVALS = [
|
7
|
+
0.02, 0.03, 0.045, 0.065, 0.1,
|
8
|
+
0.15, 0.225, 0.335, 0.5, 0.75,
|
9
|
+
1.1, 1.7, 2.5, 3.8, 5.75,
|
10
|
+
8.5, 13, 20, 30, 45,
|
11
|
+
65, 100, 150, 225, 335,
|
12
|
+
10**8 # About 3 years
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def initialize(job_class)
|
16
|
+
super()
|
17
|
+
@job_class = job_class
|
18
|
+
end
|
19
|
+
|
20
|
+
def data
|
21
|
+
table_name = GoodJob::DiscreteExecution.table_name
|
22
|
+
|
23
|
+
interval_entries = BUCKET_INTERVALS.map { "interval '#{_1}'" }.join(",")
|
24
|
+
sum_query = Arel.sql(GoodJob::Job.pg_or_jdbc_query(<<~SQL.squish))
|
25
|
+
SELECT
|
26
|
+
WIDTH_BUCKET(duration, ARRAY[#{interval_entries}]) as bucket_index,
|
27
|
+
COUNT(WIDTH_BUCKET(duration, ARRAY[#{interval_entries}])) AS count
|
28
|
+
FROM #{table_name} sources
|
29
|
+
WHERE
|
30
|
+
scheduled_at > $1::timestamp
|
31
|
+
AND scheduled_at < $2::timestamp
|
32
|
+
AND job_class = $3
|
33
|
+
AND duration IS NOT NULL
|
34
|
+
GROUP BY bucket_index
|
35
|
+
SQL
|
36
|
+
|
37
|
+
binds = [
|
38
|
+
*start_end_binds,
|
39
|
+
@job_class,
|
40
|
+
]
|
41
|
+
labels = BUCKET_INTERVALS.map { |interval| GoodJob::ApplicationController.helpers.format_duration(interval) }
|
42
|
+
labels[-1] = I18n.t("good_job.performance.show.slow")
|
43
|
+
executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Job Chart", binds)
|
44
|
+
executions_data = executions_data.to_a.index_by { |data| data["bucket_index"] }
|
45
|
+
|
46
|
+
bucket_data = 0.upto(BUCKET_INTERVALS.count).map do |bucket_index|
|
47
|
+
executions_data.dig(bucket_index, "count") || 0
|
48
|
+
end
|
49
|
+
|
50
|
+
{
|
51
|
+
type: "bar",
|
52
|
+
data: {
|
53
|
+
labels: labels,
|
54
|
+
datasets: [{
|
55
|
+
label: @job_class,
|
56
|
+
data: bucket_data,
|
57
|
+
backgroundColor: string_to_hsl(@job_class),
|
58
|
+
borderColor: string_to_hsl(@job_class),
|
59
|
+
}],
|
60
|
+
},
|
61
|
+
options: {
|
62
|
+
scales: {
|
63
|
+
y: {
|
64
|
+
beginAtZero: true,
|
65
|
+
},
|
66
|
+
},
|
67
|
+
},
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module GoodJob
|
4
|
-
class ScheduledByQueueChart
|
4
|
+
class ScheduledByQueueChart < BaseChart
|
5
5
|
def initialize(filter)
|
6
|
+
super()
|
6
7
|
@filter = filter
|
7
8
|
end
|
8
9
|
|
9
10
|
def data
|
10
|
-
end_time = Time.current
|
11
|
-
start_time = end_time - 1.day
|
12
11
|
table_name = GoodJob::Job.table_name
|
13
12
|
|
14
13
|
count_query = Arel.sql(GoodJob::Job.pg_or_jdbc_query(<<~SQL.squish))
|
@@ -31,11 +30,7 @@ module GoodJob
|
|
31
30
|
ORDER BY timestamp ASC
|
32
31
|
SQL
|
33
32
|
|
34
|
-
|
35
|
-
ActiveRecord::Relation::QueryAttribute.new('start_time', start_time, ActiveRecord::Type::DateTime.new),
|
36
|
-
ActiveRecord::Relation::QueryAttribute.new('end_time', end_time, ActiveRecord::Type::DateTime.new),
|
37
|
-
]
|
38
|
-
executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", binds)
|
33
|
+
executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", start_end_binds)
|
39
34
|
|
40
35
|
queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
|
41
36
|
labels = []
|
@@ -47,27 +42,27 @@ module GoodJob
|
|
47
42
|
end
|
48
43
|
|
49
44
|
{
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
label
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
45
|
+
type: "line",
|
46
|
+
data: {
|
47
|
+
labels: labels,
|
48
|
+
datasets: queues_data.map do |queue, data|
|
49
|
+
label = queue || '(none)'
|
50
|
+
{
|
51
|
+
label: label,
|
52
|
+
data: data,
|
53
|
+
backgroundColor: string_to_hsl(label),
|
54
|
+
borderColor: string_to_hsl(label),
|
55
|
+
}
|
56
|
+
end,
|
57
|
+
},
|
58
|
+
options: {
|
59
|
+
scales: {
|
60
|
+
y: {
|
61
|
+
beginAtZero: true,
|
62
|
+
},
|
63
|
+
},
|
64
|
+
},
|
60
65
|
}
|
61
66
|
end
|
62
|
-
|
63
|
-
def string_to_hsl(string)
|
64
|
-
hash_value = string.sum
|
65
|
-
|
66
|
-
hue = hash_value % 360
|
67
|
-
saturation = (hash_value % 50) + 50
|
68
|
-
lightness = '50'
|
69
|
-
|
70
|
-
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
|
71
|
-
end
|
72
67
|
end
|
73
68
|
end
|
@@ -9,27 +9,17 @@ module GoodJob
|
|
9
9
|
processes_count = GoodJob::Process.active.count
|
10
10
|
|
11
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),
|
12
|
+
jobs_count: helpers.number_to_human(jobs_count),
|
13
|
+
batches_count: helpers.number_to_human(batches_count),
|
14
|
+
cron_entries_count: helpers.number_to_human(cron_entries_count),
|
15
|
+
processes_count: helpers.number_to_human(processes_count),
|
16
16
|
}
|
17
17
|
end
|
18
18
|
|
19
19
|
def job_status
|
20
20
|
@filter = JobsFilter.new(params)
|
21
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'))
|
22
|
+
render json: @filter.states.transform_values { |count| helpers.number_with_delimiter(count) }
|
33
23
|
end
|
34
24
|
end
|
35
25
|
end
|
@@ -4,25 +4,13 @@ function renderCharts(animate) {
|
|
4
4
|
for (let i = 0; i < charts.length; i++) {
|
5
5
|
const chartEl = charts[i];
|
6
6
|
const chartData = JSON.parse(chartEl.dataset.json);
|
7
|
+
chartData.options ||= {};
|
8
|
+
chartData.options.animation = animate;
|
9
|
+
chartData.options.responsive = true;
|
10
|
+
chartData.options.maintainAspectRatio = false;
|
7
11
|
|
8
12
|
const ctx = chartEl.getContext('2d');
|
9
|
-
const chart = new Chart(ctx,
|
10
|
-
type: 'line',
|
11
|
-
data: {
|
12
|
-
labels: chartData.labels,
|
13
|
-
datasets: chartData.datasets
|
14
|
-
},
|
15
|
-
options: {
|
16
|
-
animation: animate,
|
17
|
-
responsive: true,
|
18
|
-
maintainAspectRatio: false,
|
19
|
-
scales: {
|
20
|
-
y: {
|
21
|
-
beginAtZero: true
|
22
|
-
}
|
23
|
-
}
|
24
|
-
}
|
25
|
-
});
|
13
|
+
const chart = new Chart(ctx, chartData);
|
26
14
|
}
|
27
15
|
}
|
28
16
|
|
@@ -14,7 +14,7 @@ module GoodJob
|
|
14
14
|
if sec < 1
|
15
15
|
t 'good_job.duration.milliseconds', ms: (sec * 1000).floor
|
16
16
|
elsif sec < 10
|
17
|
-
t 'good_job.duration.less_than_10_seconds', sec: sec.floor
|
17
|
+
t 'good_job.duration.less_than_10_seconds', sec: number_with_delimiter(sec.floor(1))
|
18
18
|
elsif sec < 60
|
19
19
|
t 'good_job.duration.seconds', sec: sec.floor
|
20
20
|
elsif sec < 3600
|
@@ -30,6 +30,14 @@ module GoodJob
|
|
30
30
|
tag.time(text, datetime: timestamp, title: timestamp)
|
31
31
|
end
|
32
32
|
|
33
|
+
def number_to_human(count)
|
34
|
+
super(count, **translate_hash("good_job.number.human.decimal_units"))
|
35
|
+
end
|
36
|
+
|
37
|
+
def number_with_delimiter(count)
|
38
|
+
super(count, **translate_hash('good_job.number.format'))
|
39
|
+
end
|
40
|
+
|
33
41
|
def translate_hash(key, **options)
|
34
42
|
translation_exists?(key, **options) ? translate(key, **options) : {}
|
35
43
|
end
|
@@ -5,41 +5,20 @@ module GoodJob
|
|
5
5
|
module ErrorEvents
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
ERROR_EVENT_RETRY_STOPPED => 4,
|
23
|
-
ERROR_EVENT_DISCARDED => 5,
|
24
|
-
}.freeze
|
25
|
-
|
26
|
-
# TODO: GoodJob v4 can make this an `enum` once migrations are guaranteed.
|
27
|
-
def error_event
|
28
|
-
return unless self.class.columns_hash['error_event']
|
29
|
-
|
30
|
-
enum = read_attribute(:error_event)
|
31
|
-
return unless enum
|
32
|
-
|
33
|
-
ERROR_EVENT_ENUMS.key(enum)
|
34
|
-
end
|
35
|
-
|
36
|
-
def error_event=(event)
|
37
|
-
return unless self.class.columns_hash['error_event']
|
38
|
-
|
39
|
-
enum = ERROR_EVENT_ENUMS[event]
|
40
|
-
raise(ArgumentError, "Invalid error_event: #{event}") if event && !enum
|
41
|
-
|
42
|
-
write_attribute(:error_event, enum)
|
8
|
+
included do
|
9
|
+
error_event_enum = {
|
10
|
+
interrupted: 0,
|
11
|
+
unhandled: 1,
|
12
|
+
handled: 2,
|
13
|
+
retried: 3,
|
14
|
+
retry_stopped: 4,
|
15
|
+
discarded: 5,
|
16
|
+
}
|
17
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new('7.1.0.a')
|
18
|
+
enum :error_event, error_event_enum, validate: { allow_nil: true }
|
19
|
+
else
|
20
|
+
enum error_event: error_event_enum
|
21
|
+
end
|
43
22
|
end
|
44
23
|
end
|
45
24
|
end
|
@@ -409,15 +409,15 @@ module GoodJob
|
|
409
409
|
|
410
410
|
interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{existing_performed_at}'"))
|
411
411
|
self.error = interrupt_error_string
|
412
|
-
self.error_event =
|
412
|
+
self.error_event = :interrupted
|
413
413
|
monotonic_duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - monotonic_start).seconds
|
414
414
|
|
415
415
|
discrete_execution_attrs = {
|
416
416
|
error: interrupt_error_string,
|
417
417
|
finished_at: job_performed_at,
|
418
|
+
error_event: :interrupted,
|
419
|
+
duration: monotonic_duration,
|
418
420
|
}
|
419
|
-
discrete_execution_attrs[:error_event] = GoodJob::ErrorEvents::ERROR_EVENT_ENUMS[GoodJob::ErrorEvents::ERROR_EVENT_INTERRUPTED]
|
420
|
-
discrete_execution_attrs[:duration] = monotonic_duration
|
421
421
|
discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
|
422
422
|
end
|
423
423
|
|
@@ -451,13 +451,13 @@ module GoodJob
|
|
451
451
|
handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
|
452
452
|
|
453
453
|
error_event = if handled_error == current_thread.error_on_discard
|
454
|
-
|
454
|
+
:discarded
|
455
455
|
elsif handled_error == current_thread.error_on_retry
|
456
|
-
|
456
|
+
:retried
|
457
457
|
elsif handled_error == current_thread.error_on_retry_stopped
|
458
|
-
|
458
|
+
:retry_stopped
|
459
459
|
elsif handled_error
|
460
|
-
|
460
|
+
:handled
|
461
461
|
end
|
462
462
|
|
463
463
|
instrument_payload.merge!(
|
@@ -469,11 +469,11 @@ module GoodJob
|
|
469
469
|
ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
|
470
470
|
rescue StandardError => e
|
471
471
|
error_event = if e.is_a?(GoodJob::InterruptError)
|
472
|
-
|
472
|
+
:interrupted
|
473
473
|
elsif e == current_thread.error_on_retry_stopped
|
474
|
-
|
474
|
+
:retry_stopped
|
475
475
|
else
|
476
|
-
|
476
|
+
:unhandled
|
477
477
|
end
|
478
478
|
|
479
479
|
instrument_payload[:unhandled_error] = e
|
@@ -481,11 +481,7 @@ module GoodJob
|
|
481
481
|
end
|
482
482
|
end
|
483
483
|
|
484
|
-
job_attributes =
|
485
|
-
{ locked_by_id: nil, locked_at: nil }
|
486
|
-
else
|
487
|
-
{}
|
488
|
-
end
|
484
|
+
job_attributes = { locked_by_id: nil, locked_at: nil }
|
489
485
|
|
490
486
|
job_error = result.handled_error || result.unhandled_error
|
491
487
|
if job_error
|
@@ -7,7 +7,6 @@ module GoodJob # :nodoc:
|
|
7
7
|
self.table_name = 'good_job_executions'
|
8
8
|
self.implicit_order_column = 'created_at'
|
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
|
11
10
|
belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
|
12
11
|
|
13
12
|
scope :finished, -> { where.not(finished_at: nil) }
|
data/app/models/good_job/job.rb
CHANGED
@@ -146,7 +146,7 @@ module GoodJob
|
|
146
146
|
|
147
147
|
self.class.transaction(joinable: false, requires_new: true) do
|
148
148
|
new_active_job = active_job.retry_job(wait: 0, error: error)
|
149
|
-
self.error_event =
|
149
|
+
self.error_event = :retried if error
|
150
150
|
save!
|
151
151
|
end
|
152
152
|
end
|
@@ -212,7 +212,7 @@ module GoodJob
|
|
212
212
|
update(
|
213
213
|
finished_at: Time.current,
|
214
214
|
error: self.class.format_error(job_error),
|
215
|
-
error_event:
|
215
|
+
error_event: :discarded
|
216
216
|
)
|
217
217
|
end
|
218
218
|
|
@@ -15,18 +15,18 @@ module GoodJob # :nodoc:
|
|
15
15
|
|
16
16
|
self.table_name = 'good_job_processes'
|
17
17
|
self.implicit_order_column = 'created_at'
|
18
|
-
LOCK_TYPES = [
|
19
|
-
LOCK_TYPE_ADVISORY = 'advisory',
|
20
|
-
].freeze
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
}
|
25
|
-
|
26
|
-
|
19
|
+
lock_type_enum = {
|
20
|
+
advisory: 0,
|
21
|
+
}
|
22
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new('7.1.0.a')
|
23
|
+
enum :lock_type, lock_type_enum, validate: { allow_nil: true }
|
24
|
+
else
|
25
|
+
enum lock_type: lock_type_enum
|
26
|
+
end
|
27
27
|
|
28
28
|
has_many :locked_jobs, class_name: "GoodJob::Job", foreign_key: :locked_by_id, inverse_of: :locked_by_process, dependent: nil
|
29
|
-
after_destroy { locked_jobs.update_all(locked_by_id: nil)
|
29
|
+
after_destroy { locked_jobs.update_all(locked_by_id: nil) } # rubocop:disable Rails/SkipsModelValidations
|
30
30
|
|
31
31
|
# Processes that are active and locked.
|
32
32
|
# @!method active
|
@@ -34,7 +34,7 @@ module GoodJob # :nodoc:
|
|
34
34
|
# @return [ActiveRecord::Relation]
|
35
35
|
scope :active, (lambda do
|
36
36
|
query = joins_advisory_locks
|
37
|
-
query.where(lock_type:
|
37
|
+
query.where(lock_type: :advisory).advisory_locked
|
38
38
|
.or(query.where(lock_type: nil).where(arel_table[:updated_at].gt(EXPIRED_INTERVAL.ago)))
|
39
39
|
end)
|
40
40
|
|
@@ -44,14 +44,14 @@ module GoodJob # :nodoc:
|
|
44
44
|
# @return [ActiveRecord::Relation]
|
45
45
|
scope :inactive, (lambda do
|
46
46
|
query = joins_advisory_locks
|
47
|
-
query.where(lock_type:
|
47
|
+
query.where(lock_type: :advisory).advisory_unlocked
|
48
48
|
.or(query.where(lock_type: nil).where(arel_table[:updated_at].lt(EXPIRED_INTERVAL.ago)))
|
49
49
|
end)
|
50
50
|
|
51
51
|
# Deletes all inactive process records.
|
52
52
|
def self.cleanup
|
53
53
|
inactive.find_each do |process|
|
54
|
-
GoodJob::Job.where(locked_by_id: process.id).update_all(locked_by_id: nil, locked_at: nil)
|
54
|
+
GoodJob::Job.where(locked_by_id: process.id).update_all(locked_by_id: nil, locked_at: nil) # rubocop:disable Rails/SkipsModelValidations
|
55
55
|
process.delete
|
56
56
|
end
|
57
57
|
end
|
@@ -63,7 +63,7 @@ module GoodJob # :nodoc:
|
|
63
63
|
}
|
64
64
|
if with_advisory_lock
|
65
65
|
attributes[:create_with_advisory_lock] = true
|
66
|
-
attributes[:lock_type] =
|
66
|
+
attributes[:lock_type] = :advisory
|
67
67
|
end
|
68
68
|
create!(attributes)
|
69
69
|
end
|
@@ -124,21 +124,5 @@ module GoodJob # :nodoc:
|
|
124
124
|
def schedulers
|
125
125
|
state.fetch("schedulers", [])
|
126
126
|
end
|
127
|
-
|
128
|
-
def lock_type
|
129
|
-
return unless self.class.columns_hash['lock_type']
|
130
|
-
|
131
|
-
enum = super
|
132
|
-
LOCK_TYPE_ENUMS.key(enum) if enum
|
133
|
-
end
|
134
|
-
|
135
|
-
def lock_type=(value)
|
136
|
-
return unless self.class.columns_hash['lock_type']
|
137
|
-
|
138
|
-
enum = LOCK_TYPE_ENUMS[value]
|
139
|
-
raise(ArgumentError, "Invalid error_event: #{value}") if value && !enum
|
140
|
-
|
141
|
-
super(enum)
|
142
|
-
end
|
143
127
|
end
|
144
128
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
<h2 class="pt-3 pb-2"><%= t ".title" %></h2>
|
3
3
|
</div>
|
4
4
|
|
5
|
+
<%= render 'good_job/shared/chart', chart_data: GoodJob::PerformanceIndexChart.new.data %>
|
6
|
+
|
5
7
|
<div class="my-3 card">
|
6
8
|
<div class="list-group list-group-flush text-nowrap" role="table">
|
7
9
|
<header class="list-group-item bg-body-tertiary">
|
@@ -18,7 +20,7 @@
|
|
18
20
|
<% @performances.each do |performance| %>
|
19
21
|
<div role="row" class="list-group-item py-3">
|
20
22
|
<div class="row align-items-center">
|
21
|
-
<div class="col-12 col-lg-4"><%= performance.job_class %></div>
|
23
|
+
<div class="col-12 col-lg-4"><%= link_to performance.job_class, performance_path(performance.job_class) %></div>
|
22
24
|
<div class="col-6 col-lg-2 text-wrap">
|
23
25
|
<div class="d-lg-none small text-muted mt-1"><%= t ".executions" %></div>
|
24
26
|
<%= performance.executions_count %>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
<option value="" <%= "selected='selected'" if params[:queue_name].blank? %>><%= t ".all_queues" %></option>
|
16
16
|
|
17
17
|
<% filter.queues.each do |name, count| %>
|
18
|
-
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= number_with_delimiter(count
|
18
|
+
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= number_with_delimiter(count) %>)</option>
|
19
19
|
<% end %>
|
20
20
|
</select>
|
21
21
|
</div>
|
@@ -26,7 +26,7 @@
|
|
26
26
|
<option value="" <%= "selected='selected'" if params[:job_class].blank? %>><%= t ".all_jobs" %></option>
|
27
27
|
|
28
28
|
<% filter.job_classes.each do |name, count| %>
|
29
|
-
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= number_with_delimiter(count
|
29
|
+
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= number_with_delimiter(count) %>)</option>
|
30
30
|
<% end %>
|
31
31
|
</select>
|
32
32
|
</div>
|
data/config/locales/de.yml
CHANGED
@@ -197,11 +197,15 @@ de:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Durchschnittliche Dauer
|
200
|
+
chart_title: Gesamtdauer der Auftragsausführung in Sekunden
|
200
201
|
executions: Hinrichtungen
|
201
202
|
job_class: Berufsklasse
|
202
203
|
maximum_duration: Maximale Dauer
|
203
204
|
minimum_duration: Mindestdauer
|
204
205
|
title: Leistung
|
206
|
+
show:
|
207
|
+
slow: Langsam
|
208
|
+
title: Leistung
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron aktiviert
|
data/config/locales/en.yml
CHANGED
@@ -197,11 +197,15 @@ en:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Average duration
|
200
|
+
chart_title: Total job execution time in seconds
|
200
201
|
executions: Executions
|
201
202
|
job_class: Job class
|
202
203
|
maximum_duration: Maximum duration
|
203
204
|
minimum_duration: Minimum duration
|
204
205
|
title: Performance
|
206
|
+
show:
|
207
|
+
slow: Slow
|
208
|
+
title: Performance
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron enabled
|
data/config/locales/es.yml
CHANGED
@@ -197,11 +197,15 @@ es:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Duración promedio
|
200
|
+
chart_title: Tiempo total de ejecución del trabajo en segundos
|
200
201
|
executions: Ejecuciones
|
201
202
|
job_class: clase de trabajo
|
202
203
|
maximum_duration: Duración máxima
|
203
204
|
minimum_duration: Duración mínima
|
204
205
|
title: Actuación
|
206
|
+
show:
|
207
|
+
slow: Lento
|
208
|
+
title: Actuación
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron habilitado
|
data/config/locales/fr.yml
CHANGED
@@ -197,11 +197,15 @@ fr:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Durée moyenne
|
200
|
+
chart_title: Durée totale d'exécution du travail en secondes
|
200
201
|
executions: Exécutions
|
201
202
|
job_class: Catégorie d'emplois
|
202
203
|
maximum_duration: Durée maximale
|
203
204
|
minimum_duration: Durée minimale
|
204
205
|
title: Performance
|
206
|
+
show:
|
207
|
+
slow: Lenteur
|
208
|
+
title: Performance
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron activé
|
data/config/locales/it.yml
CHANGED
@@ -197,11 +197,15 @@ it:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Durata media
|
200
|
+
chart_title: Tempo totale di esecuzione del lavoro in secondi
|
200
201
|
executions: Esecuzioni
|
201
202
|
job_class: Classe di lavoro
|
202
203
|
maximum_duration: Durata massima
|
203
204
|
minimum_duration: Durata minima
|
204
205
|
title: Prestazione
|
206
|
+
show:
|
207
|
+
slow: Lento
|
208
|
+
title: Prestazione
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron abilitato
|
data/config/locales/ja.yml
CHANGED
@@ -197,11 +197,15 @@ ja:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: 平均所要時間
|
200
|
+
chart_title: ジョブの総実行時間(秒
|
200
201
|
executions: 処刑
|
201
202
|
job_class: 職種
|
202
203
|
maximum_duration: 最大持続時間
|
203
204
|
minimum_duration: 最小期間
|
204
205
|
title: パフォーマンス
|
206
|
+
show:
|
207
|
+
slow: 遅い
|
208
|
+
title: パフォーマンス
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron が有効になっている
|
data/config/locales/ko.yml
CHANGED
@@ -197,11 +197,15 @@ ko:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: 평균 지속 시간
|
200
|
+
chart_title: 총 작업 실행 시간(초)
|
200
201
|
executions: 처형
|
201
202
|
job_class: 직업군
|
202
203
|
maximum_duration: 최대 기간
|
203
204
|
minimum_duration: 최소 기간
|
204
205
|
title: 성능
|
206
|
+
show:
|
207
|
+
slow: 느림
|
208
|
+
title: 성능
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron이 활성화되어 있음
|
data/config/locales/nl.yml
CHANGED
@@ -197,11 +197,15 @@ nl:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Gemiddelde duur
|
200
|
+
chart_title: Totale uitvoeringstijd van de taak in seconden
|
200
201
|
executions: Executies
|
201
202
|
job_class: Functie klasse
|
202
203
|
maximum_duration: Maximale duur
|
203
204
|
minimum_duration: Minimale duur
|
204
205
|
title: Prestatie
|
206
|
+
show:
|
207
|
+
slow: Langzaam
|
208
|
+
title: Prestatie
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron ingeschakeld
|
data/config/locales/pt-BR.yml
CHANGED
@@ -197,11 +197,15 @@ pt-BR:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Duração média
|
200
|
+
chart_title: Tempo total de execução do trabalho em segundos
|
200
201
|
executions: Execuções
|
201
202
|
job_class: Classe de trabalho
|
202
203
|
maximum_duration: Duração máxima
|
203
204
|
minimum_duration: Duração mínima
|
204
205
|
title: Desempenho
|
206
|
+
show:
|
207
|
+
slow: Lento
|
208
|
+
title: Desempenho
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Agendamento ativado
|
data/config/locales/ru.yml
CHANGED
@@ -223,11 +223,15 @@ ru:
|
|
223
223
|
performance:
|
224
224
|
index:
|
225
225
|
average_duration: Средняя продолжительность
|
226
|
+
chart_title: Общее время выполнения задания в секундах
|
226
227
|
executions: Казни
|
227
228
|
job_class: Класс работы
|
228
229
|
maximum_duration: Максимальная продолжительность
|
229
230
|
minimum_duration: Минимальная продолжительность
|
230
231
|
title: Производительность
|
232
|
+
show:
|
233
|
+
slow: Медленный
|
234
|
+
title: Производительность
|
231
235
|
processes:
|
232
236
|
index:
|
233
237
|
cron_enabled: Cron включен
|
data/config/locales/tr.yml
CHANGED
@@ -197,11 +197,15 @@ tr:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Ortalama süre
|
200
|
+
chart_title: Saniye cinsinden toplam iş yürütme süresi
|
200
201
|
executions: İnfazlar
|
201
202
|
job_class: İş sınıfı
|
202
203
|
maximum_duration: Maksimum süre
|
203
204
|
minimum_duration: Minimum süre
|
204
205
|
title: Verim
|
206
|
+
show:
|
207
|
+
slow: Yavaş
|
208
|
+
title: Verim
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron etkin
|
data/config/locales/uk.yml
CHANGED
@@ -223,11 +223,15 @@ uk:
|
|
223
223
|
performance:
|
224
224
|
index:
|
225
225
|
average_duration: Середня тривалість
|
226
|
+
chart_title: Загальний час виконання завдання в секундах
|
226
227
|
executions: Страти
|
227
228
|
job_class: Клас роботи
|
228
229
|
maximum_duration: Максимальна тривалість
|
229
230
|
minimum_duration: Мінімальна тривалість
|
230
231
|
title: Продуктивність
|
232
|
+
show:
|
233
|
+
slow: Повільно
|
234
|
+
title: Продуктивність
|
231
235
|
processes:
|
232
236
|
index:
|
233
237
|
cron_enabled: Cron увімкнено
|
data/config/routes.rb
CHANGED
@@ -31,7 +31,7 @@ GoodJob::Engine.routes.draw do
|
|
31
31
|
|
32
32
|
resources :processes, only: %i[index]
|
33
33
|
|
34
|
-
resources :performance, only: %i[index]
|
34
|
+
resources :performance, only: %i[index show]
|
35
35
|
|
36
36
|
scope :frontend, controller: :frontends do
|
37
37
|
get "modules/:name", action: :module, as: :frontend_module, constraints: { format: 'js' }
|
@@ -84,7 +84,7 @@ module GoodJob # :nodoc:
|
|
84
84
|
if !advisory_locked? || !advisory_locked_connection?
|
85
85
|
@record.class.transaction do
|
86
86
|
@record.advisory_lock!
|
87
|
-
@record.update(lock_type:
|
87
|
+
@record.update(lock_type: :advisory)
|
88
88
|
end
|
89
89
|
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
90
90
|
end
|
data/lib/good_job/notifier.rb
CHANGED
@@ -27,6 +27,8 @@ module GoodJob # :nodoc:
|
|
27
27
|
RECONNECT_INTERVAL = 5
|
28
28
|
# Number of consecutive connection errors before reporting an error
|
29
29
|
CONNECTION_ERRORS_REPORTING_THRESHOLD = 6
|
30
|
+
# Interval for emitting a noop SQL query to keep the connection alive
|
31
|
+
KEEPALIVE_INTERVAL = 10
|
30
32
|
|
31
33
|
# Connection errors that will wait {RECONNECT_INTERVAL} before reconnecting
|
32
34
|
CONNECTION_ERRORS = %w[
|
@@ -78,6 +80,7 @@ module GoodJob # :nodoc:
|
|
78
80
|
@enable_listening = enable_listening
|
79
81
|
@task = nil
|
80
82
|
@capsule = capsule
|
83
|
+
@last_keepalive_time = Time.current
|
81
84
|
|
82
85
|
start
|
83
86
|
self.class.instances << self
|
@@ -269,6 +272,10 @@ module GoodJob # :nodoc:
|
|
269
272
|
raw_connection.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
|
270
273
|
yield(channel, payload)
|
271
274
|
end
|
275
|
+
if Time.current - @last_keepalive_time >= KEEPALIVE_INTERVAL
|
276
|
+
raw_connection.async_exec("SELECT 1")
|
277
|
+
@last_keepalive_time = Time.current
|
278
|
+
end
|
272
279
|
elsif @enable_listening && raw_connection.respond_to?(:jdbc_connection)
|
273
280
|
raw_connection.execute_query("SELECT 1")
|
274
281
|
notifications = raw_connection.jdbc_connection.getNotifications
|
data/lib/good_job/version.rb
CHANGED
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: 4.0
|
4
|
+
version: 4.1.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-07-
|
11
|
+
date: 2024-07-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -248,6 +248,9 @@ files:
|
|
248
248
|
- CHANGELOG.md
|
249
249
|
- LICENSE.txt
|
250
250
|
- README.md
|
251
|
+
- app/charts/good_job/base_chart.rb
|
252
|
+
- app/charts/good_job/performance_index_chart.rb
|
253
|
+
- app/charts/good_job/performance_show_chart.rb
|
251
254
|
- app/charts/good_job/scheduled_by_queue_chart.rb
|
252
255
|
- app/controllers/good_job/application_controller.rb
|
253
256
|
- app/controllers/good_job/batches_controller.rb
|
@@ -308,6 +311,7 @@ files:
|
|
308
311
|
- app/views/good_job/jobs/index.html.erb
|
309
312
|
- app/views/good_job/jobs/show.html.erb
|
310
313
|
- app/views/good_job/performance/index.html.erb
|
314
|
+
- app/views/good_job/performance/show.html.erb
|
311
315
|
- app/views/good_job/processes/index.html.erb
|
312
316
|
- app/views/good_job/shared/_alert.erb
|
313
317
|
- app/views/good_job/shared/_chart.erb
|