good_job 2.1.0 → 2.4.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +32 -0
  4. data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +2 -2
  5. data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +2 -2
  6. data/engine/app/controllers/good_job/cron_schedules_controller.rb +9 -0
  7. data/engine/app/controllers/good_job/executions_controller.rb +14 -0
  8. data/engine/app/controllers/good_job/jobs_controller.rb +8 -4
  9. data/engine/app/filters/good_job/base_filter.rb +101 -0
  10. data/engine/app/filters/good_job/executions_filter.rb +40 -0
  11. data/engine/app/filters/good_job/jobs_filter.rb +46 -0
  12. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  13. data/engine/app/models/good_job/active_job_job.rb +127 -0
  14. data/engine/app/views/good_job/cron_schedules/index.html.erb +50 -0
  15. data/engine/app/views/good_job/executions/index.html.erb +21 -0
  16. data/engine/app/views/good_job/jobs/index.html.erb +7 -0
  17. data/engine/app/views/good_job/jobs/show.html.erb +3 -0
  18. data/engine/app/views/good_job/shared/_executions_table.erb +56 -0
  19. data/engine/app/views/good_job/shared/_filter.erb +52 -0
  20. data/engine/app/views/good_job/shared/_jobs_table.erb +19 -11
  21. data/engine/app/views/layouts/good_job/base.html.erb +13 -4
  22. data/engine/config/routes.rb +4 -3
  23. data/lib/good_job/active_job_extensions/concurrency.rb +6 -6
  24. data/lib/good_job/adapter.rb +10 -10
  25. data/lib/good_job/cron_manager.rb +3 -3
  26. data/lib/good_job/{current_execution.rb → current_thread.rb} +8 -8
  27. data/lib/good_job/execution.rb +308 -0
  28. data/lib/good_job/job.rb +6 -294
  29. data/lib/good_job/job_performer.rb +2 -2
  30. data/lib/good_job/log_subscriber.rb +4 -4
  31. data/lib/good_job/notifier.rb +3 -3
  32. data/lib/good_job/railtie.rb +2 -2
  33. data/lib/good_job/scheduler.rb +3 -3
  34. data/lib/good_job/version.rb +1 -1
  35. data/lib/good_job.rb +2 -2
  36. metadata +16 -7
  37. data/engine/app/controllers/good_job/active_jobs_controller.rb +0 -9
  38. data/engine/app/controllers/good_job/dashboards_controller.rb +0 -106
  39. data/engine/app/views/good_job/active_jobs/show.html.erb +0 -1
  40. data/engine/app/views/good_job/dashboards/index.html.erb +0 -54
data/lib/good_job/job.rb CHANGED
@@ -1,299 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
- # ActiveRecord model that represents an +ActiveJob+ job.
4
- # Parent class can be configured with +GoodJob.active_record_parent_class+.
5
- # @!parse
6
- # class Job < ActiveRecord::Base; end
7
- class Job < Object.const_get(GoodJob.active_record_parent_class)
8
- include Lockable
9
-
10
- # Raised if something attempts to execute a previously completed Job again.
11
- PreviouslyPerformedError = Class.new(StandardError)
12
-
13
- # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
14
- DEFAULT_QUEUE_NAME = 'default'
15
- # ActiveJob jobs without a +priority+ attribute are given this priority.
16
- DEFAULT_PRIORITY = 0
17
-
18
- self.table_name = 'good_jobs'
19
- self.advisory_lockable_column = 'active_job_id'
20
-
21
- # Parse a string representing a group of queues into a more readable data
22
- # structure.
23
- # @param string [String] Queue string
24
- # @return [Hash]
25
- # How to match a given queue. It can have the following keys and values:
26
- # - +{ all: true }+ indicates that all queues match.
27
- # - +{ exclude: Array<String> }+ indicates the listed queue names should
28
- # not match.
29
- # - +{ include: Array<String> }+ indicates the listed queue names should
30
- # match.
31
- # @example
32
- # GoodJob::Job.queue_parser('-queue1,queue2')
33
- # => { exclude: [ 'queue1', 'queue2' ] }
34
- def self.queue_parser(string)
35
- string = string.presence || '*'
36
-
37
- if string.first == '-'
38
- exclude_queues = true
39
- string = string[1..-1]
40
- end
41
-
42
- queues = string.split(',').map(&:strip)
43
-
44
- if queues.include?('*')
45
- { all: true }
46
- elsif exclude_queues
47
- { exclude: queues }
48
- else
49
- { include: queues }
50
- end
51
- end
52
-
53
- # Get Jobs with given class name
54
- # @!method with_job_class
55
- # @!scope class
56
- # @param string [String]
57
- # Job class name
58
- # @return [ActiveRecord::Relation]
59
- scope :with_job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
60
-
61
- # Get Jobs that have not yet been completed.
62
- # @!method unfinished
63
- # @!scope class
64
- # @return [ActiveRecord::Relation]
65
- scope :unfinished, -> { where(finished_at: nil) }
66
-
67
- # Get Jobs that are not scheduled for a later time than now (i.e. jobs that
68
- # are not scheduled or scheduled for earlier than the current time).
69
- # @!method only_scheduled
70
- # @!scope class
71
- # @return [ActiveRecord::Relation]
72
- scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
73
-
74
- # Order jobs by priority (highest priority first).
75
- # @!method priority_ordered
76
- # @!scope class
77
- # @return [ActiveRecord::Relation]
78
- scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
79
-
80
- # Order jobs by scheduled (unscheduled or soonest first).
81
- # @!method schedule_ordered
82
- # @!scope class
83
- # @return [ActiveRecord::Relation]
84
- scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
85
-
86
- # Get Jobs were completed before the given timestamp. If no timestamp is
87
- # provided, get all jobs that have been completed. By default, GoodJob
88
- # deletes jobs after they are completed and this will find no jobs.
89
- # However, if you have changed {GoodJob.preserve_job_records}, this may
90
- # find completed Jobs.
91
- # @!method finished(timestamp = nil)
92
- # @!scope class
93
- # @param timestamp (Float)
94
- # Get jobs that finished before this time (in epoch time).
95
- # @return [ActiveRecord::Relation]
96
- scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
97
-
98
- # Get Jobs that started but not finished yet.
99
- # @!method running
100
- # @!scope class
101
- # @return [ActiveRecord::Relation]
102
- scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
103
-
104
- # Get Jobs that do not have subsequent retries
105
- # @!method running
106
- # @!scope class
107
- # @return [ActiveRecord::Relation]
108
- scope :head, -> { where(retried_good_job_id: nil) }
109
-
110
- # Get Jobs have errored that will not be retried further
111
- # @!method running
112
- # @!scope class
113
- # @return [ActiveRecord::Relation]
114
- scope :dead, -> { head.where.not(error: nil) }
115
-
116
- # Get Jobs on queues that match the given queue string.
117
- # @!method queue_string(string)
118
- # @!scope class
119
- # @param string [String]
120
- # A string expression describing what queues to select. See
121
- # {Job.queue_parser} or
122
- # {file:README.md#optimize-queues-threads-and-processes} for more details
123
- # on the format of the string. Note this only handles individual
124
- # semicolon-separated segments of that string format.
125
- # @return [ActiveRecord::Relation]
126
- scope :queue_string, (lambda do |string|
127
- parsed = queue_parser(string)
128
-
129
- if parsed[:all]
130
- all
131
- elsif parsed[:exclude]
132
- where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
133
- elsif parsed[:include]
134
- where(queue_name: parsed[:include])
135
- end
136
- end)
137
-
138
- # Get Jobs in display order with optional keyset pagination.
139
- # @!method display_all(after_scheduled_at: nil, after_id: nil)
140
- # @!scope class
141
- # @param after_scheduled_at [DateTime, String, nil]
142
- # Display records scheduled after this time for keyset pagination
143
- # @param after_id [Numeric, String, nil]
144
- # Display records after this ID for keyset pagination
145
- # @return [ActiveRecord::Relation]
146
- scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
147
- query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
148
- if after_scheduled_at.present? && after_id.present?
149
- query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
150
- elsif after_scheduled_at.present?
151
- query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
152
- end
153
- query
154
- end)
155
-
156
- # Finds the next eligible Job, acquire an advisory lock related to it, and
157
- # executes the job.
158
- # @return [ExecutionResult, nil]
159
- # If a job was executed, returns an array with the {Job} record, the
160
- # return value for the job's +#perform+ method, and the exception the job
161
- # raised, if any (if the job raised, then the second array entry will be
162
- # +nil+). If there were no jobs to execute, returns +nil+.
163
- def self.perform_with_advisory_lock
164
- unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |good_jobs|
165
- good_job = good_jobs.first
166
- break if good_job.blank?
167
- break :unlocked unless good_job&.executable?
168
-
169
- begin
170
- good_job.with_advisory_lock(key: "good_jobs-#{good_job.active_job_id}") do
171
- good_job.perform
172
- end
173
- rescue RecordAlreadyAdvisoryLockedError => e
174
- ExecutionResult.new(value: nil, handled_error: e)
175
- end
176
- end
177
- end
178
-
179
- # Fetches the scheduled execution time of the next eligible Job(s).
180
- # @param after [DateTime]
181
- # @param limit [Integer]
182
- # @param now_limit [Integer, nil]
183
- # @return [Array<DateTime>]
184
- def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
185
- query = advisory_unlocked.unfinished.schedule_ordered
186
-
187
- after ||= Time.current
188
- after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
189
- after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
190
-
191
- if now_limit&.positive?
192
- now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
193
- now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
194
- end
195
-
196
- Array(now_at) + after_at
197
- end
198
-
199
- # Places an ActiveJob job on a queue by creating a new {Job} record.
200
- # @param active_job [ActiveJob::Base]
201
- # The job to enqueue.
202
- # @param scheduled_at [Float]
203
- # Epoch timestamp when the job should be executed.
204
- # @param create_with_advisory_lock [Boolean]
205
- # Whether to establish a lock on the {Job} record after it is created.
206
- # @return [Job]
207
- # The new {Job} instance representing the queued ActiveJob job.
208
- def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
209
- ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
210
- good_job_args = {
211
- active_job_id: active_job.job_id,
212
- queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
213
- priority: active_job.priority || DEFAULT_PRIORITY,
214
- serialized_params: active_job.serialize,
215
- scheduled_at: scheduled_at,
216
- create_with_advisory_lock: create_with_advisory_lock,
217
- }
218
-
219
- good_job_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
220
-
221
- if CurrentExecution.cron_key
222
- good_job_args[:cron_key] = CurrentExecution.cron_key
223
- elsif CurrentExecution.active_job_id == active_job.job_id
224
- good_job_args[:cron_key] = CurrentExecution.good_job.cron_key
225
- end
226
-
227
- good_job = GoodJob::Job.new(**good_job_args)
228
-
229
- instrument_payload[:good_job] = good_job
230
-
231
- good_job.save!
232
- active_job.provider_job_id = good_job.id
233
-
234
- CurrentExecution.good_job.retried_good_job_id = good_job.id if CurrentExecution.good_job && CurrentExecution.good_job.active_job_id == active_job.job_id
235
-
236
- good_job
237
- end
238
- end
239
-
240
- # Execute the ActiveJob job this {Job} represents.
241
- # @return [ExecutionResult]
242
- # An array of the return value of the job's +#perform+ method and the
243
- # exception raised by the job, if any. If the job completed successfully,
244
- # the second array entry (the exception) will be +nil+ and vice versa.
245
- def perform
246
- raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
247
-
248
- self.performed_at = Time.current
249
- save! if GoodJob.preserve_job_records
250
-
251
- result = execute
252
-
253
- job_error = result.handled_error || result.unhandled_error
254
- self.error = "#{job_error.class}: #{job_error.message}" if job_error
255
-
256
- if result.unhandled_error && GoodJob.retry_on_unhandled_error
257
- save!
258
- elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
259
- self.finished_at = Time.current
260
- save!
261
- else
262
- destroy!
263
- end
264
-
265
- result
266
- end
267
-
268
- # Tests whether this job is safe to be executed by this thread.
269
- # @return [Boolean]
270
- def executable?
271
- self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
272
- end
273
-
274
- private
275
-
276
- # @return [ExecutionResult]
277
- def execute
278
- GoodJob::CurrentExecution.reset
279
- GoodJob::CurrentExecution.good_job = self
280
-
281
- job_data = serialized_params.deep_dup
282
- job_data["provider_job_id"] = id
283
-
284
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
285
- value = ActiveJob::Base.execute(job_data)
286
-
287
- if value.is_a?(Exception)
288
- handled_error = value
289
- value = nil
290
- end
291
- handled_error ||= GoodJob::CurrentExecution.error_on_retry || GoodJob::CurrentExecution.error_on_discard
292
-
293
- ExecutionResult.new(value: value, handled_error: handled_error)
294
- rescue StandardError => e
295
- ExecutionResult.new(value: nil, unhandled_error: e)
296
- end
3
+ # @deprecated Use {GoodJob::Execution} instead.
4
+ class Job < Execution
5
+ after_initialize do |_job|
6
+ ActiveSupport::Deprecation.warn(
7
+ "The `GoodJob::Job` class name is deprecated. Replace with `GoodJob::Execution`."
8
+ )
297
9
  end
