good_job 3.0.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 381f27d5c5f8fe4be355f3b2ea1e1f0bcd76f545d7378183b4faca9d6770a6cd
4
- data.tar.gz: 20b315936f49810e9aa70e0bc8c2a37c140436212f2e84d15e05d275c93715ee
3
+ metadata.gz: 9ecbb75f2764a72686acb96367389abb1900321b5cf1292d6522aa8094c68451
4
+ data.tar.gz: 43a5305a9f2610c1544d6e52c61f0b338771b07d92d6493fed0058ad33bbc045
5
5
  SHA512:
6
- metadata.gz: f0f107ba18b85211856391873650309997a63e16d2ddc34e55bdbdb3f8563e595f2e3176cfaa1e13f65eaca70e75df0ff950562740d51c1a82bc885dfd3421ea
7
- data.tar.gz: cc47bc865ca1264238b5253f9ab06c8ebdc840e5adaf3220366f622626f8fac872c5fec5cd3b939acfd60137af6d984a0b2a61aad19ccd3ea277708f3c291414
6
+ metadata.gz: 0e637579c190a3f3d1aba6c02f6a71e2193d9e5c4bb449a3d4d9571093150a25a1c293ac3f5c541d2fae09aeffff771692fa7ef58bc8ed672f61a2c9d35a7e74
7
+ data.tar.gz: bc669b4d1628f7ff4b885787c65a38a57ad020aa168c3ae85f0c6bb26c9f0e19b64947682b0c99cf3eb28c2d660e1d79ee6095000a72340f09229d1679fa5dfe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.2.0](https://github.com/bensheldon/good_job/tree/v3.2.0) (2022-07-12)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.1.0...v3.2.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Ordered queue handling by workers [\#665](https://github.com/bensheldon/good_job/pull/665) ([jrochkind](https://github.com/jrochkind))
10
+
11
+ ## [v3.1.0](https://github.com/bensheldon/good_job/tree/v3.1.0) (2022-07-11)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.0.2...v3.1.0)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - Improve Dashboard display of parameters \(CronEntry kwargs; Process configuration; Job and Execution database values\) [\#662](https://github.com/bensheldon/good_job/pull/662) ([bensheldon](https://github.com/bensheldon))
18
+
19
+ **Fixed bugs:**
20
+
21
+ - Don't delegate `GoodJob::Job#status` to executions to avoid race condition [\#661](https://github.com/bensheldon/good_job/pull/661) ([bensheldon](https://github.com/bensheldon))
22
+
23
+ **Closed issues:**
24
+
25
+ - How to suppress repetitive logs in development? [\#658](https://github.com/bensheldon/good_job/issues/658)
26
+ - 500 Internal Server Error Exception in web interface trying to view running jobs [\#656](https://github.com/bensheldon/good_job/issues/656)
27
+ - Cron schedule page in dashboard not showing kwargs [\#608](https://github.com/bensheldon/good_job/issues/608)
28
+ - Paralelism x database connections [\#569](https://github.com/bensheldon/good_job/issues/569)
29
+
30
+ **Merged pull requests:**
31
+
32
+ - Show job/cron/process counts in the Navbar [\#663](https://github.com/bensheldon/good_job/pull/663) ([bensheldon](https://github.com/bensheldon))
33
+ - Dequeing should be first-in first-out [\#651](https://github.com/bensheldon/good_job/pull/651) ([jrochkind](https://github.com/jrochkind))
34
+
35
+ ## [v3.0.2](https://github.com/bensheldon/good_job/tree/v3.0.2) (2022-07-10)
36
+
37
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.0.1...v3.0.2)
38
+
39
+ **Fixed bugs:**
40
+
41
+ - Copy forward concurrency key value when retrying a job, rather than regenerating it [\#622](https://github.com/bensheldon/good_job/issues/622)
42
+ - All concurrency controlled jobs throw exceptions and are rescheduled if they are called using perform\_now [\#591](https://github.com/bensheldon/good_job/issues/591)
43
+
44
+ **Closed issues:**
45
+
46
+ - Queue config not respecting limits [\#659](https://github.com/bensheldon/good_job/issues/659)
47
+ - UI engine does not work without explicit require [\#646](https://github.com/bensheldon/good_job/issues/646)
48
+ - Should `:inline` adapter mode retry jobs? [\#611](https://github.com/bensheldon/good_job/issues/611)
49
+ - Error Job Not Preserved [\#594](https://github.com/bensheldon/good_job/issues/594)
50
+ - Jobs never get run... [\#516](https://github.com/bensheldon/good_job/issues/516)
51
+ - Release GoodJob 3.0 [\#507](https://github.com/bensheldon/good_job/issues/507)
52
+ - Improve security of Gem releases [\#422](https://github.com/bensheldon/good_job/issues/422)
53
+
54
+ **Merged pull requests:**
55
+
56
+ - Preserve initial concurrency key when retrying jobs [\#657](https://github.com/bensheldon/good_job/pull/657) ([bensheldon](https://github.com/bensheldon))
57
+ - Add Dashboard troubleshooting note to explicitly require the engine [\#654](https://github.com/bensheldon/good_job/pull/654) ([bensheldon](https://github.com/bensheldon))
58
+ - Removes wrong parentheses [\#653](https://github.com/bensheldon/good_job/pull/653) ([esasse](https://github.com/esasse))
59
+
3
60
  ## [v3.0.1](https://github.com/bensheldon/good_job/tree/v3.0.1) (2022-07-02)
4
61
 
5
62
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.0.0...v3.0.1)
data/README.md CHANGED
@@ -370,6 +370,16 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
370
370
 
371
371
  See more at [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
372
372
 
373
+ **Troubleshooting the Dashboard:** Some applications are unable to autoload the Goodjob Engine. To work around this, explicitly require the Engine at the top of your `config/application.rb` file, immediately after Rails is required and before Bundler requires the Rails' groups.
374
+
375
+ ```ruby
376
+ # config/application.rb
377
+ require_relative 'boot'
378
+ require 'rails/all'
379
+ require 'good_job/engine' # <= Add this line
380
+ # ...
381
+ ```
382
+
373
383
  #### API-only Rails applications
374
384
 
375
385
  API-only Rails applications may not have all of the required Rack middleware for the GoodJob Dashboard to function. To re-add the middlware:
@@ -655,7 +665,7 @@ By default, GoodJob creates a single thread execution pool that will execute job
655
665
 
656
666
  A pool is configured with the following syntax `<participating_queues>:<thread_count>`:
657
667
 
658
- - `<participating_queues>`: either `queue1,queue2` (only those queues), `*` (all) or `-queue1,queue2` (all except those queues).
668
+ - `<participating_queues>`: either `queue1,queue2` (only those queues), `+queue1,queue2` (only those queues, and processed in order), `*` (all) or `-queue1,queue2` (all except those queues).
659
669
  - `<thread_count>`: a count overriding for this specific pool the global `max-threads`.
660
670
 
661
671
  Pool configurations are separated with a semicolon (;) in the `queues` configuration
@@ -673,6 +683,12 @@ By default, GoodJob creates a single thread execution pool that will execute job
673
683
  - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.
674
684
  - `*`: execute jobs on any queue, with up to 5 threads (as configured by `--max-threads=5`).
675
685
 
686
+ When a pool is performing jobs from multiple queues, jobs will be performed from specified queues, ordered by priority and creation time. To perform jobs from queues in the queues' given order, use the `+` modifier. In this example, jobs in `batch_processing` will be performed only when there are no jobs in `transactional_messages`:
687
+
688
+ ```bash
689
+ bundle exec good_job --queues="+transactional_messages,batch_processing"
690
+ ```
691
+
676
692
  Configuration can be injected by environment variables too:
677
693
 
678
694
  ```bash
@@ -713,7 +729,7 @@ Each GoodJob execution thread requires its own database connection that is autom
713
729
 
714
730
  ```yaml
715
731
  # config/database.yml
716
- pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 3 + (ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
732
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 3 + ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
717
733
  ```
718
734
 
719
735
  To calculate the total number of the database connections you'll need:
@@ -56,7 +56,7 @@ module GoodJob
56
56
  end
57
57
 
58
58
  def show
59
- @job = Job.find(params[:id])
59
+ @job = Job.includes_advisory_locks.find(params[:id])
60
60
  end
61
61
 
62
62
  def discard
@@ -38,7 +38,7 @@
38
38
  </div>
39
39
  <% end %>
40
40
  <div>
41
- <%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse bg-light card card-body p-3 my-3" %>
41
+ <%= tag.pre JSON.pretty_generate(execution.display_serialized_params), id: dom_id(execution, "params"), class: "collapse bg-light card card-body p-3 my-3" %>
42
42
  </div>
43
43
  <% end %>
44
44
  <% end %>
@@ -13,7 +13,7 @@
13
13
  <th>Executions</th>
14
14
  <th>Error</th>
15
15
  <th>
16
- ActiveJob Params&nbsp;
16
+ Parameters&nbsp;
17
17
  <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
18
18
  data: { bs_toggle: "collapse", bs_target: ".job-params" },
19
19
  aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
@@ -71,7 +71,7 @@
71
71
  data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
72
72
  aria: { expanded: false, controls: dom_id(job, "params") }
73
73
  %>
74
- <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
74
+ <%= tag.pre JSON.pretty_generate(job.display_serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
75
75
  </td>
76
76
  <td>
77
77
  <div class="text-nowrap">
@@ -54,4 +54,4 @@
54
54
  <%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', ') %>
55
55
  </div>
56
56
 
57
- <%= render 'executions', executions: @job.executions.reverse %>
57
+ <%= render 'executions', executions: @job.executions.includes_advisory_locks.reverse %>
@@ -11,7 +11,7 @@
11
11
  <li class="nav-item">
12
12
  <%= link_to url_for({state: name}), class: "nav-link #{"active" if params[:state] == name}" do %>
13
13
  <%= t(name, scope: 'good_job.status') %>
14
- <span class="badge bg-primary rounded-pill <%= "bg-secondary" if count == 0 %>"><%= count %></span>
14
+ <span class="badge bg-primary rounded-pill <%= "bg-secondary" if count == 0 %>"><%= number_with_delimiter(count) %></span>
15
15
  <% end %>
16
16
  </li>
17
17
  <% end %>
@@ -8,13 +8,25 @@
8
8
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
9
9
  <ul class="navbar-nav me-auto">
10
10
  <li class="nav-item">
11
- <%= link_to t(".jobs"), jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] %>
11
+ <%= link_to jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] do %>
12
+ <%= t(".jobs") %>
13
+ <% jobs_count = GoodJob::Job.count %>
14
+ <span class="badge bg-secondary rounded-pill"><%= number_to_human(jobs_count) %></span>
15
+ <% end %>
12
16
  </li>
13
17
  <li class="nav-item">
14
- <%= link_to t(".cron_schedules"), cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] %>
18
+ <%= link_to cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] do %>
19
+ <%= t(".cron_schedules") %>
20
+ <% cron_entries_count = GoodJob::CronEntry.all.size %>
21
+ <span class="badge bg-secondary rounded-pill"><%= cron_entries_count %></span>
22
+ <% end %>
15
23
  </li>
16
24
  <li class="nav-item">
17
- <%= link_to t(".processes"), processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] %>
25
+ <%= link_to processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] do %>
26
+ <%= t(".processes") %>
27
+ <% processes_count = GoodJob::Process.count %>
28
+ <span class="badge bg-secondary rounded-pill <%= "bg-danger" if processes_count == 0 %>"><%= processes_count %></span>
29
+ <% end %>
18
30
  </li>
19
31
  </ul>
20
32
  <div class="nav-item pe-2">
@@ -63,3 +63,22 @@ en:
63
63
  retried: Retried
64
64
  running: Running
65
65
  scheduled: Scheduled
66
+ number:
67
+ format:
68
+ delimiter: ","
69
+ separator: "."
70
+ human:
71
+ decimal_units:
72
+ format: "%n%u"
73
+ units:
74
+ billion: B
75
+ hundred: ''
76
+ million: M
77
+ quadrillion: Q
78
+ thousand: K
79
+ trillion: T
80
+ unit: ''
81
+ format:
82
+ delimiter: ","
83
+ precision: 3
84
+ separator: "."
@@ -63,3 +63,22 @@ es:
63
63
  retried: reintentado
64
64
  running: Corriendo
65
65
  scheduled: Programado
66
+ number:
67
+ format:
68
+ delimiter: " "
69
+ separator: ","
70
+ human:
71
+ decimal_units:
72
+ format: "%n%u"
73
+ units:
74
+ billion: B
75
+ hundred: ''
76
+ million: M
77
+ quadrillion: q
78
+ thousand: k
79
+ trillion: T
80
+ unit: ''
81
+ format:
82
+ delimiter: " "
83
+ precision: 3
84
+ separator: ","
@@ -63,3 +63,22 @@ nl:
63
63
  retried: Opnieuw geprobeerd
64
64
  running: Rennen
65
65
  scheduled: Gepland
66
+ number:
67
+ format:
68
+ delimiter: "."
69
+ separator: ","
70
+ human:
71
+ decimal_units:
72
+ format: "%n%u"
73
+ units:
74
+ billion: B
75
+ hundred: ''
76
+ million: M
77
+ quadrillion: Q
78
+ thousand: K
79
+ trillion: T
80
+ unit: ''
81
+ format:
82
+ delimiter: "."
83
+ precision: 3
84
+ separator: ","
@@ -87,3 +87,22 @@ ru:
87
87
  retried: Повторная попытка
88
88
  running: Бег
89
89
  scheduled: по расписанию
90
+ number:
91
+ format:
92
+ delimiter: " "
93
+ separator: ","
94
+ human:
95
+ decimal_units:
96
+ format: "%n%u"
97
+ units:
98
+ billion: Б
99
+ hundred: ''
100
+ million: М
101
+ quadrillion: Q
102
+ thousand: К
103
+ trillion: Т
104
+ unit: ''
105
+ format:
106
+ delimiter: " "
107
+ precision: 3
108
+ separator: ","
@@ -10,8 +10,18 @@ module GoodJob
10
10
  end
11
11
  end
12
12
 
13
+ module Prepends
14
+ def deserialize(job_data)
15
+ super
16
+ self.good_job_concurrency_key = job_data['good_job_concurrency_key']
17
+ end
18
+ end
19
+
13
20
  included do
21
+ prepend Prepends
22
+
14
23
  class_attribute :good_job_concurrency_config, instance_accessor: false, default: {}
24
+ attr_writer :good_job_concurrency_key
15
25
 
16
26
  around_enqueue do |job, block|
17
27
  # Don't attempt to enforce concurrency limits with other queue adapters.
@@ -27,6 +37,8 @@ module GoodJob
27
37
  (total_limit.present? && (0...Float::INFINITY).cover?(total_limit))
28
38
  next(block.call) unless has_limit
29
39
 
40
+ # Only generate the concurrency key on the initial enqueue in case it is dynamic
41
+ job.good_job_concurrency_key ||= job._good_job_concurrency_key
30
42
  key = job.good_job_concurrency_key
31
43
  next(block.call) if key.blank?
32
44
 
@@ -80,7 +92,15 @@ module GoodJob
80
92
  end
81
93
  end
82
94
 
95
+ # Existing or dynamically generated concurrency key
96
+ # @return [Object] concurrency key
83
97
  def good_job_concurrency_key
98
+ @good_job_concurrency_key || _good_job_concurrency_key
99
+ end
100
+
101
+ # Generates the concurrency key from the configuration
102
+ # @return [Object] concurrency key
103
+ def _good_job_concurrency_key
84
104
  key = self.class.good_job_concurrency_config[:key]
85
105
  return if key.blank?
86
106
 
@@ -24,7 +24,7 @@ module GoodJob
24
24
  # Perform the next eligible job
25
25
  # @return [Object, nil] Returns job result or +nil+ if no job was found
26
26
  def next
27
- job_query.perform_with_advisory_lock
27
+ job_query.perform_with_advisory_lock(parsed_queues: parsed_queues)
28
28
  end
29
29
 
30
30
  # Tests whether this performer should be used in GoodJob's current state.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.0.1'
4
+ VERSION = '3.2.0'
5
5
  end
@@ -112,9 +112,11 @@ module GoodJob # :nodoc:
112
112
  class: job_class,
113
113
  cron: schedule,
114
114
  set: display_property(set),
115
- args: display_property(args),
116
115
  description: display_property(description),
117
- }
116
+ }.tap do |properties|
117
+ properties[:args] = display_property(args) if args.present?
118
+ properties[:kwargs] = display_property(kwargs) if kwargs.present?
119
+ end
118
120
  end
119
121
 
120
122
  private
@@ -29,15 +29,21 @@ module GoodJob
29
29
  # not match.
30
30
  # - +{ include: Array<String> }+ indicates the listed queue names should
31
31
  # match.
32
+ # - +{ include: Array<String>, ordered_queues: true }+ indicates the listed
33
+ # queue names should match, and dequeue should respect queue order.
32
34
  # @example
33
35
  # GoodJob::Execution.queue_parser('-queue1,queue2')
34
36
  # => { exclude: [ 'queue1', 'queue2' ] }
35
37
  def self.queue_parser(string)
36
38
  string = string.presence || '*'
37
39
 
38
- if string.first == '-'
40
+ case string.first
41
+ when '-'
39
42
  exclude_queues = true
40
43
  string = string[1..-1]
44
+ when '+'
45
+ ordered_queues = true
46
+ string = string[1..-1]
41
47
  end
42
48
 
43
49
  queues = string.split(',').map(&:strip)
@@ -46,6 +52,11 @@ module GoodJob
46
52
  { all: true }
47
53
  elsif exclude_queues
48
54
  { exclude: queues }
55
+ elsif ordered_queues
56
+ {
57
+ include: queues,
58
+ ordered_queues: true,
59
+ }
49
60
  else
50
61
  { include: queues }
51
62
  end
@@ -88,6 +99,42 @@ module GoodJob
88
99
  # @return [ActiveRecord::Relation]
89
100
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
90
101
 
102
+ # Order jobs by created_at, for first-in first-out
103
+ # @!method creation_ordered
104
+ # @!scope class
105
+ # @return [ActiveRecord:Relation]
106
+ scope :creation_ordered, -> { order('created_at ASC') }
107
+
108
+ # Order jobs for de-queueing
109
+ # @!method dequeueing_ordered
110
+ # @!scope class
111
+ # @param parsed_queues [Hash]
112
+ # optional output of .queue_parser, parsed queues, will be used for
113
+ # ordered queues.
114
+ # @return [ActiveRecord::Relation]
115
+ scope :dequeueing_ordered, (lambda do |parsed_queues|
116
+ relation = self
117
+ relation = relation.queue_ordered(parsed_queues[:include]) if parsed_queues && parsed_queues[:ordered_queues] && parsed_queues[:include]
118
+ relation = relation.priority_ordered.creation_ordered
119
+
120
+ relation
121
+ end)
122
+
123
+ # Order jobs in order of queues in array param
124
+ # @!method queue_ordered
125
+ # @!scope class
126
+ # @param queues [Array<string] ordered names of queues
127
+ # @return [ActiveRecord::Relation]
128
+ scope :queue_ordered, (lambda do |queues|
129
+ clauses = queues.map.with_index do |queue_name, index|
130
+ "WHEN queue_name = '#{queue_name}' THEN #{index}"
131
+ end
132
+
133
+ order(
134
+ Arel.sql("(CASE #{clauses.join(' ')} ELSE #{queues.length} END)")
135
+ )
136
+ end)
137
+
91
138
  # Order jobs by scheduled or created (oldest first).
92
139
  # @!method schedule_ordered
93
140
  # @!scope class
@@ -153,8 +200,8 @@ module GoodJob
153
200
  # return value for the job's +#perform+ method, and the exception the job
154
201
  # raised, if any (if the job raised, then the second array entry will be
155
202
  # +nil+). If there were no jobs to execute, returns +nil+.
156
- def self.perform_with_advisory_lock
157
- unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |executions|
203
+ def self.perform_with_advisory_lock(parsed_queues: nil)
204
+ unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(unlock_session: true) do |executions|
158
205
  execution = executions.first
159
206
  break if execution.blank?
160
207
  break :unlocked unless execution&.executable?
@@ -279,7 +326,9 @@ module GoodJob
279
326
  # @return [Symbol]
280
327
  def status
281
328
  if finished_at.present?
282
- if error.present?
329
+ if error.present? && retried_good_job_id.present?
330
+ :retried
331
+ elsif error.present? && retried_good_job_id.nil?
283
332
  :discarded
284
333
  else
285
334
  :finished
@@ -297,8 +346,20 @@ module GoodJob
297
346
  end
298
347
  end
299
348
 
349
+ # Return formatted serialized_params for display in the dashboard
350
+ # @return [Hash]
351
+ def display_serialized_params
352
+ serialized_params.merge({
353
+ _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
354
+ })
355
+ end
356
+
300
357
  def running?
301
- performed_at? && !finished_at?
358
+ if has_attribute?(:locktype)
359
+ self['locktype'].present?
360
+ else
361
+ advisory_locked?
362
+ end
302
363
  end
303
364
 
304
365
  def number
@@ -314,7 +375,7 @@ module GoodJob
314
375
  def queue_latency
315
376
  now = Time.zone.now
316
377
  expected_start = scheduled_at || created_at
317
- actual_start = performed_at || now
378
+ actual_start = performed_at || finished_at || now
318
379
 
319
380
  actual_start - expected_start unless expected_start >= now
320
381
  end
@@ -330,6 +391,7 @@ module GoodJob
330
391
  serialized_params.deep_dup
331
392
  .tap do |job_data|
332
393
  job_data["provider_job_id"] = id
394
+ job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
333
395
  end
334
396
  end
335
397
 
@@ -3,7 +3,8 @@ module GoodJob
3
3
  # ActiveRecord model that represents an +ActiveJob+ job.
4
4
  # There is not a table in the database whose discrete rows represents "Jobs".
5
5
  # The +good_jobs+ table is a table of individual {GoodJob::Execution}s that share the same +active_job_id+.
6
- # A single row from the +good_jobs+ table of executions is fetched to represent an Job
6
+ # A single row from the +good_jobs+ table of executions is fetched to represent a Job.
7
+ #
7
8
  class Job < BaseRecord
8
9
  include Filterable
9
10
  include Lockable
@@ -72,9 +73,51 @@ module GoodJob
72
73
  serialized_params['job_class']
73
74
  end
74
75
 
75
- # The status of the Job, based on the state of its most recent execution.
76
- # @return [Symbol]
77
- delegate :status, :last_status_at, to: :head_execution
76
+ def last_status_at
77
+ finished_at || performed_at || scheduled_at || created_at
78
+ end
79
+
80
+ def status
81
+ if finished_at.present?
82
+ if error.present? && retried_good_job_id.present?
83
+ :retried
84
+ elsif error.present? && retried_good_job_id.nil?
85
+ :discarded
86
+ else
87
+ :finished
88
+ end
89
+ elsif (scheduled_at || created_at) > DateTime.current
90
+ if serialized_params.fetch('executions', 0) > 1
91
+ :retried
92
+ else
93
+ :scheduled
94
+ end
95
+ elsif running?
96
+ :running
97
+ else
98
+ :queued
99
+ end
100
+ end
101
+
102
+ # Override #reload to add a custom scope to ensure the reloaded record is the head execution
103
+ # @return [Job]
104
+ def reload(options = nil)
105
+ self.class.connection.clear_query_cache
106
+
107
+ # override with the `where(retried_good_job_id: nil)` scope
108
+ override_query = self.class.where(retried_good_job_id: nil)
109
+ fresh_object =
110
+ if options && options[:lock]
111
+ self.class.unscoped { override_query.lock(options[:lock]).find(id) }
112
+ else
113
+ self.class.unscoped { override_query.find(id) }
114
+ end
115
+
116
+ @attributes = fresh_object.instance_variable_get(:@attributes)
117
+ @new_record = false
118
+ @previously_new_record = false
119
+ self
120
+ end
78
121
 
79
122
  # This job's most recent {Execution}
80
123
  # @param reload [Booelan] whether to reload executions
@@ -94,7 +137,7 @@ module GoodJob
94
137
  # The number of times this job has been executed, according to ActiveJob's serialized state.
95
138
  # @return [Numeric]
96
139
  def executions_count
97
- aj_count = head_execution.serialized_params.fetch('executions', 0)
140
+ aj_count = serialized_params.fetch('executions', 0)
98
141
  # The execution count within serialized_params is not updated
99
142
  # once the underlying execution has been executed.
100
143
  if status.in? [:discarded, :finished, :running]
@@ -114,7 +157,15 @@ module GoodJob
114
157
  # If the job has been retried, the error will be fetched from the previous {Execution} record.
115
158
  # @return [String]
116
159
  def recent_error
117
- head_execution.error || executions[-2]&.error
160
+ error || executions[-2]&.error
161
+ end
162
+
163
+ # Return formatted serialized_params for display in the dashboard
164
+ # @return [Hash]
165
+ def display_serialized_params
166
+ serialized_params.merge({
167
+ _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
168
+ })
118
169
  end
119
170
 
120
171
  # Tests whether the job is being executed right now.
@@ -192,7 +243,6 @@ module GoodJob
192
243
 
193
244
  raise ActionForStateMismatchError if execution.finished_at.present?
194
245
 
195
- execution = head_execution(reload: true)
196
246
  execution.update(scheduled_at: scheduled_at)
197
247
  end
198
248
  end
@@ -45,6 +45,8 @@ module GoodJob # :nodoc:
45
45
  hostname: Socket.gethostname,
46
46
  pid: ::Process.pid,
47
47
  proctitle: $PROGRAM_NAME,
48
+ preserve_job_records: GoodJob.preserve_job_records,
49
+ retry_on_unhandled_error: GoodJob.retry_on_unhandled_error,
48
50
  schedulers: GoodJob::Scheduler.instances.map(&:name),
49
51
  }
50
52
  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: 3.0.1
4
+ version: 3.2.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: 2022-07-02 00:00:00.000000000 Z
11
+ date: 2022-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob