good_job 4.0.3 → 4.1.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 +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
|