298
10
  end
299
11
  end
@@ -14,8 +14,8 @@ module GoodJob
14
14
  def initialize(queue_string)
15
15
  @queue_string = queue_string
16
16
 
17
- @job_query = Concurrent::Delay.new { GoodJob::Job.queue_string(queue_string) }
18
- @parsed_queues = Concurrent::Delay.new { GoodJob::Job.queue_parser(queue_string) }
17
+ @job_query = Concurrent::Delay.new { GoodJob::Execution.queue_string(queue_string) }
18
+ @parsed_queues = Concurrent::Delay.new { GoodJob::Execution.queue_parser(queue_string) }
19
19
  end
20
20
 
21
21
  # A meaningful name to identify the performer in logs and for debugging.
@@ -19,10 +19,10 @@ module GoodJob
19
19
  # @return [void]
20
20
  def create(event)
21
21
  # FIXME: This method does not match any good_job notifications.
22
- good_job = event.payload[:good_job]
22
+ execution = event.payload[:execution]
23
23
 
24
24
  debug do
25
- "GoodJob created job resource with id #{good_job.id}"
25
+ "GoodJob created job resource with id #{execution.id}"
26
26
  end
27
27
  end
28
28
 
@@ -96,12 +96,12 @@ module GoodJob
96
96
 
