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.
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