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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +1 -1
  4. data/app/charts/good_job/base_chart.rb +25 -0
  5. data/app/charts/good_job/performance_index_chart.rb +69 -0
  6. data/app/charts/good_job/performance_show_chart.rb +71 -0
  7. data/app/charts/good_job/scheduled_by_queue_chart.rb +23 -28
  8. data/app/controllers/good_job/metrics_controller.rb +5 -15
  9. data/app/controllers/good_job/performance_controller.rb +5 -0
  10. data/app/frontend/good_job/modules/charts.js +5 -17
  11. data/app/helpers/good_job/application_helper.rb +9 -1
  12. data/app/models/concerns/good_job/error_events.rb +14 -35
  13. data/app/models/good_job/base_execution.rb +11 -15
  14. data/app/models/good_job/discrete_execution.rb +0 -1
  15. data/app/models/good_job/job.rb +2 -2
  16. data/app/models/good_job/process.rb +13 -29
  17. data/app/views/good_job/performance/index.html.erb +3 -1
  18. data/app/views/good_job/performance/show.html.erb +5 -0
  19. data/app/views/good_job/shared/_filter.erb +2 -2
  20. data/config/locales/de.yml +4 -0
  21. data/config/locales/en.yml +4 -0
  22. data/config/locales/es.yml +4 -0
  23. data/config/locales/fr.yml +4 -0
  24. data/config/locales/it.yml +4 -0
  25. data/config/locales/ja.yml +4 -0
  26. data/config/locales/ko.yml +4 -0
  27. data/config/locales/nl.yml +4 -0
  28. data/config/locales/pt-BR.yml +4 -0
  29. data/config/locales/ru.yml +4 -0
  30. data/config/locales/tr.yml +4 -0
  31. data/config/locales/uk.yml +4 -0
  32. data/config/routes.rb +1 -1
  33. data/lib/good_job/capsule_tracker.rb +1 -1
  34. data/lib/good_job/notifier.rb +7 -0
  35. data/lib/good_job/version.rb +1 -1
  36. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41adaae5b9a9c016ba000f7610efa23d533a177374e61039f135a1d152be146c
4
- data.tar.gz: 92443b132e11697ae1ba31758852a818e5fbaf201cf080649f1c948d927265d4
3
+ metadata.gz: aead87131c66c247222ce6733d9fe579b6d61fa0cc166b23ae75c6011e2b52db
4
+ data.tar.gz: 4158e3be14f1b93f46bb2eeefbeee246b4a2d98347519166b507811d343234ab
5
5
  SHA512:
6
- metadata.gz: 155174df9c9d4daf50f4e898585e2cf40898de46eb5974592acdf32cc435cb4cb13b6d4953862a89a3b8223d7bbd2c9e79c60d4a974642f180092d836d04ebb2
7
- data.tar.gz: 84b922a4853ffb85da2380ab80c20b3d7392f403218838c3fdcb8872726d2df469d81edeaa28cb2f5a76401280460839c1d3f6ca1f2c9a98c4a1b275ff99cee4
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 already if you have kept up with GoodJob updates.
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
- binds = [
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
- labels: labels,
51
- datasets: queues_data.map do |queue, data|
52
- label = queue || '(none)'
53
- {
54
- label: label,
55
- data: data,
56
- backgroundColor: string_to_hsl(label),
57
- borderColor: string_to_hsl(label),
58
- }
59
- end,
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
@@ -15,5 +15,10 @@ module GoodJob
15
15
  ")
16
16
  .order("job_class")
17
17
  end
18
+
19
+ def show
20
+ representative_job = GoodJob::Job.find_by!(job_class: params[:id])
21
+ @job_class = representative_job.job_class
22
+ end
18
23
  end
19
24
  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
- ERROR_EVENTS = [
9
- ERROR_EVENT_INTERRUPTED = 'interrupted',
10
- ERROR_EVENT_UNHANDLED = 'unhandled',
11
- ERROR_EVENT_HANDLED = 'handled',
12
- ERROR_EVENT_RETRIED = 'retried',
13
- ERROR_EVENT_RETRY_STOPPED = 'retry_stopped',
14
- ERROR_EVENT_DISCARDED = 'discarded',
15
- ].freeze
16
-
17
- ERROR_EVENT_ENUMS = {
18
- ERROR_EVENT_INTERRUPTED => 0,
19
- ERROR_EVENT_UNHANDLED => 1,
20
- ERROR_EVENT_HANDLED => 2,
21
- ERROR_EVENT_RETRIED => 3,
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 = ERROR_EVENT_INTERRUPTED
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
- ERROR_EVENT_DISCARDED
454
+ :discarded
455
455
  elsif handled_error == current_thread.error_on_retry
456
- ERROR_EVENT_RETRIED
456
+ :retried
457
457
  elsif handled_error == current_thread.error_on_retry_stopped
458
- ERROR_EVENT_RETRY_STOPPED
458
+ :retry_stopped
459
459
  elsif handled_error
460
- ERROR_EVENT_HANDLED
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
- ERROR_EVENT_INTERRUPTED
472
+ :interrupted
473
473
  elsif e == current_thread.error_on_retry_stopped
474
- ERROR_EVENT_RETRY_STOPPED
474
+ :retry_stopped
475
475
  else
476
- ERROR_EVENT_UNHANDLED
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 = if self.class.columns_hash.key?("locked_by_id")
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) }
@@ -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 = ERROR_EVENT_RETRIED if error
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: ERROR_EVENT_DISCARDED
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
- LOCK_TYPE_ENUMS = {
23
- LOCK_TYPE_ADVISORY => 1,
24
- }.freeze
25
-
26
- self.table_name = 'good_job_processes'
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) if GoodJob::Job.columns_hash.key?("locked_by_id") } # rubocop:disable Rails/SkipsModelValidations
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: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_locked
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: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_unlocked
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) if GoodJob::Job.columns_hash.key?("locked_by_id") # rubocop:disable Rails/SkipsModelValidations
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] = LOCK_TYPE_ADVISORY
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 %>
@@ -0,0 +1,5 @@
1
+ <div class="border-bottom">
2
+ <h2 class="pt-3 pb-2"><%= t ".title" %> - <%= @job_class %></h2>
3
+ </div>
4
+
5
+ <%= render 'good_job/shared/chart', chart_data: GoodJob::PerformanceShowChart.new(@job_class).data %>
@@ -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, translate_hash('good_job.number.format')) %>)</option>
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, translate_hash('good_job.number.format')) %>)</option>
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>
@@ -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
@@ -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
@@ -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
@@ -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é
@@ -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
@@ -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 が有効になっている
@@ -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이 활성화되어 있음
@@ -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
@@ -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
@@ -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 включен
@@ -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
@@ -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: GoodJob::Process::LOCK_TYPE_ADVISORY)
87
+ @record.update(lock_type: :advisory)
88
88
  end
89
89
  @advisory_locked_connection = WeakRef.new(@record.class.connection)
90
90
  end
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.0.3'
5
+ VERSION = '4.1.0'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
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.3
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-10 00:00:00.000000000 Z
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