97
97
  # @!macro notification_responder
98
98
  def perform_job(event)
99
- good_job = event.payload[:good_job]
99
+ execution = event.payload[:execution]
100
100
  process_id = event.payload[:process_id]
101
101
  thread_name = event.payload[:thread_name]
102
102
 
103
103
  info(tags: [process_id, thread_name]) do
104
- "Executed GoodJob #{good_job.id}"
104
+ "Executed GoodJob #{execution.id}"
105
105
  end
106
106
  end
107
107
 
@@ -39,7 +39,7 @@ module GoodJob # :nodoc:
39
39
  # Send a message via Postgres NOTIFY
40
40
  # @param message [#to_json]
41
41
  def self.notify(message)
42
- connection = Job.connection
42
+ connection = Execution.connection
43
43
  connection.exec_query <<~SQL.squish
44
44
  NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
45
45
  SQL
@@ -169,8 +169,8 @@ module GoodJob # :nodoc:
169
169
  end
170
170
 
171
171
  def with_listen_connection
172
- ar_conn = Job.connection_pool.checkout.tap do |conn|
173
- Job.connection_pool.remove(conn)
172
+ ar_conn = Execution.connection_pool.checkout.tap do |conn|
173
+ Execution.connection_pool.remove(conn)
174
174
  end
175
175
  pg_conn = ar_conn.raw_connection
176
176
  raise AdapterCannotListenError unless pg_conn.respond_to? :wait_for_notify
@@ -14,11 +14,11 @@ module GoodJob
14
14
 
15
15
  initializer "good_job.active_job_notifications" do
16
16
  ActiveSupport::Notifications.subscribe "enqueue_retry.active_job" do |event|
17
- GoodJob::CurrentExecution.error_on_retry = event.payload[:error]
17
+ GoodJob::CurrentThread.error_on_retry = event.payload[:error]
18
18
  end
19
19
 
20
20
  ActiveSupport::Notifications.subscribe "discard.active_job" do |event|
21
- GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
21
+ GoodJob::CurrentThread.error_on_discard = event.payload[:error]
22
22
  end
23
23
  end
24
24
 
@@ -230,7 +230,7 @@ module GoodJob # :nodoc:
230
230
  # @return [void]
231
231
  def create_task(delay = 0)
232
232
  future = Concurrent::ScheduledTask.new(delay, args: [performer], executor: executor, timer_set: timer_set) do |thr_performer|
233
- Rails.application.executor.wrap do
233
+ Rails.application.reloader.wrap do
234
234
  thr_performer.next
235
235
  end
236
236
  end
@@ -244,8 +244,8 @@ module GoodJob # :nodoc:
244
244
  def instrument(name, payload = {}, &block)
245
245
  payload = payload.reverse_merge({
246
246
  scheduler: self,
247
- process_id: GoodJob::CurrentExecution.process_id,
248
- thread_name: GoodJob::CurrentExecution.thread_name,
247
+ process_id: GoodJob::CurrentThread.process_id,
248
+ thread_name: GoodJob::CurrentThread.thread_name,
249
249
  })
250
250
 
251
251
  ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.1.0'
4
+ VERSION = '2.4.0'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -20,7 +20,7 @@ require "good_job/railtie"
20
20
  module GoodJob
21
21
  # @!attribute [rw] active_record_parent_class
22
22
  # @!scope class
23
- # The ActiveRecord parent class inherited by +GoodJob::Job+ (default: +ActiveRecord::Base+).
23
+ # The ActiveRecord parent class inherited by +GoodJob::Execution+ (default: +ActiveRecord::Base+).
24
24
  # Use this when using multiple databases or other custom ActiveRecord configuration.
25
25
  # @return [ActiveRecord::Base]
26
26
  # @example Change the base class:
@@ -129,7 +129,7 @@ module GoodJob
129
129
  timestamp = Time.current - older_than
130
130
 
131
131
  ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
132
- deleted_records_count = GoodJob::Job.finished(timestamp).delete_all
132
+ deleted_records_count = GoodJob::Execution.finished(timestamp).delete_all
133
133
 
134
134
  payload[:deleted_records_count] = deleted_records_count
135
135
  end
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: 2.1.0
4
+ version: 2.4.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: 2021-09-09 00:00:00.000000000 Z
11
+ date: 2021-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -351,15 +351,23 @@ files:
351
351
  - engine/app/assets/vendor/bootstrap/bootstrap.min.css
352
352
  - engine/app/assets/vendor/chartist/chartist.css
353
353
  - engine/app/assets/vendor/chartist/chartist.js
354
- - engine/app/controllers/good_job/active_jobs_controller.rb
355
354
  - engine/app/controllers/good_job/assets_controller.rb
356
355
  - engine/app/controllers/good_job/base_controller.rb
357
- - engine/app/controllers/good_job/dashboards_controller.rb
356
+ - engine/app/controllers/good_job/cron_schedules_controller.rb
357
+ - engine/app/controllers/good_job/executions_controller.rb
358
358
  - engine/app/controllers/good_job/jobs_controller.rb
359
+ - engine/app/filters/good_job/base_filter.rb
360
+ - engine/app/filters/good_job/executions_filter.rb
361
+ - engine/app/filters/good_job/jobs_filter.rb
359
362
  - engine/app/helpers/good_job/application_helper.rb
360
- - engine/app/views/good_job/active_jobs/show.html.erb
361
- - engine/app/views/good_job/dashboards/index.html.erb
363
+ - engine/app/models/good_job/active_job_job.rb
364
+ - engine/app/views/good_job/cron_schedules/index.html.erb
365
+ - engine/app/views/good_job/executions/index.html.erb
366
+ - engine/app/views/good_job/jobs/index.html.erb
367
+ - engine/app/views/good_job/jobs/show.html.erb
362
368
  - engine/app/views/good_job/shared/_chart.erb
369
+ - engine/app/views/good_job/shared/_executions_table.erb
370
+ - engine/app/views/good_job/shared/_filter.erb
363
371
  - engine/app/views/good_job/shared/_jobs_table.erb
364
372
  - engine/app/views/good_job/shared/icons/_check.html.erb
365
373
  - engine/app/views/good_job/shared/icons/_exclamation.html.erb
@@ -380,8 +388,9 @@ files:
380
388
  - lib/good_job/cli.rb
381
389
  - lib/good_job/configuration.rb
382
390
  - lib/good_job/cron_manager.rb
383
- - lib/good_job/current_execution.rb
391
+ - lib/good_job/current_thread.rb
384
392
  - lib/good_job/daemon.rb
393
+ - lib/good_job/execution.rb
385
394
  - lib/good_job/execution_result.rb
386
395
  - lib/good_job/job.rb
387
396
  - lib/good_job/job_performer.rb
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
- module GoodJob
3
- class ActiveJobsController < GoodJob::BaseController
4
- def show
5
- @jobs = GoodJob::Job.where("serialized_params ->> 'job_id' = ?", params[:id])
6
- .order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
7
- end
8
- end
9
- end
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
- module GoodJob
3
- class DashboardsController < GoodJob::BaseController
4
- class JobFilter
5
- attr_accessor :params
6
-
7
- def initialize(params)
8
- @params = params
9
- end
10
-
11
- def last
12
- @_last ||= jobs.last
13
- end
14
-
15
- def jobs
16
- after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
17
- sql = GoodJob::Job.display_all(after_scheduled_at: after_scheduled_at, after_id: params[:after_id])
18
- .limit(params.fetch(:limit, 25))
19
- sql = sql.with_job_class(params[:job_class]) if params[:job_class]
20
- if params[:state]
21
- case params[:state]
22
- when 'finished'
23
- sql = sql.finished
24
- when 'unfinished'
25
- sql = sql.unfinished
26
- when 'running'
27
- sql = sql.running
28
- when 'errors'
29
- sql = sql.where.not(error: nil)
30
- end
31
- end
32
- sql
33
- end
34
-
35
- def states
36
- {
37
- 'finished' => GoodJob::Job.finished.count,
38
- 'unfinished' => GoodJob::Job.unfinished.count,
39
- 'running' => GoodJob::Job.running.count,
40
- 'errors' => GoodJob::Job.where.not(error: nil).count,
41
- }
42
- end
43
-
44
- def job_classes
45
- GoodJob::Job.group("serialized_params->>'job_class'").count
46
- end
47
-
48
- def to_params(override)
49
- {
50
- state: params[:state],
51
- job_class: params[:job_class],
52
- }.merge(override).delete_if { |_, v| v.nil? }
53
- end
54
- end
55
-
56
- def index
57
- @filter = JobFilter.new(params)
58
-
59
- count_query = Arel.sql(GoodJob::Job.pg_or_jdbc_query(<<~SQL.squish))
60
- SELECT *
61
- FROM generate_series(
62
- date_trunc('hour', $1::timestamp),
63
- date_trunc('hour', $2::timestamp),
64
- '1 hour'
65
- ) timestamp
66
- LEFT JOIN (
67
- SELECT
68
- date_trunc('hour', scheduled_at) AS scheduled_at,
69
- queue_name,
70
- count(*) AS count
71
- FROM (
72
- SELECT
73
- COALESCE(scheduled_at, created_at)::timestamp AS scheduled_at,
74
- queue_name
75
- FROM good_jobs
76
- ) sources
77
- GROUP BY date_trunc('hour', scheduled_at), queue_name
78
- ) sources ON sources.scheduled_at = timestamp
79
- ORDER BY timestamp ASC
80
- SQL
81
-
82
- current_time = Time.current
83
- binds = [[nil, current_time - 1.day], [nil, current_time]]
84
- job_data = GoodJob::Job.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
85
-
86
- queue_names = job_data.map { |d| d['queue_name'] }.uniq
87
- labels = []
88
- queues_data = job_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
89
- labels << timestamp.in_time_zone.strftime('%H:%M %z')
90
- queue_names.each do |queue_name|
91
- (hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
92
- end
93
- end
94
-
95
- @chart = {
96
- labels: labels,
97
- series: queues_data.map do |queue, data|
98
- {
99
- name: queue,
100
- data: data,
101
- }
102
- end,
103
- }
104
- end
105
- end
106
- end
@@ -1 +0,0 @@
1
- <%= render 'good_job/shared/jobs_table', jobs: @jobs %>
@@ -1,54 +0,0 @@
1
- <div class="card my-3 p-6">
2
- <%= render 'good_job/shared/chart', chart_data: @chart %>
3
- </div>
4
-
5
- <div class='card mb-2'>
6
- <div class='card-body d-flex flex-wrap'>
7
- <div class='me-4'>
8
- <small>Filter by job class</small>
9
- <br>
10
- <% @filter.job_classes.each do |name, count| %>
11
- <% if params[:job_class] == name %>
12
- <%= link_to(root_path(@filter.to_params(job_class: nil)), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
13
- <%= name %> (<%= count %>)
14
- <% end %>
15
- <% else %>
16
- <%= link_to(root_path(@filter.to_params(job_class: name)), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
17
- <%= name %> (<%= count %>)
18
- <% end %>
19
- <% end %>
20
- <% end %>
21
- </div>
22
- <div>
23
- <small>Filter by state</small>
24
- <br>
25
- <% @filter.states.each do |name, count| %>
26
- <% if params[:state] == name %>
27
- <%= link_to(root_path(@filter.to_params(state: nil)), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
28
- <%= name %> (<%= count %>)
29
- <% end %>
30
- <% else %>
31
- <%= link_to(root_path(@filter.to_params(state: name)), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
32
- <%= name %> (<%= count %>)
33
- <% end %>
34
- <% end %>
35
- <% end %>
36
- </div>
37
- </div>
38
- </div>
39
-
40
- <% if @filter.jobs.present? %>
41
- <%= render 'good_job/shared/jobs_table', jobs: @filter.jobs %>
42
-
43
- <nav aria-label="Job pagination" class="mt-3">
44
- <ul class="pagination">
45
- <li class="page-item">
46
- <%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
47
- Next jobs <span aria-hidden="true">&raquo;</span>
48
- <% end %>
49
- </li>
50
- </ul>
51
- </nav>
52
- <% else %>
53
- <em>No jobs present.</em>
54
- <% end %>