good_job 4.0.3 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -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/application_controller.rb +1 -1
  9. data/app/controllers/good_job/frontends_controller.rb +6 -2
  10. data/app/controllers/good_job/metrics_controller.rb +5 -15
  11. data/app/controllers/good_job/performance_controller.rb +6 -1
  12. data/app/frontend/good_job/icons.svg +79 -0
  13. data/app/frontend/good_job/modules/charts.js +5 -17
  14. data/app/frontend/good_job/style.css +5 -0
  15. data/app/helpers/good_job/application_helper.rb +9 -1
  16. data/app/helpers/good_job/icons_helper.rb +8 -5
  17. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  18. data/app/models/concerns/good_job/error_events.rb +14 -35
  19. data/app/models/concerns/good_job/reportable.rb +8 -12
  20. data/app/models/good_job/batch.rb +10 -5
  21. data/app/models/good_job/batch_record.rb +18 -15
  22. data/app/models/good_job/discrete_execution.rb +6 -60
  23. data/app/models/good_job/execution.rb +59 -4
  24. data/app/models/good_job/execution_result.rb +6 -6
  25. data/app/models/good_job/job.rb +569 -14
  26. data/app/models/good_job/process.rb +13 -29
  27. data/app/views/good_job/batches/_jobs.erb +1 -1
  28. data/app/views/good_job/batches/_table.erb +1 -1
  29. data/app/views/good_job/jobs/index.html.erb +1 -1
  30. data/app/views/good_job/performance/index.html.erb +3 -1
  31. data/app/views/good_job/performance/show.html.erb +5 -0
  32. data/app/views/good_job/shared/_filter.erb +2 -2
  33. data/app/views/layouts/good_job/application.html.erb +7 -7
  34. data/config/brakeman.ignore +75 -0
  35. data/config/locales/de.yml +52 -48
  36. data/config/locales/en.yml +4 -0
  37. data/config/locales/es.yml +16 -12
  38. data/config/locales/fr.yml +4 -0
  39. data/config/locales/it.yml +4 -0
  40. data/config/locales/ja.yml +4 -0
  41. data/config/locales/ko.yml +4 -0
  42. data/config/locales/nl.yml +4 -0
  43. data/config/locales/pt-BR.yml +4 -0
  44. data/config/locales/ru.yml +4 -0
  45. data/config/locales/tr.yml +4 -0
  46. data/config/locales/uk.yml +4 -0
  47. data/config/routes.rb +4 -4
  48. data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
  49. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  50. data/lib/good_job/adapter.rb +59 -53
  51. data/lib/good_job/capsule_tracker.rb +1 -1
  52. data/lib/good_job/configuration.rb +3 -4
  53. data/lib/good_job/cron_manager.rb +1 -3
  54. data/lib/good_job/current_thread.rb +4 -4
  55. data/lib/good_job/notifier.rb +7 -0
  56. data/lib/good_job/version.rb +1 -1
  57. data/lib/good_job.rb +6 -5
  58. metadata +10 -20
  59. data/app/models/good_job/base_execution.rb +0 -609
  60. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  61. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  62. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  63. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  64. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  65. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  66. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  67. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  68. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  69. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  70. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  71. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  72. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  73. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  74. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  75. data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -49,8 +49,8 @@ module GoodJob
49
49
 
50
50
  lock_condition = "#{function}(('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"
51
51
  query = cte_table.project(cte_table[:id])
52
- .with(composed_cte)
53
- .where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
52
+ .with(composed_cte)
53
+ .where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
54
54
 
55
55
  limit = original_query.arel.ast.limit
56
56
  query.limit = limit.value if limit.present?
@@ -174,8 +174,11 @@ module GoodJob
174
174
  if unlock_session
175
175
  advisory_unlock_session
176
176
  else
177
- records.each do |record|
178
- record.advisory_unlock(key: record.lockable_column_key(column: column), function: advisory_unlockable_function(function))
177
+ unlock_function = advisory_unlockable_function(function)
178
+ if unlock_function
179
+ records.each do |record|
180
+ record.advisory_unlock(key: record.lockable_column_key(column: column), function: unlock_function)
181
+ end
179
182
  end
180
183
  end
181
184
  end
@@ -209,7 +212,8 @@ module GoodJob
209
212
  begin
210
213
  yield
211
214
  ensure
212
- advisory_unlock_key(key, function: advisory_unlockable_function(function))
215
+ unlock_function = advisory_unlockable_function(function)
216
+ advisory_unlock_key(key, function: unlock_function) if unlock_function
213
217
  end
214
218
  end
215
219
 
@@ -220,6 +224,9 @@ module GoodJob
220
224
  # @param function [String, Symbol] Postgres Advisory Lock function name to use
221
225
  # @return [Boolean] whether the lock was released.
222
226
  def advisory_unlock_key(key, function: advisory_unlockable_function)
227
+ raise ArgumentError, "Cannot unlock transactional locks" if function.include? "_xact_"
228
+ raise ArgumentError, "No unlock function provide" if function.blank?
229
+
223
230
  query = <<~SQL.squish
224
231
  SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked
225
232
  SQL
@@ -284,6 +291,8 @@ module GoodJob
284
291
  # @param function [String, Symbol] name of advisory lock or unlock function
285
292
  # @return [Boolean]
286
293
  def advisory_unlockable_function(function = advisory_lockable_function)
294
+ return nil if function.include? "_xact_" # Cannot unlock transactional locks
295
+
287
296
  function.to_s.sub("_lock", "_unlock").sub("_try_", "_")
288
297
  end
289
298
 
@@ -358,7 +367,8 @@ module GoodJob
358
367
  begin
359
368
  yield
360
369
  ensure
361
- advisory_unlock(key: key, function: self.class.advisory_unlockable_function(function))
370
+ unlock_function = self.class.advisory_unlockable_function(function)
371
+ advisory_unlock(key: key, function: unlock_function) if unlock_function
362
372
  end
363
373
  end
364
374
 
@@ -403,7 +413,7 @@ module GoodJob
403
413
  # @param key [String, Symbol] Key to lock against
404
414
  # @param function [String, Symbol] Postgres Advisory Lock function name to use
405
415
  # @return [void]
406
- def advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function))
416
+ def advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_function)
407
417
  advisory_unlock(key: key, function: function) while advisory_locked?
408
418
  end
409
419
 
@@ -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
@@ -16,27 +16,23 @@ module GoodJob
16
16
  # @return [Symbol]
17
17
  def status
18
18
  if finished_at.present?
19
- if error.present? && retried_good_job_id.present?
20
- :retried
21
- elsif error.present? && retried_good_job_id.nil?
19
+ if error.present?
22
20
  :discarded
23
21
  else
24
22
  :succeeded
25
23
  end
26
- elsif (scheduled_at || created_at) > DateTime.current
27
- if serialized_params.fetch('executions', 0) > 1
28
- :retried
29
- else
30
- :scheduled
31
- end
32
- elsif running?
24
+ elsif performed_at.present?
33
25
  :running
34
- else
26
+ elsif (scheduled_at || created_at) <= DateTime.current
35
27
  :queued
28
+ elsif error.present?
29
+ :retried
30
+ else
31
+ :scheduled
36
32
  end
37
33
  end
38
34
 
39
- # The last relevant timestamp for this execution
35
+ # The last relevant timestamp for this job
40
36
  def last_status_at
41
37
  finished_at || performed_at || scheduled_at || created_at
42
38
  end
@@ -102,12 +102,17 @@ module GoodJob
102
102
  active_jobs = add(active_jobs, &block)
103
103
 
104
104
  Rails.application.executor.wrap do
105
- record.with_advisory_lock(function: "pg_advisory_lock") do
106
- record.update!(enqueued_at: Time.current)
107
-
108
- # During inline execution, this could enqueue and execute further jobs
109
- record._continue_discard_or_finish(lock: false)
105
+ buffer = GoodJob::Adapter::InlineBuffer.capture do
106
+ record.transaction do
107
+ record.with_advisory_lock(function: "pg_advisory_xact_lock") do
108
+ record.update!(enqueued_at: Time.current)
109
+
110
+ # During inline execution, this could enqueue and execute further jobs
111
+ record._continue_discard_or_finish(lock: false)
112
+ end
113
+ end
110
114
  end
115
+ buffer.call
111
116
  end
112
117
 
113
118
  active_jobs
@@ -53,22 +53,25 @@ module GoodJob
53
53
  end
54
54
 
55
55
  def _continue_discard_or_finish(execution = nil, lock: true)
56
- execution_discarded = execution && execution.error.present? && execution.finished_at && execution.retried_good_job_id.nil?
57
- take_advisory_lock(lock) do
58
- Batch.within_thread(batch_id: nil, batch_callback_id: id) do
59
- reload
60
- if execution_discarded && !discarded_at
61
- update(discarded_at: Time.current)
62
- on_discard.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard }) if on_discard.present?
63
- end
64
-
65
- if enqueued_at && !finished_at && jobs.where(finished_at: nil).count.zero?
66
- update(finished_at: Time.current)
67
- on_success.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success }) if !discarded_at && on_success.present?
68
- on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish }) if on_finish.present?
56
+ execution_discarded = execution && execution.finished_at.present? && execution.error.present?
57
+ buffer = GoodJob::Adapter::InlineBuffer.capture do
58
+ advisory_lock_maybe(lock) do
59
+ Batch.within_thread(batch_id: nil, batch_callback_id: id) do
60
+ reload
61
+ if execution_discarded && !discarded_at
62
+ update(discarded_at: Time.current)
63
+ on_discard.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard }) if on_discard.present?
64
+ end
65
+
66
+ if enqueued_at && !finished_at && jobs.where(finished_at: nil).count.zero?
67
+ update(finished_at: Time.current)
68
+ on_success.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success }) if !discarded_at && on_success.present?
69
+ on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish }) if on_finish.present?
70
+ end
69
71
  end
70
72
  end
71
73
  end
74
+ buffer.call
72
75
  end
73
76
 
74
77
  class PropertySerializer
@@ -100,9 +103,9 @@ module GoodJob
100
103
 
101
104
  private
102
105
 
103
- def take_advisory_lock(value, &block)
106
+ def advisory_lock_maybe(value, &block)
104
107
  if value
105
- with_advisory_lock(function: "pg_advisory_lock", &block)
108
+ transaction { with_advisory_lock(function: "pg_advisory_xact_lock", &block) }
106
109
  else
107
110
  yield
108
111
  end
@@ -1,64 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GoodJob # :nodoc:
4
- class DiscreteExecution < BaseRecord
5
- include ErrorEvents
6
-
7
- self.table_name = 'good_job_executions'
8
- self.implicit_order_column = 'created_at'
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
- belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
12
-
13
- scope :finished, -> { where.not(finished_at: nil) }
14
-
15
- alias_attribute :performed_at, :created_at
16
-
17
- # TODO: Remove when support for Rails 6.1 is dropped
18
- attribute :duration, :interval if ActiveJob.version.canonical_segments.take(2) == [6, 1]
19
-
20
- def number
21
- serialized_params.fetch('executions', 0) + 1
22
- end
23
-
24
- # Time between when this job was expected to run and when it started running
25
- def queue_latency
26
- created_at - scheduled_at
27
- end
28
-
29
- # Monotonic time between when this job started and finished
30
- def runtime_latency
31
- duration
32
- end
33
-
34
- def last_status_at
35
- finished_at || created_at
36
- end
37
-
38
- def status
39
- if finished_at.present?
40
- if error.present? && job.finished_at.present?
41
- :discarded
42
- elsif error.present?
43
- :retried
44
- else
45
- :succeeded
46
- end
47
- else
48
- :running
49
- end
50
- end
51
-
52
- def display_serialized_params
53
- serialized_params.merge({
54
- _good_job_execution: attributes.except('serialized_params'),
55
- })
56
- end
57
-
58
- def filtered_error_backtrace
59
- Rails.backtrace_cleaner.clean(error_backtrace || [])
60
- end
3
+ module GoodJob
4
+ # Deprecated, use +Execution+ instead.
5
+ class DiscreteExecution < Execution
61
6
  end
62
- end
63
7
 
64
- ActiveSupport.run_load_hooks(:good_job_execution, GoodJob::DiscreteExecution)
8
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
9
+ deprecate_constant :DiscreteExecution, 'Execution', deprecator: GoodJob.deprecator
10
+ end
@@ -1,8 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GoodJob
4
- # Created at the time a Job begins executing.
5
- # Behavior from +DiscreteExecution+ will be merged into this class.
6
- class Execution < DiscreteExecution
3
+ module GoodJob # :nodoc:
4
+ class Execution < BaseRecord
5
+ include ErrorEvents
6
+
7
+ self.table_name = 'good_job_executions'
8
+ self.implicit_order_column = 'created_at'
9
+
10
+ belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'id', inverse_of: :executions, optional: true
11
+
12
+ scope :finished, -> { where.not(finished_at: nil) }
13
+
14
+ alias_attribute :performed_at, :created_at
15
+
16
+ # TODO: Remove when support for Rails 6.1 is dropped
17
+ attribute :duration, :interval if ActiveJob.version.canonical_segments.take(2) == [6, 1]
18
+
19
+ def number
20
+ serialized_params.fetch('executions', 0) + 1
21
+ end
22
+
23
+ # Time between when this job was expected to run and when it started running
24
+ def queue_latency
25
+ created_at - scheduled_at
26
+ end
27
+
28
+ # Monotonic time between when this job started and finished
29
+ def runtime_latency
30
+ duration
31
+ end
32
+
33
+ def last_status_at
34
+ finished_at || created_at
35
+ end
36
+
37
+ def status
38
+ if finished_at.present?
39
+ if error.present? && job.finished_at.present?
40
+ :discarded
41
+ elsif error.present?
42
+ :retried
43
+ else
44
+ :succeeded
45
+ end
46
+ else
47
+ :running
48
+ end
49
+ end
50
+
51
+ def display_serialized_params
52
+ serialized_params.merge({
53
+ _good_job_execution: attributes.except('serialized_params'),
54
+ })
55
+ end
56
+
57
+ def filtered_error_backtrace
58
+ Rails.backtrace_cleaner.clean(error_backtrace || [])
59
+ end
7
60
  end
8
61
  end
62
+
63
+ ActiveSupport.run_load_hooks(:good_job_execution, GoodJob::Execution)
@@ -13,22 +13,22 @@ module GoodJob
13
13
  attr_reader :error_event
14
14
  # @return [Boolean, nil]
15
15
  attr_reader :unexecutable
16
- # @return [GoodJob::Execution, nil]
17
- attr_reader :retried
16
+ # @return [GoodJob::Job, nil]
17
+ attr_reader :retried_job
18
18
 
19
19
  # @param value [Object, nil]
20
20
  # @param handled_error [Exception, nil]
21
21
  # @param unhandled_error [Exception, nil]
22
22
  # @param error_event [String, nil]
23
23
  # @param unexecutable [Boolean, nil]
24
- # @param retried [Boolean, nil]
25
- def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried: nil)
24
+ # @param retried_job [GoodJob::Job, nil]
25
+ def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried_job: nil)
26
26
  @value = value
27
27
  @handled_error = handled_error
28
28
  @unhandled_error = unhandled_error
29
29
  @error_event = error_event
30
30
  @unexecutable = unexecutable
31
- @retried = retried
31
+ @retried_job = retried_job
32
32
  end
33
33
 
34
34
  # @return [Boolean]
@@ -38,7 +38,7 @@ module GoodJob
38
38
 
39
39
  # @return [Boolean]
40
40
  def retried?
41
- retried.present?
41
+ retried_job.present?
42
42
  end
43
43
  end
44
44
  end