good_job 1.3.0 → 1.3.5

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: 17fac9485f08bf55c8d2b2fbbc6e85efde95202d759d6155a4b23bf0755236ce
4
- data.tar.gz: 562115863243ac98e3fd0cca7c04ad9f57a80f932682d7161e43336b521ea62d
3
+ metadata.gz: 46b04fbfe44919834c406101ee18f2b53f5cc05e076525f7243e346fa966ca94
4
+ data.tar.gz: 798738a1f2b0c7f21457921c2c640cdc78151a09262dfa7d8a383e8778282b9d
5
5
  SHA512:
6
- metadata.gz: a06ee5d2bea077e7b612f6d44aa30666d52517849e5345d8882a9f21653513be2386e577dbbab8ad41897aeb48d14885cbfe8cd51784c96da300aee7233a5190
7
- data.tar.gz: 7c366b21a4f28d66ce8d59d22fa56a081a63c181ee0b35ddd7965422c7c758f020bc7fba42aa34e61ec8abb17e9dcc5a9d0cb3b15f2d1c03d7153d83d7fbffdd
6
+ metadata.gz: 841c85e7b4d9fa28d1fcdbb7c6a48771438f24ae33a264a80afdff1c9730a3438b98aa5d82f54bd1d4c979b2dab64997fbf6d64036bc96fdac6c851f70e0f68f
7
+ data.tar.gz: 9c2d11c56e3db986aa5bbecc85a317396a19ba1c838b97d32cd16f21b9cd29b6c154be817879d8c8a8925da645d298442c0ebc39aba3d5bba707ed01558b55c5
@@ -1,12 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.3.5](https://github.com/bensheldon/good_job/tree/v1.3.5) (2020-12-17)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.4...v1.3.5)
6
+
7
+ **Closed issues:**
8
+
9
+ - not running jobs [\#168](https://github.com/bensheldon/good_job/issues/168)
10
+ - how to run good\_job on a separate machine [\#162](https://github.com/bensheldon/good_job/issues/162)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Ensure advisory lock CTE is MATERIALIZED on Postgres v12+ [\#179](https://github.com/bensheldon/good_job/pull/179) ([bensheldon](https://github.com/bensheldon))
15
+ - Ensure that deleted jobs are unlocked [\#178](https://github.com/bensheldon/good_job/pull/178) ([bensheldon](https://github.com/bensheldon))
16
+ - Add Appraisal for Rails 6.1-rc2 [\#175](https://github.com/bensheldon/good_job/pull/175) ([bensheldon](https://github.com/bensheldon))
17
+
18
+ ## [v1.3.4](https://github.com/bensheldon/good_job/tree/v1.3.4) (2020-12-02)
19
+
20
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.3...v1.3.4)
21
+
22
+ **Merged pull requests:**
23
+
24
+ - Fix job ordering for Rails 6.1 [\#174](https://github.com/bensheldon/good_job/pull/174) ([morgoth](https://github.com/morgoth))
25
+
26
+ ## [v1.3.3](https://github.com/bensheldon/good_job/tree/v1.3.3) (2020-12-01)
27
+
28
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.2...v1.3.3)
29
+
30
+ **Merged pull requests:**
31
+
32
+ - UI: Admin UI with filters and space efficient layout [\#173](https://github.com/bensheldon/good_job/pull/173) ([zealot128](https://github.com/zealot128))
33
+
34
+ ## [v1.3.2](https://github.com/bensheldon/good_job/tree/v1.3.2) (2020-11-12)
35
+
36
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.1...v1.3.2)
37
+
38
+ **Fixed bugs:**
39
+
40
+ - \(bug\) MultiScheduler polling bug [\#171](https://github.com/bensheldon/good_job/issues/171)
41
+ - MultiScheduler should delegate to all schedulers when state is nil [\#172](https://github.com/bensheldon/good_job/pull/172) ([bensheldon](https://github.com/bensheldon))
42
+
43
+ ## [v1.3.1](https://github.com/bensheldon/good_job/tree/v1.3.1) (2020-11-01)
44
+
45
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.0...v1.3.1)
46
+
47
+ **Implemented enhancements:**
48
+
49
+ - Extract polling from scheduler into Polling object [\#128](https://github.com/bensheldon/good_job/issues/128)
50
+ - Format serialized params to ease reading [\#170](https://github.com/bensheldon/good_job/pull/170) ([morgoth](https://github.com/morgoth))
51
+
52
+ **Fixed bugs:**
53
+
54
+ - Don't disconnect a nil activerecord connection [\#161](https://github.com/bensheldon/good_job/pull/161) ([bensheldon](https://github.com/bensheldon))
55
+
56
+ **Closed issues:**
57
+
58
+ - Propose addition of GoodJob to queue-shootout benchmarks [\#40](https://github.com/bensheldon/good_job/issues/40)
59
+
60
+ **Merged pull requests:**
61
+
62
+ - Ensure Rails is a development dependency [\#169](https://github.com/bensheldon/good_job/pull/169) ([bensheldon](https://github.com/bensheldon))
63
+ - 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))
64
+ - Cache ruby version explicitly in Github Action [\#165](https://github.com/bensheldon/good_job/pull/165) ([bensheldon](https://github.com/bensheldon))
65
+ - Update development dependencies, rubocop [\#164](https://github.com/bensheldon/good_job/pull/164) ([bensheldon](https://github.com/bensheldon))
66
+ - Fix intended constant hierarchy of GoodJob::Scheduler::ThreadPoolExecutor [\#158](https://github.com/bensheldon/good_job/pull/158) ([bensheldon](https://github.com/bensheldon))
67
+ - Add bin/test\_app executable for Rails debugging [\#157](https://github.com/bensheldon/good_job/pull/157) ([bensheldon](https://github.com/bensheldon))
68
+ - Extract Scheduler polling behavior to its own object [\#152](https://github.com/bensheldon/good_job/pull/152) ([bensheldon](https://github.com/bensheldon))
69
+
3
70
  ## [v1.3.0](https://github.com/bensheldon/good_job/tree/v1.3.0) (2020-10-03)
4
71
 
5
72
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.6...v1.3.0)
6
73
 
7
- **Merged pull requests:**
74
+ **Implemented enhancements:**
8
75
 
9
76
  - Lengthen default poll interval from 1 to 5 seconds [\#156](https://github.com/bensheldon/good_job/pull/156) ([bensheldon](https://github.com/bensheldon))
77
+
78
+ **Merged pull requests:**
79
+
10
80
  - 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))
11
81
 
12
82
  ## [v1.2.6](https://github.com/bensheldon/good_job/tree/v1.2.6) (2020-09-29)
@@ -358,7 +428,6 @@
358
428
  **Merged pull requests:**
359
429
 
360
430
  - Improve ActiveRecord usage for advisory locking [\#24](https://github.com/bensheldon/good_job/pull/24) ([bensheldon](https://github.com/bensheldon))
361
- - Remove support for Rails 5.1 [\#23](https://github.com/bensheldon/good_job/pull/23) ([bensheldon](https://github.com/bensheldon))
362
431
 
363
432
  ## [v0.3.0](https://github.com/bensheldon/good_job/tree/v0.3.0) (2020-03-22)
364
433
 
@@ -377,6 +446,7 @@
377
446
 
378
447
  **Merged pull requests:**
379
448
 
449
+ - Remove support for Rails 5.1 [\#23](https://github.com/bensheldon/good_job/pull/23) ([bensheldon](https://github.com/bensheldon))
380
450
  - Gracefully shutdown Scheduler when executable receives TERM or INT [\#17](https://github.com/bensheldon/good_job/pull/17) ([bensheldon](https://github.com/bensheldon))
381
451
  - Update Appraisals [\#16](https://github.com/bensheldon/good_job/pull/16) ([bensheldon](https://github.com/bensheldon))
382
452
 
@@ -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>
@@ -1,24 +1,24 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Good job</title>
4
+ <title>Good Job Dashboard</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
8
8
  <style>
9
- <%= render "vendor/bootstrap/bootstrap.css" %>
10
- <%= render "vendor/chartist/chartist.css" %>
11
- <%= render "assets/style.css" %>
9
+ <%= render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.css") %>
10
+ <%= render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.css") %>
11
+ <%= render file: GoodJob::Engine.root.join("app", "assets", "style.css") %>
12
12
  </style>
13
13
 
14
14
  <script>
15
- <%= render "vendor/bootstrap/bootstrap-native.js" %>
16
- <%= render "vendor/chartist/chartist.js" %>
15
+ <%= render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap-native.js") %>
16
+ <%= render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.js") %>
17
17
  </script>
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>
@@ -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
 
@@ -45,11 +45,13 @@ module GoodJob
45
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
 
@@ -32,11 +32,18 @@ module GoodJob
32
32
  original_query = self
33
33
 
34
34
  cte_table = Arel::Table.new(:rows)
35
- composed_cte = Arel::Nodes::As.new(cte_table, original_query.select(primary_key).except(:limit).arel)
35
+ cte_query = original_query.select(primary_key).except(:limit)
36
+ cte_type = if supports_cte_materialization_specifiers?
37
+ 'MATERIALIZED'
38
+ else
39
+ ''
40
+ end
41
+
42
+ composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::SqlLiteral.new([cte_type, "(", cte_query.to_sql, ")"].join(' ')))
36
43
 
37
44
  query = cte_table.project(cte_table[:id])
38
- .with(composed_cte)
39
- .where(Arel.sql(sanitize_sql_for_conditions(["pg_try_advisory_lock(('x' || substr(md5(:table_name || #{connection.quote_table_name(cte_table.name)}.#{quoted_primary_key}::text), 1, 16))::bit(64)::bigint)", { table_name: table_name }])))
45
+ .with(composed_cte)
46
+ .where(Arel.sql(sanitize_sql_for_conditions(["pg_try_advisory_lock(('x' || substr(md5(:table_name || #{connection.quote_table_name(cte_table.name)}.#{quoted_primary_key}::text), 1, 16))::bit(64)::bigint)", { table_name: table_name }])))
40
47
 
41
48
  limit = original_query.arel.ast.limit
42
49
  query.limit = limit.value if limit.present?
@@ -56,7 +63,7 @@ module GoodJob
56
63
  # @example Get the records that have a session awaiting a lock:
57
64
  # MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
58
65
  scope :joins_advisory_locks, (lambda do
59
- join_sql = <<~SQL
66
+ join_sql = <<~SQL.squish
60
67
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
61
68
  AND pg_locks.objsubid = 1
62
69
  AND pg_locks.classid = ('x' || substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(32)::int
@@ -132,6 +139,12 @@ module GoodJob
132
139
  records.each(&:advisory_unlock)
133
140
  end
134
141
  end
142
+
143
+ def supports_cte_materialization_specifiers?
144
+ return @supports_cte_materialization_specifiers if defined?(@supports_cte_materialization_specifiers)
145
+
146
+ @supports_cte_materialization_specifiers = ActiveRecord::Base.connection.postgresql_version >= 120000
147
+ end
135
148
  end
136
149
 
137
150
  # Acquires an advisory lock on this record if it is not already locked by
@@ -140,10 +153,10 @@ module GoodJob
140
153
  # all remaining locks).
141
154
  # @return [Boolean] whether the lock was acquired.
142
155
  def advisory_lock
143
- where_sql = <<~SQL
156
+ where_sql = <<~SQL.squish
144
157
  pg_try_advisory_lock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
145
158
  SQL
146
- self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
159
+ self.class.unscoped.exists?([where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }])
147
160
  end
148
161
 
149
162
  # Releases an advisory lock on this record if it is locked by this database
@@ -151,10 +164,11 @@ module GoodJob
151
164
  # {#advisory_unlock} and {#advisory_lock} the same number of times.
152
165
  # @return [Boolean] whether the lock was released.
153
166
  def advisory_unlock
154
- where_sql = <<~SQL
155
- pg_advisory_unlock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
167
+ query = <<~SQL.squish
168
+ SELECT 1 AS one
169
+ WHERE pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
156
170
  SQL
157
- self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
171
+ self.class.connection.execute(sanitize_sql_for_conditions([query, { table_name: self.class.table_name, id: send(self.class.primary_key) }])).ntuples.positive?
158
172
  end
159
173
 
160
174
  # Acquires an advisory lock on this record or raises
@@ -191,13 +205,13 @@ module GoodJob
191
205
  # Tests whether this record has an advisory lock on it.
192
206
  # @return [Boolean]
193
207
  def advisory_locked?
194
- self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
208
+ self.class.unscoped.advisory_locked.exists?(id: send(self.class.primary_key))
195
209
  end
196
210
 
197
211
  # Tests whether this record is locked by the current database session.
198
212
  # @return [Boolean]
199
213
  def owns_advisory_lock?
200
- self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
214
+ self.class.unscoped.owns_advisory_locked.exists?(id: send(self.class.primary_key))
201
215
  end
202
216
 
203
217
  # 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.3.0'.freeze
3
+ VERSION = '1.3.5'.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.3.0
4
+ version: 1.3.5
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-10-03 00:00:00.000000000 Z
11
+ date: 2020-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -164,20 +164,6 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: erb_lint
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- version: '0'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - ">="
179
- - !ruby/object:Gem::Version
180
- version: '0'
181
167
  - !ruby/object:Gem::Dependency
182
168
  name: foreman
183
169
  requirement: !ruby/object:Gem::Requirement
@@ -248,20 +234,6 @@ dependencies:
248
234
  - - ">="
249
235
  - !ruby/object:Gem::Version
250
236
  version: '0'
251
- - !ruby/object:Gem::Dependency
252
- name: mdl
253
- requirement: !ruby/object:Gem::Requirement
254
- requirements:
255
- - - ">="
256
- - !ruby/object:Gem::Version
257
- version: '0'
258
- type: :development
259
- prerelease: false
260
- version_requirements: !ruby/object:Gem::Requirement
261
- requirements:
262
- - - ">="
263
- - !ruby/object:Gem::Version
264
- version: '0'
265
237
  - !ruby/object:Gem::Dependency
266
238
  name: pry-rails
267
239
  requirement: !ruby/object:Gem::Requirement
@@ -291,7 +263,7 @@ dependencies:
291
263
  - !ruby/object:Gem::Version
292
264
  version: '0'
293
265
  - !ruby/object:Gem::Dependency
294
- name: rbtrace
266
+ name: rails
295
267
  requirement: !ruby/object:Gem::Requirement
296
268
  requirements:
297
269
  - - ">="
@@ -305,49 +277,7 @@ dependencies:
305
277
  - !ruby/object:Gem::Version
306
278
  version: '0'
307
279
  - !ruby/object:Gem::Dependency
308
- name: rspec-rails
309
- requirement: !ruby/object:Gem::Requirement
310
- requirements:
311
- - - ">="
312
- - !ruby/object:Gem::Version
313
- version: '0'
314
- type: :development
315
- prerelease: false
316
- version_requirements: !ruby/object:Gem::Requirement
317
- requirements:
318
- - - ">="
319
- - !ruby/object:Gem::Version
320
- version: '0'
321
- - !ruby/object:Gem::Dependency
322
- name: rubocop
323
- requirement: !ruby/object:Gem::Requirement
324
- requirements:
325
- - - ">="
326
- - !ruby/object:Gem::Version
327
- version: '0'
328
- type: :development
329
- prerelease: false
330
- version_requirements: !ruby/object:Gem::Requirement
331
- requirements:
332
- - - ">="
333
- - !ruby/object:Gem::Version
334
- version: '0'
335
- - !ruby/object:Gem::Dependency
336
- name: rubocop-performance
337
- requirement: !ruby/object:Gem::Requirement
338
- requirements:
339
- - - ">="
340
- - !ruby/object:Gem::Version
341
- version: '0'
342
- type: :development
343
- prerelease: false
344
- version_requirements: !ruby/object:Gem::Requirement
345
- requirements:
346
- - - ">="
347
- - !ruby/object:Gem::Version
348
- version: '0'
349
- - !ruby/object:Gem::Dependency
350
- name: rubocop-rails
280
+ name: rbtrace
351
281
  requirement: !ruby/object:Gem::Requirement
352
282
  requirements:
353
283
  - - ">="
@@ -361,7 +291,7 @@ dependencies:
361
291
  - !ruby/object:Gem::Version
362
292
  version: '0'
363
293
  - !ruby/object:Gem::Dependency
364
- name: rubocop-rspec
294
+ name: rspec-rails
365
295
  requirement: !ruby/object:Gem::Requirement
366
296
  requirements:
367
297
  - - ">="
@@ -444,20 +374,20 @@ files:
444
374
  - CHANGELOG.md
445
375
  - LICENSE.txt
446
376
  - README.md
377
+ - engine/app/assets/style.css
378
+ - engine/app/assets/vendor/bootstrap/bootstrap-native.js
379
+ - engine/app/assets/vendor/bootstrap/bootstrap.css
380
+ - engine/app/assets/vendor/chartist/chartist.css
381
+ - engine/app/assets/vendor/chartist/chartist.js
447
382
  - engine/app/controllers/good_job/active_jobs_controller.rb
448
383
  - engine/app/controllers/good_job/base_controller.rb
449
384
  - engine/app/controllers/good_job/dashboards_controller.rb
450
385
  - engine/app/helpers/good_job/application_helper.rb
451
- - engine/app/views/assets/_style.css.erb
452
386
  - engine/app/views/good_job/active_jobs/show.html.erb
453
387
  - engine/app/views/good_job/dashboards/index.html.erb
454
388
  - engine/app/views/layouts/good_job/base.html.erb
455
389
  - engine/app/views/shared/_chart.erb
456
390
  - engine/app/views/shared/_jobs_table.erb
457
- - engine/app/views/vendor/bootstrap/_bootstrap-native.js.erb
458
- - engine/app/views/vendor/bootstrap/_bootstrap.css.erb
459
- - engine/app/views/vendor/chartist/_chartist.css.erb
460
- - engine/app/views/vendor/chartist/_chartist.js.erb
461
391
  - engine/config/routes.rb
462
392
  - engine/lib/good_job/engine.rb
463
393
  - exe/good_job
@@ -475,6 +405,7 @@ files:
475
405
  - lib/good_job/multi_scheduler.rb
476
406
  - lib/good_job/notifier.rb
477
407
  - lib/good_job/performer.rb
408
+ - lib/good_job/poller.rb
478
409
  - lib/good_job/railtie.rb
479
410
  - lib/good_job/scheduler.rb
480
411
  - lib/good_job/version.rb
@@ -510,7 +441,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
510
441
  - !ruby/object:Gem::Version
511
442
  version: '0'
512
443
  requirements: []
513
- rubygems_version: 3.0.3
444
+ rubygems_version: 3.1.4
514
445
  signing_key:
515
446
  specification_version: 4
516
447
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails