good_job 1.2.6 → 1.3.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bb8c33f26176b048399e596b7c4c5dcb10553209b9237fe6256e7af66b1ffcc
4
- data.tar.gz: b401649bfe1dee5f83575e82cb2703407c2ddc367dabaabc05f41d9f79e4315d
3
+ metadata.gz: 7c7d64f21fbabba859d20d0a277d0a963672fcfde1284881b0130b1e05e62252
4
+ data.tar.gz: b7dbc95191a2bf115176f0e8ce09b8f7c997a2cb852fb673e5fee17c241434be
5
5
  SHA512:
6
- metadata.gz: d38c25e5f61a8d6509f323509ecd5e0fb49a31466b58a3eae47a5614b0e56f3b30a90725d8e57dc0d170a1cda635cb49850cc98d6b4c1c4033c8a65db691af8c
7
- data.tar.gz: 31add663e7307890b66f89e14b5ea164c9c67252c695db50f0d761921308019fcf7874984db7f263ae67557d1538f6b25d15271ca9880190b6faf13dc29346f8
6
+ metadata.gz: a0e58bf60a8008617703b575d55cdefd94fa9405d8d7cb034a75013b3a3fc8e4205833cf818d03caa137470b9ff431258d19e166e5d56f83a70667c3c484d904
7
+ data.tar.gz: adc2a494ea2f4e3e4ac7f569b013edade8a223f1e54474835cba34c6ac6233760f94bbbffb62f0ed37a119f8f15dfd1310a87b304483bbae72ecd0129f10f439
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.3.4](https://github.com/bensheldon/good_job/tree/v1.3.4) (2020-12-02)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.3...v1.3.4)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Fix job ordering. [\#174](https://github.com/bensheldon/good_job/pull/174) ([morgoth](https://github.com/morgoth))
10
+
11
+ ## [v1.3.3](https://github.com/bensheldon/good_job/tree/v1.3.3) (2020-12-01)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.2...v1.3.3)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - UI: Admin UI with filters and space efficient layout [\#173](https://github.com/bensheldon/good_job/pull/173) ([zealot128](https://github.com/zealot128))
18
+
19
+ ## [v1.3.2](https://github.com/bensheldon/good_job/tree/v1.3.2) (2020-11-12)
20
+
21
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.1...v1.3.2)
22
+
23
+ **Fixed bugs:**
24
+
25
+ - \(bug\) MultiScheduler polling bug [\#171](https://github.com/bensheldon/good_job/issues/171)
26
+ - MultiScheduler should delegate to all schedulers when state is nil [\#172](https://github.com/bensheldon/good_job/pull/172) ([bensheldon](https://github.com/bensheldon))
27
+
28
+ ## [v1.3.1](https://github.com/bensheldon/good_job/tree/v1.3.1) (2020-11-01)
29
+
30
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.0...v1.3.1)
31
+
32
+ **Implemented enhancements:**
33
+
34
+ - Extract polling from scheduler into Polling object [\#128](https://github.com/bensheldon/good_job/issues/128)
35
+ - Format serialized params to ease reading [\#170](https://github.com/bensheldon/good_job/pull/170) ([morgoth](https://github.com/morgoth))
36
+
37
+ **Fixed bugs:**
38
+
39
+ - Don't disconnect a nil activerecord connection [\#161](https://github.com/bensheldon/good_job/pull/161) ([bensheldon](https://github.com/bensheldon))
40
+
41
+ **Closed issues:**
42
+
43
+ - Propose addition of GoodJob to queue-shootout benchmarks [\#40](https://github.com/bensheldon/good_job/issues/40)
44
+
45
+ **Merged pull requests:**
46
+
47
+ - Ensure Rails is a development dependency [\#169](https://github.com/bensheldon/good_job/pull/169) ([bensheldon](https://github.com/bensheldon))
48
+ - Fix Ruby 2.7 GH action by setting default bundler explicitly [\#166](https://github.com/bensheldon/good_job/pull/166) ([bensheldon](https://github.com/bensheldon))
49
+ - Cache ruby version explicitly in Github Action [\#165](https://github.com/bensheldon/good_job/pull/165) ([bensheldon](https://github.com/bensheldon))
50
+ - Update development dependencies, rubocop [\#164](https://github.com/bensheldon/good_job/pull/164) ([bensheldon](https://github.com/bensheldon))
51
+ - Fix intended constant hierarchy of GoodJob::Scheduler::ThreadPoolExecutor [\#158](https://github.com/bensheldon/good_job/pull/158) ([bensheldon](https://github.com/bensheldon))
52
+ - Add bin/test\_app executable for Rails debugging [\#157](https://github.com/bensheldon/good_job/pull/157) ([bensheldon](https://github.com/bensheldon))
53
+ - Extract Scheduler polling behavior to its own object [\#152](https://github.com/bensheldon/good_job/pull/152) ([bensheldon](https://github.com/bensheldon))
54
+
55
+ ## [v1.3.0](https://github.com/bensheldon/good_job/tree/v1.3.0) (2020-10-03)
56
+
57
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.6...v1.3.0)
58
+
59
+ **Implemented enhancements:**
60
+
61
+ - Lengthen default poll interval from 1 to 5 seconds [\#156](https://github.com/bensheldon/good_job/pull/156) ([bensheldon](https://github.com/bensheldon))
62
+
63
+ **Merged pull requests:**
64
+
65
+ - Rename reperform\_jobs\_on\_standard\_error to retry\_on\_unhandled\_error [\#154](https://github.com/bensheldon/good_job/pull/154) ([morgoth](https://github.com/morgoth))
66
+
3
67
  ## [v1.2.6](https://github.com/bensheldon/good_job/tree/v1.2.6) (2020-09-29)
4
68
 
5
69
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.5...v1.2.6)
@@ -57,7 +121,6 @@
57
121
  - Correct example on how to configure multiple queues by command line. [\#135](https://github.com/bensheldon/good_job/pull/135) ([morgoth](https://github.com/morgoth))
58
122
  - Update ActionMailer Job class, to match the default [\#130](https://github.com/bensheldon/good_job/pull/130) ([morgoth](https://github.com/morgoth))
59
123
  - Add initial Engine scaffold [\#125](https://github.com/bensheldon/good_job/pull/125) ([bensheldon](https://github.com/bensheldon))
60
- - Zeitwerk Loader Implementation [\#123](https://github.com/bensheldon/good_job/pull/123) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
61
124
  - Update code-level documentation [\#111](https://github.com/bensheldon/good_job/pull/111) ([bensheldon](https://github.com/bensheldon))
62
125
 
63
126
  ## [v1.2.4](https://github.com/bensheldon/good_job/tree/v1.2.4) (2020-09-01)
@@ -81,6 +144,7 @@
81
144
 
82
145
  **Merged pull requests:**
83
146
 
147
+ - Zeitwerk Loader Implementation [\#123](https://github.com/bensheldon/good_job/pull/123) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
84
148
  - Remove unused PgLocks class [\#120](https://github.com/bensheldon/good_job/pull/120) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
85
149
  - Fix readme CommandLine option links [\#115](https://github.com/bensheldon/good_job/pull/115) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
86
150
  - Have YARD render markdown files with GFM \(Github Flavored Markdown\) [\#113](https://github.com/bensheldon/good_job/pull/113) ([bensheldon](https://github.com/bensheldon))
@@ -270,10 +334,6 @@
270
334
 
271
335
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.2...v0.9.0)
272
336
 
273
- **Merged pull requests:**
274
-
275
- - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
276
-
277
337
  ## [v0.8.2](https://github.com/bensheldon/good_job/tree/v0.8.2) (2020-07-18)
278
338
 
279
339
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.1...v0.8.2)
@@ -301,6 +361,7 @@
301
361
 
302
362
  **Merged pull requests:**
303
363
 
364
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
304
365
  - Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
305
366
 
306
367
  ## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-16)
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # GoodJob
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/good_job.svg)](https://rubygems.org/gems/good_job)
4
+ [![Test Status](https://github.com/bensheldon/good_job/workflows/Test/badge.svg)](https://github.com/bensheldon/good_job/actions)
5
+
3
6
  GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
4
7
 
5
8
  **Inspired by [Delayed::Job](https://github.com/collectiveidea/delayed_job) and [Que](https://github.com/que-rb/que), GoodJob is designed for maximum compatibility with Ruby on Rails, ActiveJob, and Postgres to be simple and performant for most workloads.**
@@ -210,7 +213,7 @@ Good Job’s general behavior can also be configured via several attributes dire
210
213
 
211
214
  - **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
212
215
  - **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
213
- - **`GoodJob.reperform_jobs_on_standard_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
216
+ - **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
214
217
  - **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
215
218
 
216
219
  You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
@@ -218,7 +221,7 @@ You’ll generally want to configure these in `config/initializers/good_job.rb`,
218
221
  ```ruby
219
222
  # config/initializers/good_job.rb
220
223
  GoodJob.preserve_job_records = true
221
- GoodJob.reperform_jobs_on_standard_error = false
224
+ GoodJob.retry_on_unhandled_error = false
222
225
  GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
223
226
  ```
224
227
 
@@ -301,7 +304,7 @@ When using `retry_on` with _a limited number of retries_, the final exception wi
301
304
 
302
305
  ```ruby
303
306
  # config/initializers/good_job.rb
304
- GoodJob.reperform_jobs_on_standard_error = false
307
+ GoodJob.retry_on_unhandled_error = false
305
308
  ```
306
309
 
307
310
  Alternatively, pass a block to `retry_on` to handle the final exception instead of raising it to GoodJob:
@@ -2,7 +2,7 @@ module GoodJob
2
2
  class ActiveJobsController < GoodJob::BaseController
3
3
  def show
4
4
  @jobs = GoodJob::Job.where("serialized_params ->> 'job_id' = ?", params[:id])
5
- .order('COALESCE(scheduled_at, created_at) DESC')
5
+ .order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
6
6
  end
7
7
  end
8
8
  end
@@ -1,10 +1,59 @@
1
1
  module GoodJob
2
2
  class DashboardsController < GoodJob::BaseController
3
- def index
4
- @jobs = GoodJob::Job.display_all(after_scheduled_at: params[:after_scheduled_at], after_id: params[:after_id])
3
+ class JobFilter
4
+ attr_accessor :params
5
+
6
+ def initialize(params)
7
+ @params = params
8
+ end
9
+
10
+ def last
11
+ @_last ||= jobs.last
12
+ end
13
+
14
+ def jobs
15
+ sql = GoodJob::Job.display_all(after_scheduled_at: params[:after_scheduled_at], after_id: params[:after_id])
5
16
  .limit(params.fetch(:limit, 10))
17
+ if params[:job_class] # rubocop:disable Style/IfUnlessModifier
18
+ sql = sql.where("serialized_params->>'job_class' = ?", params[:job_class])
19
+ end
20
+ if params[:state]
21
+ case params[:state]
22
+ when 'finished'
23
+ sql = sql.finished
24
+ when 'unfinished'
25
+ sql = sql.unfinished
26
+ when 'errors'
27
+ sql = sql.where.not(error: nil)
28
+ end
29
+ end
30
+ sql
31
+ end
32
+
33
+ def states
34
+ {
35
+ 'finished' => GoodJob::Job.finished.count,
36
+ 'unfinished' => GoodJob::Job.unfinished.count,
37
+ 'errors' => GoodJob::Job.where.not(error: nil).count,
38
+ }
39
+ end
40
+
41
+ def job_classes
42
+ GoodJob::Job.group("serialized_params->>'job_class'").count
43
+ end
44
+
45
+ def to_query(override)
46
+ {
47
+ state: params[:state],
48
+ job_class: params[:job_class],
49
+ }.merge(override).delete_if { |_, v| v.nil? }.to_query
50
+ end
51
+ end
52
+
53
+ def index
54
+ @filter = JobFilter.new(params)
6
55
 
7
- job_data = GoodJob::Job.connection.exec_query Arel.sql(<<~SQL)
56
+ job_data = GoodJob::Job.connection.exec_query Arel.sql(<<~SQL.squish)
8
57
  SELECT *
9
58
  FROM generate_series(
10
59
  date_trunc('hour', NOW() - '1 day'::interval),
@@ -2,13 +2,36 @@
2
2
  <%= render 'shared/chart', chart_data: @chart %>
3
3
  </div>
4
4
 
5
- <% if @jobs.present? %>
6
- <%= render 'shared/jobs_table', jobs: @jobs %>
5
+ <div class='card mb-2'>
6
+ <div class='card-body d-flex flex-wrap'>
7
+ <div class='mr-4'>
8
+ <small>Filter by job class</small>
9
+ <br>
10
+ <% @filter.job_classes.each do |name, count| %>
11
+ <a href='<%= request.path + "?#{@filter.to_query(job_class: name)}" %>' class='btn btn-sm btn-outline-secondary <%= "active" if params[:job_class] == name %>'>
12
+ <%= name %> (<%= count %>)
13
+ </a>
14
+ <% end %>
15
+ </div>
16
+ <div>
17
+ <small>Filter by state</small>
18
+ <br>
19
+ <% @filter.states.each do |name, count| %>
20
+ <a href='<%= request.path + "?#{@filter.to_query(state: name)}" %>' class='btn btn-sm btn-outline-secondary <%= "active" if params[:state] == name %>'>
21
+ <%= name %> (<%= count %>)
22
+ </a>
23
+ <% end %>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <% if @filter.jobs.present? %>
29
+ <%= render 'shared/jobs_table', jobs: @filter.jobs %>
7
30
 
8
31
  <nav aria-label="Job pagination">
9
32
  <ul class="pagination">
10
33
  <li class="page-item">
11
- <%= link_to({ after_scheduled_at: (@jobs.last.scheduled_at || @jobs.last.created_at), after_id: @jobs.last.id }, class: "page-link") do %>
34
+ <%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
12
35
  Next jobs <span aria-hidden="true">&raquo;</span>
13
36
  <% end %>
14
37
  </li>
@@ -18,7 +18,7 @@
18
18
  </head>
19
19
  <body>
20
20
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
21
- <div class="container">
21
+ <div class="container-fluid">
22
22
  <%= link_to "GoodJob 👍", root_path, class: 'navbar-brand mb-0 h1' %>
23
23
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
24
24
  <span class="navbar-toggler-icon"></span>
@@ -48,7 +48,7 @@
48
48
  </div>
49
49
  </nav>
50
50
 
51
- <div class="container">
51
+ <div class="container-fluid">
52
52
  <div class="card border-warning text-dark my-3">
53
53
  <div class="card-body">
54
54
  <p class="card-text">🚧 GoodJob's dashboard is a work in progress. Please contribute ideas and code on <a href="https://github.com/bensheldon/good_job/issues" target="_blank" rel="nofollow noopener noreferrer">Github</a>.</p>
@@ -1,13 +1,13 @@
1
1
  <div class="table-responsive">
2
- <table class="table table-bordered table-hover">
2
+ <table class="table table-bordered table-hover table-sm">
3
3
  <thead>
4
4
  <th>GoodJob ID</th>
5
5
  <th>ActiveJob ID</th>
6
6
  <th>Job Class</th>
7
7
  <th>Queue</th>
8
8
  <th>Scheduled At</th>
9
- <th>ActiveJob Params</th>
10
9
  <th>Error</th>
10
+ <th>ActiveJob Params</th>
11
11
  </thead>
12
12
  <tbody>
13
13
  <% jobs.each do |job| %>
@@ -17,8 +17,8 @@
17
17
  <td><%= job.serialized_params['job_class'] %></td>
18
18
  <td><%= job.queue_name %></td>
19
19
  <td><%= job.scheduled_at || job.created_at %></td>
20
- <td><%= job.serialized_params %></td>
21
20
  <td><%= job.error %></td>
21
+ <td><pre><%= JSON.pretty_generate(job.serialized_params) %></pre></td>
22
22
  </tr>
23
23
  <% end %>
24
24
  </tbody>
@@ -37,14 +37,30 @@ module GoodJob
37
37
  # @return [Boolean]
38
38
  mattr_accessor :preserve_job_records, default: false
39
39
 
40
- # @!attribute [rw] reperform_jobs_on_standard_error
40
+ # @!attribute [rw] retry_on_unhandled_error
41
41
  # @!scope class
42
42
  # Whether to re-perform a job when a type of +StandardError+ is raised to GoodJob (default: +true+).
43
43
  # If +true+, causes jobs to be re-queued and retried if they raise an instance of +StandardError+.
44
44
  # If +false+, jobs will be discarded or marked as finished if they raise an instance of +StandardError+.
45
45
  # Instances of +Exception+, like +SIGINT+, will *always* be retried, regardless of this attribute's value.
46
46
  # @return [Boolean]
47
- mattr_accessor :reperform_jobs_on_standard_error, default: true
47
+ mattr_accessor :retry_on_unhandled_error, default: true
48
+
49
+ # @deprecated Use {GoodJob#retry_on_unhandled_error} instead.
50
+ def self.reperform_jobs_on_standard_error
51
+ ActiveSupport::Deprecation.warn(
52
+ "Calling 'GoodJob.reperform_jobs_on_standard_error' is deprecated. Please use 'retry_on_unhandled_error'"
53
+ )
54
+ retry_on_unhandled_error
55
+ end
56
+
57
+ # @deprecated Use {GoodJob#retry_on_unhandled_error=} instead.
58
+ def self.reperform_jobs_on_standard_error=(value)
59
+ ActiveSupport::Deprecation.warn(
60
+ "Setting 'GoodJob.reperform_jobs_on_standard_error=' is deprecated. Please use 'retry_on_unhandled_error='"
61
+ )
62
+ self.retry_on_unhandled_error = value
63
+ end
48
64
 
49
65
  # @!attribute [rw] on_thread_error
50
66
  # @!scope class
@@ -43,8 +43,10 @@ module GoodJob
43
43
 
44
44
  if @execution_mode == :async # rubocop:disable Style/GuardClause
45
45
  @notifier = notifier || GoodJob::Notifier.new
46
+ @poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
46
47
  @scheduler = scheduler || GoodJob::Scheduler.from_configuration(configuration)
47
48
  @notifier.recipients << [@scheduler, :create_thread]
49
+ @poller.recipients << [@scheduler, :create_thread]
48
50
  end
49
51
  end
50
52
 
@@ -88,6 +90,7 @@ module GoodJob
88
90
  # @return [void]
89
91
  def shutdown(wait: true)
90
92
  @notifier&.shutdown(wait: wait)
93
+ @poller&.shutdown(wait: wait)
91
94
  @scheduler&.shutdown(wait: wait)
92
95
  end
93
96
 
@@ -42,14 +42,16 @@ module GoodJob
42
42
  method_option :poll_interval,
43
43
  type: :numeric,
44
44
  banner: 'SECONDS',
45
- desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)"
45
+ desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
46
46
  def start
47
47
  set_up_application!
48
+ configuration = GoodJob::Configuration.new(options)
48
49
 
49
50
  notifier = GoodJob::Notifier.new
50
- configuration = GoodJob::Configuration.new(options)
51
+ poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
51
52
  scheduler = GoodJob::Scheduler.from_configuration(configuration)
52
53
  notifier.recipients << [scheduler, :create_thread]
54
+ poller.recipients << [scheduler, :create_thread]
53
55
 
54
56
  @stop_good_job_executable = false
55
57
  %w[INT TERM].each do |signal|
@@ -62,6 +64,7 @@ module GoodJob
62
64
  end
63
65
 
64
66
  notifier.shutdown
67
+ poller.shutdown
65
68
  scheduler.shutdown
66
69
  end
67
70
 
@@ -8,7 +8,7 @@ module GoodJob
8
8
  # Default number of threads to use per {Scheduler}
9
9
  DEFAULT_MAX_THREADS = 5
10
10
  # Default number of seconds between polls for jobs
11
- DEFAULT_POLL_INTERVAL = 1
11
+ DEFAULT_POLL_INTERVAL = 5
12
12
  # Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
13
13
  DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
14
14
 
@@ -204,7 +204,7 @@ module GoodJob
204
204
 
205
205
  self.error = "#{job_error.class}: #{job_error.message}" if job_error
206
206
 
207
- if unhandled_error && GoodJob.reperform_jobs_on_standard_error
207
+ if unhandled_error && GoodJob.retry_on_unhandled_error
208
208
  save!
209
209
  elsif GoodJob.preserve_job_records == true || (unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
210
210
  self.finished_at = Time.current
@@ -56,7 +56,7 @@ module GoodJob
56
56
  # @example Get the records that have a session awaiting a lock:
57
57
  # MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
58
58
  scope :joins_advisory_locks, (lambda do
59
- join_sql = <<~SQL
59
+ join_sql = <<~SQL.squish
60
60
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
61
61
  AND pg_locks.objsubid = 1
62
62
  AND pg_locks.classid = ('x' || substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(32)::int
@@ -140,10 +140,10 @@ module GoodJob
140
140
  # all remaining locks).
141
141
  # @return [Boolean] whether the lock was acquired.
142
142
  def advisory_lock
143
- where_sql = <<~SQL
143
+ where_sql = <<~SQL.squish
144
144
  pg_try_advisory_lock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
145
145
  SQL
146
- self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
146
+ self.class.unscoped.exists?([where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }])
147
147
  end
148
148
 
149
149
  # Releases an advisory lock on this record if it is locked by this database
@@ -151,10 +151,10 @@ module GoodJob
151
151
  # {#advisory_unlock} and {#advisory_lock} the same number of times.
152
152
  # @return [Boolean] whether the lock was released.
153
153
  def advisory_unlock
154
- where_sql = <<~SQL
154
+ where_sql = <<~SQL.squish
155
155
  pg_advisory_unlock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
156
156
  SQL
157
- self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
157
+ self.class.unscoped.exists?([where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }])
158
158
  end
159
159
 
160
160
  # Acquires an advisory lock on this record or raises
@@ -191,13 +191,13 @@ module GoodJob
191
191
  # Tests whether this record has an advisory lock on it.
192
192
  # @return [Boolean]
193
193
  def advisory_locked?
194
- self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
194
+ self.class.unscoped.advisory_locked.exists?(id: send(self.class.primary_key))
195
195
  end
196
196
 
197
197
  # Tests whether this record is locked by the current database session.
198
198
  # @return [Boolean]
199
199
  def owns_advisory_lock?
200
- self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
200
+ self.class.unscoped.owns_advisory_locked.exists?(id: send(self.class.primary_key))
201
201
  end
202
202
 
203
203
  # Releases all advisory locks on the record that are held by the current
@@ -45,14 +45,13 @@ module GoodJob
45
45
  end
46
46
 
47
47
  # @macro notification_responder
48
- def scheduler_create_pools(event)
48
+ def scheduler_create_pool(event)
49
49
  max_threads = event.payload[:max_threads]
50
- poll_interval = event.payload[:poll_interval]
51
50
  performer_name = event.payload[:performer_name]
52
51
  process_id = event.payload[:process_id]
53
52
 
54
53
  info(tags: [process_id]) do
55
- "GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
54
+ "GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads}."
56
55
  end
57
56
  end
58
57
 
@@ -166,12 +165,12 @@ module GoodJob
166
165
  # @return [Logger]
167
166
  def logger
168
167
  @_logger ||= begin
169
- logger = Logger.new(StringIO.new)
170
- loggers.each do |each_logger|
171
- logger.extend(ActiveSupport::Logger.broadcast(each_logger))
172
- end
173
- logger
174
- end
168
+ logger = Logger.new(StringIO.new)
169
+ loggers.each do |each_logger|
170
+ logger.extend(ActiveSupport::Logger.broadcast(each_logger))
171
+ end
172
+ logger
173
+ end
175
174
  end
176
175
 
177
176
  # Reset {LogSubscriber.logger} and force it to rebuild a new shortcut to
@@ -192,11 +191,12 @@ module GoodJob
192
191
  # @return [void]
193
192
  def tag_logger(*tags, &block)
194
193
  tags = tags.dup.unshift("GoodJob").compact
194
+ good_job_tag = ["ActiveJob"].freeze
195
195
 
196
196
  self.class.loggers.inject(block) do |inner, each_logger|
197
197
  if each_logger.respond_to?(:tagged)
198
198
  tags_for_logger = if each_logger.formatter.current_tags.include?("ActiveJob")
199
- ["ActiveJob"] + tags
199
+ good_job_tag + tags
200
200
  else
201
201
  tags
202
202
  end
@@ -26,14 +26,23 @@ module GoodJob
26
26
  # Delegates to {Scheduler#create_thread}.
27
27
  def create_thread(state = nil)
28
28
  results = []
29
- any_true = schedulers.any? do |scheduler|
30
- scheduler.create_thread(state).tap { |result| results << result }
29
+
30
+ if state
31
+ schedulers.any? do |scheduler|
32
+ scheduler.create_thread(state).tap { |result| results << result }
33
+ end
34
+ else
35
+ schedulers.each do |scheduler|
36
+ results << scheduler.create_thread(state)
37
+ end
31
38
  end
32
39
 
33
- if any_true
40
+ if results.any?
34
41
  true
35
- else
36
- results.any? { |result| result == false } ? false : nil
42
+ elsif results.any? { |result| result == false }
43
+ false
44
+ else # rubocop:disable Style/EmptyElse
45
+ nil
37
46
  end
38
47
  end
39
48
  end
@@ -34,7 +34,7 @@ module GoodJob # :nodoc:
34
34
  # @param message [#to_json]
35
35
  def self.notify(message)
36
36
  connection = ActiveRecord::Base.connection
37
- connection.exec_query <<~SQL
37
+ connection.exec_query <<~SQL.squish
38
38
  NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
39
39
  SQL
40
40
  end
@@ -75,7 +75,7 @@ module GoodJob # :nodoc:
75
75
  # If +wait+ is +true+, the notifier will wait for background thread to shutdown.
76
76
  # If +wait+ is +false+, this method will return immediately even though threads may still be running.
77
77
  # Use {#shutdown?} to determine whether threads have stopped.
78
- # @param wait [Boolean] Wait for actively executing jobs to finish
78
+ # @param wait [Boolean] Wait for actively executing threads to finish
79
79
  # @return [void]
80
80
  def shutdown(wait: true)
81
81
  return unless @pool.running?
@@ -147,7 +147,7 @@ module GoodJob # :nodoc:
147
147
  pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}")
148
148
  yield pg_conn
149
149
  ensure
150
- ar_conn.disconnect!
150
+ ar_conn&.disconnect!
151
151
  end
152
152
  end
153
153
  end
@@ -0,0 +1,94 @@
1
+ require 'concurrent/atomic/atomic_boolean'
2
+
3
+ module GoodJob # :nodoc:
4
+ #
5
+ # Pollers regularly wake up execution threads to check for new work.
6
+ #
7
+ class Poller
8
+ # Defaults for instance of Concurrent::TimerTask.
9
+ # The timer controls how and when sleeping threads check for new work.
10
+ DEFAULT_TIMER_OPTIONS = {
11
+ execution_interval: Configuration::DEFAULT_POLL_INTERVAL,
12
+ timeout_interval: 1,
13
+ run_now: true,
14
+ }.freeze
15
+
16
+ # @!attribute [r] instances
17
+ # @!scope class
18
+ # List of all instantiated Pollers in the current process.
19
+ # @return [array<GoodJob:Poller>]
20
+ cattr_reader :instances, default: [], instance_reader: false
21
+
22
+ def self.from_configuration(configuration)
23
+ GoodJob::Poller.new(poll_interval: configuration.poll_interval)
24
+ end
25
+
26
+ # List of recipients that will receive notifications.
27
+ # @return [Array<#call, Array(Object, Symbol)>]
28
+ attr_reader :recipients
29
+
30
+ # @param recipients [Array<#call, Array(Object, Symbol)>]
31
+ # @param poll_interval [Hash] number of seconds between polls
32
+ def initialize(*recipients, poll_interval: nil)
33
+ @recipients = Concurrent::Array.new(recipients)
34
+
35
+ @timer_options = DEFAULT_TIMER_OPTIONS.dup
36
+ @timer_options[:execution_interval] = poll_interval if poll_interval.present?
37
+
38
+ self.class.instances << self
39
+
40
+ create_pool
41
+ end
42
+
43
+ # Shut down the poller.
44
+ # If +wait+ is +true+, the poller will wait for background thread to shutdown.
45
+ # If +wait+ is +false+, this method will return immediately even though threads may still be running.
46
+ # Use {#shutdown?} to determine whether threads have stopped.
47
+ # @param wait [Boolean] Wait for actively executing threads to finish
48
+ # @return [void]
49
+ def shutdown(wait: true)
50
+ return unless @timer&.running?
51
+
52
+ @timer.shutdown
53
+ @timer.wait_for_termination if wait
54
+ end
55
+
56
+ # Tests whether the poller is shutdown.
57
+ # @return [true, false, nil]
58
+ def shutdown?
59
+ !@timer&.running?
60
+ end
61
+
62
+ # Restart the poller.
63
+ # When shutdown, start; or shutdown and start.
64
+ # @param wait [Boolean] Wait for background thread to finish
65
+ # @return [void]
66
+ def restart(wait: true)
67
+ shutdown(wait: wait)
68
+ create_pool
69
+ end
70
+
71
+ # Invoked on completion of TimerTask task.
72
+ # @!visibility private
73
+ # @return [void]
74
+ def timer_observer(time, executed_task, thread_error)
75
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
76
+ instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
77
+ end
78
+
79
+ private
80
+
81
+ def create_pool
82
+ return if @timer_options[:execution_interval] <= 0
83
+
84
+ @timer = Concurrent::TimerTask.new(@timer_options) do
85
+ recipients.each do |recipient|
86
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
87
+ target.send(method_name)
88
+ end
89
+ end
90
+ @timer.add_observer(self, :timer_observer)
91
+ @timer.execute
92
+ end
93
+ end
94
+ end
@@ -26,14 +26,6 @@ module GoodJob # :nodoc:
26
26
  fallback_policy: :discard,
27
27
  }.freeze
28
28
 
29
- # Defaults for instance of Concurrent::TimerTask.
30
- # The timer controls how and when sleeping threads check for new work.
31
- DEFAULT_TIMER_OPTIONS = {
32
- execution_interval: Configuration::DEFAULT_POLL_INTERVAL,
33
- timeout_interval: 1,
34
- run_now: true,
35
- }.freeze
36
-
37
29
  # @!attribute [r] instances
38
30
  # @!scope class
39
31
  # List of all instantiated Schedulers in the current process.
@@ -41,7 +33,6 @@ module GoodJob # :nodoc:
41
33
  cattr_reader :instances, default: [], instance_reader: false
42
34
 
43
35
  # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
44
- # TODO: move this to GoodJob::Configuration
45
36
  # @param configuration [GoodJob::Configuration]
46
37
  # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
47
38
  def self.from_configuration(configuration)
@@ -53,7 +44,7 @@ module GoodJob # :nodoc:
53
44
  parsed = GoodJob::Job.queue_parser(queue_string)
54
45
  job_filter = proc do |state|
55
46
  if parsed[:exclude]
56
- !parsed[:exclude].include? state[:queue_name]
47
+ parsed[:exclude].exclude?(state[:queue_name])
57
48
  elsif parsed[:include]
58
49
  parsed[:include].include? state[:queue_name]
59
50
  else
@@ -62,7 +53,7 @@ module GoodJob # :nodoc:
62
53
  end
63
54
  job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
64
55
 
65
- GoodJob::Scheduler.new(job_performer, max_threads: max_threads, poll_interval: configuration.poll_interval)
56
+ GoodJob::Scheduler.new(job_performer, max_threads: max_threads)
66
57
  end
67
58
 
68
59
  if schedulers.size > 1
@@ -73,23 +64,19 @@ module GoodJob # :nodoc:
73
64
  end
74
65
 
75
66
  # @param performer [GoodJob::Performer]
76
- # @param max_threads [Numeric, nil] the number of execution threads to use
77
- # @param poll_interval [Numeric, nil] the number of seconds between polls for jobs
78
- def initialize(performer, max_threads: nil, poll_interval: nil)
67
+ # @param max_threads [Numeric, nil] number of seconds between polls for jobs
68
+ def initialize(performer, max_threads: nil)
79
69
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
80
70
 
81
71
  self.class.instances << self
82
72
 
83
73
  @performer = performer
84
74
 
85
- @timer_options = DEFAULT_TIMER_OPTIONS.dup
86
- @timer_options[:execution_interval] = poll_interval if poll_interval.present?
87
-
88
75
  @pool_options = DEFAULT_POOL_OPTIONS.dup
89
76
  @pool_options[:max_threads] = max_threads if max_threads.present?
90
- @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
77
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]})"
91
78
 
92
- create_pools
79
+ create_pool
93
80
  end
94
81
 
95
82
  # Shut down the scheduler.
@@ -100,28 +87,20 @@ module GoodJob # :nodoc:
100
87
  # @param wait [Boolean] Wait for actively executing jobs to finish
101
88
  # @return [void]
102
89
  def shutdown(wait: true)
103
- @_shutdown = true
90
+ return unless @pool&.running?
104
91
 
105
92
  instrument("scheduler_shutdown_start", { wait: wait })
106
93
  instrument("scheduler_shutdown", { wait: wait }) do
107
- if @timer&.running?
108
- @timer.shutdown
109
- @timer.wait_for_termination if wait
110
- # TODO: Should be killed if wait is not true
111
- end
112
-
113
- if @pool&.running?
114
- @pool.shutdown
115
- @pool.wait_for_termination if wait
116
- # TODO: Should be killed if wait is not true
117
- end
94
+ @pool.shutdown
95
+ @pool.wait_for_termination if wait
96
+ # TODO: Should be killed if wait is not true
118
97
  end
119
98
  end
120
99
 
121
100
  # Tests whether the scheduler is shutdown.
122
101
  # @return [true, false, nil]
123
102
  def shutdown?
124
- @_shutdown
103
+ !@pool&.running?
125
104
  end
126
105
 
127
106
  # Restart the Scheduler.
@@ -131,8 +110,7 @@ module GoodJob # :nodoc:
131
110
  def restart(wait: true)
132
111
  instrument("scheduler_restart_pools") do
133
112
  shutdown(wait: wait) unless shutdown?
134
- create_pools
135
- @_shutdown = false
113
+ create_pool
136
114
  end
137
115
  end
138
116
 
@@ -157,14 +135,6 @@ module GoodJob # :nodoc:
157
135
  true
158
136
  end
159
137
 
160
- # Invoked on completion of TimerTask task.
161
- # @!visibility private
162
- # @return [void]
163
- def timer_observer(time, executed_task, thread_error)
164
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
165
- instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
166
- end
167
-
168
138
  # Invoked on completion of ThreadPoolExecutor task
169
139
  # @!visibility private
170
140
  # @return [void]
@@ -176,14 +146,9 @@ module GoodJob # :nodoc:
176
146
 
177
147
  private
178
148
 
179
- def create_pools
180
- instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
149
+ def create_pool
150
+ instrument("scheduler_create_pool", { performer_name: @performer.name, max_threads: @pool_options[:max_threads] }) do
181
151
  @pool = ThreadPoolExecutor.new(@pool_options)
182
- next unless @timer_options[:execution_interval].positive?
183
-
184
- @timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
185
- @timer.add_observer(self, :timer_observer)
186
- @timer.execute
187
152
  end
188
153
  end
189
154
 
@@ -196,20 +161,20 @@ module GoodJob # :nodoc:
196
161
 
197
162
  ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
198
163
  end
199
- end
200
164
 
201
- # Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
202
- # @private
203
- class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
204
- # Number of inactive threads available to execute tasks.
205
- # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
206
- # @return [Integer]
207
- def ready_worker_count
208
- synchronize do
209
- workers_still_to_be_created = @max_length - @pool.length
210
- workers_created_but_waiting = @ready.length
211
-
212
- workers_still_to_be_created + workers_created_but_waiting
165
+ # Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
166
+ # @private
167
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
168
+ # Number of inactive threads available to execute tasks.
169
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
170
+ # @return [Integer]
171
+ def ready_worker_count
172
+ synchronize do
173
+ workers_still_to_be_created = @max_length - @pool.length
174
+ workers_created_but_waiting = @ready.length
175
+
176
+ workers_still_to_be_created + workers_created_but_waiting
177
+ end
213
178
  end
214
179
  end
215
180
  end
@@ -1,4 +1,4 @@
1
1
  module GoodJob
2
2
  # GoodJob gem version.
3
- VERSION = '1.2.6'.freeze
3
+ VERSION = '1.3.4'.freeze
4
4
  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: 1.2.6
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-29 00:00:00.000000000 Z
11
+ date: 2020-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -290,6 +290,20 @@ dependencies:
290
290
  - - ">="
291
291
  - !ruby/object:Gem::Version
292
292
  version: '0'
293
+ - !ruby/object:Gem::Dependency
294
+ name: rails
295
+ requirement: !ruby/object:Gem::Requirement
296
+ requirements:
297
+ - - ">="
298
+ - !ruby/object:Gem::Version
299
+ version: '0'
300
+ type: :development
301
+ prerelease: false
302
+ version_requirements: !ruby/object:Gem::Requirement
303
+ requirements:
304
+ - - ">="
305
+ - !ruby/object:Gem::Version
306
+ version: '0'
293
307
  - !ruby/object:Gem::Dependency
294
308
  name: rbtrace
295
309
  requirement: !ruby/object:Gem::Requirement
@@ -475,6 +489,7 @@ files:
475
489
  - lib/good_job/multi_scheduler.rb
476
490
  - lib/good_job/notifier.rb
477
491
  - lib/good_job/performer.rb
492
+ - lib/good_job/poller.rb
478
493
  - lib/good_job/railtie.rb
479
494
  - lib/good_job/scheduler.rb
480
495
  - lib/good_job/version.rb
@@ -510,7 +525,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
510
525
  - !ruby/object:Gem::Version
511
526
  version: '0'
512
527
  requirements: []
513
- rubygems_version: 3.0.3
528
+ rubygems_version: 3.1.4
514
529
  signing_key:
515
530
  specification_version: 4
516
531
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails