good_job 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -7
  3. data/README.md +13 -10
  4. data/engine/app/controllers/good_job/active_jobs_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/base_controller.rb +5 -0
  6. data/engine/app/controllers/good_job/dashboards_controller.rb +50 -0
  7. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  8. data/engine/app/views/assets/_style.css.erb +16 -0
  9. data/engine/app/views/good_job/active_jobs/show.html.erb +1 -0
  10. data/engine/app/views/good_job/dashboards/index.html.erb +19 -0
  11. data/engine/app/views/layouts/good_job/base.html.erb +50 -0
  12. data/engine/app/views/shared/_chart.erb +51 -0
  13. data/engine/app/views/shared/_jobs_table.erb +26 -0
  14. data/engine/app/views/vendor/bootstrap/_bootstrap-native.js.erb +1662 -0
  15. data/engine/app/views/vendor/bootstrap/_bootstrap.css.erb +10258 -0
  16. data/engine/app/views/vendor/chartist/_chartist.css.erb +613 -0
  17. data/engine/app/views/vendor/chartist/_chartist.js.erb +4516 -0
  18. data/engine/config/routes.rb +4 -0
  19. data/engine/lib/good_job/engine.rb +5 -0
  20. data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -2
  21. data/lib/generators/good_job/install_generator.rb +8 -0
  22. data/lib/good_job.rb +40 -24
  23. data/lib/good_job/adapter.rb +38 -0
  24. data/lib/good_job/cli.rb +30 -7
  25. data/lib/good_job/configuration.rb +44 -0
  26. data/lib/good_job/job.rb +116 -20
  27. data/lib/good_job/lockable.rb +119 -6
  28. data/lib/good_job/log_subscriber.rb +70 -4
  29. data/lib/good_job/multi_scheduler.rb +6 -0
  30. data/lib/good_job/notifier.rb +55 -29
  31. data/lib/good_job/performer.rb +38 -0
  32. data/lib/good_job/railtie.rb +1 -0
  33. data/lib/good_job/scheduler.rb +33 -20
  34. data/lib/good_job/version.rb +2 -1
  35. metadata +96 -9
@@ -1,10 +1,33 @@
1
1
  module GoodJob
2
+ #
3
+ # Adds Postgres advisory locking capabilities to an ActiveRecord record.
4
+ # For details on advisory locks, see the Postgres documentation:
5
+ # - {https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS Advisory Locks Overview}
6
+ # - {https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS Advisory Locks Functions}
7
+ #
8
+ # @example Add this concern to a +MyRecord+ class:
9
+ # class MyRecord < ActiveRecord::Base
10
+ # include Lockable
11
+ #
12
+ # def my_method
13
+ # ...
14
+ # end
15
+ # end
16
+ #
2
17
  module Lockable
3
18
  extend ActiveSupport::Concern
4
19
 
20
+ # Indicates an advisory lock is already held on a record by another
21
+ # database session.
5
22
  RecordAlreadyAdvisoryLockedError = Class.new(StandardError)
6
23
 
7
24
  included do
25
+ # Attempt to acquire an advisory lock on the selected records and
26
+ # return only those records for which a lock could be acquired.
27
+ # @!method advisory_lock
28
+ # @!scope class
29
+ # @return [ActiveRecord::Relation]
30
+ # A relation selecting only the records that were locked.
8
31
  scope :advisory_lock, (lambda do
9
32
  original_query = self
10
33
 
@@ -13,35 +36,92 @@ module GoodJob
13
36
 
14
37
  query = cte_table.project(cte_table[:id])
15
38
  .with(composed_cte)
16
- .where(Arel.sql(sanitize_sql_for_conditions(["pg_try_advisory_lock(('x'||substr(md5(:table_name || \"#{cte_table.name}\".\"#{primary_key}\"::text), 1, 16))::bit(64)::bigint)", { table_name: table_name }])))
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 }])))
17
40
 
18
41
  limit = original_query.arel.ast.limit
19
42
  query.limit = limit.value if limit.present?
20
43
 
21
- unscoped.where(arel_table[:id].in(query)).merge(original_query.only(:order))
44
+ unscoped.where(arel_table[primary_key].in(query)).merge(original_query.only(:order))
22
45
  end)
23
46
 
47
+ # Joins the current query with Postgres's +pg_locks+ table (it provides
48
+ # data about existing locks) such that each row in the main query joins
49
+ # to all the advisory locks associated with that row.
50
+ #
51
+ # For details on +pg_locks+, see
52
+ # {https://www.postgresql.org/docs/current/view-pg-locks.html}.
53
+ # @!method joins_advisory_locks
54
+ # @!scope class
55
+ # @return [ActiveRecord::Relation]
56
+ # @example Get the records that have a session awaiting a lock:
57
+ # MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
24
58
  scope :joins_advisory_locks, (lambda do
25
59
  join_sql = <<~SQL
26
60
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
27
61
  AND pg_locks.objsubid = 1
28
- AND pg_locks.classid = ('x'||substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(32)::int
29
- AND pg_locks.objid = (('x'||substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(64) << 32)::bit(32)::int
62
+ AND pg_locks.classid = ('x' || substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(32)::int
63
+ AND pg_locks.objid = (('x' || substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(64) << 32)::bit(32)::int
30
64
  SQL
31
65
 
32
66
  joins(sanitize_sql_for_conditions([join_sql, { table_name: table_name }]))
33
67
  end)
34
68
 
69
+ # Find records that do not have an advisory lock on them.
70
+ # @!method advisory_unlocked
71
+ # @!scope class
72
+ # @return [ActiveRecord::Relation]
35
73
  scope :advisory_unlocked, -> { joins_advisory_locks.where(pg_locks: { locktype: nil }) }
74
+
75
+ # Find records that have an advisory lock on them.
76
+ # @!method advisory_locked
77
+ # @!scope class
78
+ # @return [ActiveRecord::Relation]
36
79
  scope :advisory_locked, -> { joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
80
+
81
+ # Find records with advisory locks owned by the current Postgres
82
+ # session/connection.
83
+ # @!method advisory_locked
84
+ # @!scope class
85
+ # @return [ActiveRecord::Relation]
37
86
  scope :owns_advisory_locked, -> { joins_advisory_locks.where('"pg_locks"."pid" = pg_backend_pid()') }
38
87
 
88
+ # @!attribute [r] create_with_advisory_lock
89
+ # @return [Boolean]
90
+ # Whether an advisory lock should be acquired in the same transaction
91
+ # that created the record.
92
+ #
93
+ # This helps prevent another thread or database session from acquiring a
94
+ # lock on the record between the time you create it and the time you
95
+ # request a lock, since other sessions will not be able to see the new
96
+ # record until the transaction that creates it is completed (at which
97
+ # point you have already acquired the lock).
98
+ #
99
+ # @example
100
+ # record = MyLockableRecord.create(create_with_advisory_lock: true)
101
+ # record.advisory_locked?
102
+ # => true
39
103
  attr_accessor :create_with_advisory_lock
40
104
 
41
105
  after_create -> { advisory_lock }, if: :create_with_advisory_lock
42
106
  end
43
107
 
44
108
  class_methods do
109
+ # Acquires an advisory lock on the selected record(s) and safely releases
110
+ # it after the passed block is completed. The block will be passed an
111
+ # array of the locked records as its first argument.
112
+ #
113
+ # Note that this will not block and wait for locks to be acquired.
114
+ # Instead, it will acquire a lock on all the selected records that it
115
+ # can (as in {Lockable.advisory_lock}) and only pass those that could be
116
+ # locked to the block.
117
+ #
118
+ # @yield [Array<Lockable>] the records that were successfully locked.
119
+ # @return [Object] the result of the block.
120
+ #
121
+ # @example Work on the first two +MyLockableRecord+ objects that could be locked:
122
+ # MyLockableRecord.order(created_at: :asc).limit(2).with_advisory_lock do |record|
123
+ # do_something_with record
124
+ # end
45
125
  def with_advisory_lock
46
126
  raise ArgumentError, "Must provide a block" unless block_given?
47
127
 
@@ -54,25 +134,51 @@ module GoodJob
54
134
  end
55
135
  end
56
136
 
137
+ # Acquires an advisory lock on this record if it is not already locked by
138
+ # another database session. Be careful to ensure you release the lock when
139
+ # you are done with {#advisory_unlock} (or {#advisory_unlock!} to release
140
+ # all remaining locks).
141
+ # @return [Boolean] whether the lock was acquired.
57
142
  def advisory_lock
58
143
  where_sql = <<~SQL
59
- pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
144
+ pg_try_advisory_lock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
60
145
  SQL
61
146
  self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
62
147
  end
63
148
 
149
+ # Releases an advisory lock on this record if it is locked by this database
150
+ # session. Note that advisory locks stack, so you must call
151
+ # {#advisory_unlock} and {#advisory_lock} the same number of times.
152
+ # @return [Boolean] whether the lock was released.
64
153
  def advisory_unlock
65
154
  where_sql = <<~SQL
66
- pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
155
+ pg_advisory_unlock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
67
156
  SQL
68
157
  self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
69
158
  end
70
159
 
160
+ # Acquires an advisory lock on this record or raises
161
+ # {RecordAlreadyAdvisoryLockedError} if it is already locked by another
162
+ # database session.
163
+ # @raise [RecordAlreadyAdvisoryLockedError]
164
+ # @return [Boolean] +true+
71
165
  def advisory_lock!
72
166
  result = advisory_lock
73
167
  result || raise(RecordAlreadyAdvisoryLockedError)
74
168
  end
75
169
 
170
+ # Acquires an advisory lock on this record and safely releases it after the
171
+ # passed block is completed. If the record is locked by another database
172
+ # session, this raises {RecordAlreadyAdvisoryLockedError}.
173
+ #
174
+ # @yield Nothing
175
+ # @return [Object] The result of the block.
176
+ #
177
+ # @example
178
+ # record = MyLockableRecord.first
179
+ # record.with_advisory_lock do
180
+ # do_something_with record
181
+ # end
76
182
  def with_advisory_lock
77
183
  raise ArgumentError, "Must provide a block" unless block_given?
78
184
 
@@ -82,14 +188,21 @@ module GoodJob
82
188
  advisory_unlock unless $ERROR_INFO.is_a? RecordAlreadyAdvisoryLockedError
83
189
  end
84
190
 
191
+ # Tests whether this record has an advisory lock on it.
192
+ # @return [Boolean]
85
193
  def advisory_locked?
86
194
  self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
87
195
  end
88
196
 
197
+ # Tests whether this record is locked by the current database session.
198
+ # @return [Boolean]
89
199
  def owns_advisory_lock?
90
200
  self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
91
201
  end
92
202
 
203
+ # Releases all advisory locks on the record that are held by the current
204
+ # database session.
205
+ # @return [void]
93
206
  def advisory_unlock!
94
207
  advisory_unlock while advisory_locked?
95
208
  end
@@ -1,6 +1,22 @@
1
1
  module GoodJob
2
+ #
3
+ # Listens to GoodJob notifications and logs them.
4
+ #
5
+ # Each method corresponds to the name of a notification. For example, when
6
+ # the {Scheduler} shuts down, it sends a notification named
7
+ # +"scheduler_shutdown.good_job"+ and the {#scheduler_shutdown} method will
8
+ # be called here. See the
9
+ # {https://api.rubyonrails.org/classes/ActiveSupport/LogSubscriber.html ActiveSupport::LogSubscriber}
10
+ # documentation for more.
11
+ #
2
12
  class LogSubscriber < ActiveSupport::LogSubscriber
13
+ # @!group Notifications
14
+
15
+ # @!macro notification_responder
16
+ # Responds to the +$0.good_job+ notification.
17
+ # @return [void]
3
18
  def create(event)
19
+ # FIXME: This method does not match any good_job notifications.
4
20
  good_job = event.payload[:good_job]
5
21
 
6
22
  debug do
@@ -8,7 +24,9 @@ module GoodJob
8
24
  end
9
25
  end
10
26
 
27
+ # @macro notification_responder
11
28
  def timer_task_finished(event)
29
+ # FIXME: This method does not match any good_job notifications.
12
30
  exception = event.payload[:error]
13
31
  return unless exception
14
32
 
@@ -17,7 +35,9 @@ module GoodJob
17
35
  end
18
36
  end
19
37
 
38
+ # @macro notification_responder
20
39
  def job_finished(event)
40
+ # FIXME: This method does not match any good_job notifications.
21
41
  exception = event.payload[:error]
22
42
  return unless exception
23
43
 
@@ -26,6 +46,7 @@ module GoodJob
26
46
  end
27
47
  end
28
48
 
49
+ # @macro notification_responder
29
50
  def scheduler_create_pools(event)
30
51
  max_threads = event.payload[:max_threads]
31
52
  poll_interval = event.payload[:poll_interval]
@@ -37,6 +58,7 @@ module GoodJob
37
58
  end
38
59
  end
39
60
 
61
+ # @macro notification_responder
40
62
  def scheduler_shutdown_start(event)
41
63
  process_id = event.payload[:process_id]
42
64
 
@@ -45,6 +67,7 @@ module GoodJob
45
67
  end
46
68
  end
47
69
 
70
+ # @macro notification_responder
48
71
  def scheduler_shutdown(event)
49
72
  process_id = event.payload[:process_id]
50
73
 
@@ -53,6 +76,7 @@ module GoodJob
53
76
  end
54
77
  end
55
78
 
79
+ # @macro notification_responder
56
80
  def scheduler_restart_pools(event)
57
81
  process_id = event.payload[:process_id]
58
82
 
@@ -61,6 +85,7 @@ module GoodJob
61
85
  end
62
86
  end
63
87
 
88
+ # @macro notification_responder
64
89
  def perform_job(event)
65
90
  good_job = event.payload[:good_job]
66
91
  process_id = event.payload[:process_id]
@@ -71,12 +96,14 @@ module GoodJob
71
96
  end
72
97
  end
73
98
 
99
+ # @macro notification_responder
74
100
  def notifier_listen(_event)
75
101
  info do
76
102
  "Notifier subscribed with LISTEN"
77
103
  end
78
104
  end
79
105
 
106
+ # @macro notification_responder
80
107
  def notifier_notified(event)
81
108
  payload = event.payload[:payload]
82
109
 
@@ -85,6 +112,7 @@ module GoodJob
85
112
  end
86
113
  end
87
114
 
115
+ # @macro notification_responder
88
116
  def notifier_notify_error(event)
89
117
  error = event.payload[:error]
90
118
 
@@ -93,12 +121,14 @@ module GoodJob
93
121
  end
94
122
  end
95
123
 
124
+ # @macro notification_responder
96
125
  def notifier_unlisten(_event)
97
126
  info do
98
127
  "Notifier unsubscribed with UNLISTEN"
99
128
  end
100
129
  end
101
130
 
131
+ # @macro notification_responder
102
132
  def cleanup_preserved_jobs(event)
103
133
  timestamp = event.payload[:timestamp]
104
134
  deleted_records_count = event.payload[:deleted_records_count]
@@ -108,11 +138,34 @@ module GoodJob
108
138
  end
109
139
  end
110
140
 
141
+ # @!endgroup
142
+
143
+ # Get the logger associated with this {LogSubscriber} instance.
144
+ # @return [Logger]
145
+ def logger
146
+ GoodJob::LogSubscriber.logger
147
+ end
148
+
111
149
  class << self
150
+ # Tracks all loggers that {LogSubscriber} is writing to. You can write to
151
+ # multiple logs by appending to this array. After updating it, you should
152
+ # usually call {LogSubscriber.reset_logger} to make sure they are all
153
+ # written to.
154
+ #
155
+ # Defaults to {GoodJob.logger}.
156
+ # @return [Array<Logger>]
157
+ # @example Write to STDOUT and to a file:
158
+ # GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
159
+ # GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
160
+ # GoodJob::LogSubscriber.reset_logger
112
161
  def loggers
113
162
  @_loggers ||= [GoodJob.logger]
114
163
  end
115
164
 
165
+ # Represents all the loggers attached to {LogSubscriber} with a single
166
+ # logging interface. Writing to this logger is a shortcut for writing to
167
+ # each of the loggers in {LogSubscriber.loggers}.
168
+ # @return [Logger]
116
169
  def logger
117
170
  @_logger ||= begin
118
171
  logger = Logger.new(StringIO.new)
@@ -123,17 +176,22 @@ module GoodJob
123
176
  end
124
177
  end
125
178
 
179
+ # Reset {LogSubscriber.logger} and force it to rebuild a new shortcut to
180
+ # all the loggers in {LogSubscriber.loggers}. You should usually call
181
+ # this after modifying the {LogSubscriber.loggers} array.
182
+ # @return [void]
126
183
  def reset_logger
127
184
  @_logger = nil
128
185
  end
129
186
  end
130
187
 
131
- def logger
132
- GoodJob::LogSubscriber.logger
133
- end
134
-
135
188
  private
136
189
 
190
+ # Add "GoodJob" plus any specified tags to every
191
+ # {ActiveSupport::TaggedLogging} logger in {LogSubscriber.loggers}. Tags
192
+ # are only applicable inside the block passed to this method.
193
+ # @yield [void]
194
+ # @return [void]
137
195
  def tag_logger(*tags, &block)
138
196
  tags = tags.dup.unshift("GoodJob").compact
139
197
 
@@ -152,6 +210,14 @@ module GoodJob
152
210
  end.call
153
211
  end
154
212
 
213
+ # Ensure that the standard logging methods include "GoodJob" as a tag and
214
+ # that they include a second argument allowing callers to specify ad-hoc
215
+ # tags to include in the message.
216
+ #
217
+ # For example, to include the tag "ForFunsies" on an +info+ message:
218
+ #
219
+ # self.info("Some message", tags: ["ForFunsies"])
220
+ #
155
221
  %w(info debug warn error fatal unknown).each do |level|
156
222
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
157
223
  def #{level}(progname = nil, tags: [], &block)
@@ -1,23 +1,29 @@
1
1
  module GoodJob
2
+ # Delegates the interface of a single {Scheduler} to multiple Schedulers.
2
3
  class MultiScheduler
4
+ # @return [array<Scheduler>] List of the scheduler delegates
3
5
  attr_reader :schedulers
4
6
 
5
7
  def initialize(schedulers)
6
8
  @schedulers = schedulers
7
9
  end
8
10
 
11
+ # Delegates to {Scheduler#shutdown}.
9
12
  def shutdown(wait: true)
10
13
  schedulers.each { |s| s.shutdown(wait: wait) }
11
14
  end
12
15
 
16
+ # Delegates to {Scheduler#shutdown?}.
13
17
  def shutdown?
14
18
  schedulers.all?(&:shutdown?)
15
19
  end
16
20
 
21
+ # Delegates to {Scheduler#restart}.
17
22
  def restart(wait: true)
18
23
  schedulers.each { |s| s.restart(wait: wait) }
19
24
  end
20
25
 
26
+ # Delegates to {Scheduler#create_thread}.
21
27
  def create_thread(state = nil)
22
28
  results = []
23
29
  any_true = schedulers.any? do |scheduler|
@@ -2,10 +2,16 @@ require 'concurrent/atomic/atomic_boolean'
2
2
 
3
3
  module GoodJob # :nodoc:
4
4
  #
5
- # Wrapper for Postgres LISTEN/NOTIFY
5
+ # Notifiers hook into Postgres LISTEN/NOTIFY functionality to emit and listen for notifications across processes.
6
+ #
7
+ # Notifiers can emit NOTIFY messages through Postgres.
8
+ # A notifier will LISTEN for messages by creating a background thread that runs in an instance of +Concurrent::ThreadPoolExecutor+.
9
+ # When a message is received, the notifier passes the message to each of its recipients.
6
10
  #
7
11
  class Notifier
12
+ # Default Postgres channel for LISTEN/NOTIFY
8
13
  CHANNEL = 'good_job'.freeze
14
+ # Defaults for instance of Concurrent::ThreadPoolExecutor
9
15
  POOL_OPTIONS = {
10
16
  name: name,
11
17
  min_threads: 0,
@@ -15,13 +21,17 @@ module GoodJob # :nodoc:
15
21
  max_queue: 1,
16
22
  fallback_policy: :discard,
17
23
  }.freeze
24
+ # Seconds to block while LISTENing for a message
18
25
  WAIT_INTERVAL = 1
19
26
 
20
27
  # @!attribute [r] instances
21
28
  # @!scope class
22
- # @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
29
+ # List of all instantiated Notifiers in the current process.
30
+ # @return [array<GoodJob:Adapter>]
23
31
  cattr_reader :instances, default: [], instance_reader: false
24
32
 
33
+ # Send a message via Postgres NOTIFY
34
+ # @param message [#to_json]
25
35
  def self.notify(message)
26
36
  connection = ActiveRecord::Base.connection
27
37
  connection.exec_query <<~SQL
@@ -29,8 +39,11 @@ module GoodJob # :nodoc:
29
39
  SQL
30
40
  end
31
41
 
42
+ # List of recipients that will receive notifications.
43
+ # @return [Array<#call, Array(Object, Symbol)>]
32
44
  attr_reader :recipients
33
45
 
46
+ # @param recipients [Array<#call, Array(Object, Symbol)>]
34
47
  def initialize(*recipients)
35
48
  @recipients = Concurrent::Array.new(recipients)
36
49
  @listening = Concurrent::AtomicBoolean.new(false)
@@ -41,16 +54,29 @@ module GoodJob # :nodoc:
41
54
  listen
42
55
  end
43
56
 
57
+ # Tests whether the notifier is active and listening for new messages.
58
+ # @return [true, false, nil]
44
59
  def listening?
45
60
  @listening.true?
46
61
  end
47
62
 
63
+ # Restart the notifier.
64
+ # When shutdown, start; or shutdown and start.
65
+ # @param wait [Boolean] Wait for background thread to finish
66
+ # @return [void]
48
67
  def restart(wait: true)
49
68
  shutdown(wait: wait)
50
69
  create_pool
51
70
  listen
52
71
  end
53
72
 
73
+ # Shut down the notifier.
74
+ # This stops the background LISTENing thread.
75
+ # If +wait+ is +true+, the notifier will wait for background thread to shutdown.
76
+ # If +wait+ is +false+, this method will return immediately even though threads may still be running.
77
+ # Use {#shutdown?} to determine whether threads have stopped.
78
+ # @param wait [Boolean] Wait for actively executing jobs to finish
79
+ # @return [void]
54
80
  def shutdown(wait: true)
55
81
  return unless @pool.running?
56
82
 
@@ -58,6 +84,8 @@ module GoodJob # :nodoc:
58
84
  @pool.wait_for_termination if wait
59
85
  end
60
86
 
87
+ # Tests whether the notifier is shutdown.
88
+ # @return [true, false, nil]
61
89
  def shutdown?
62
90
  !@pool.running?
63
91
  end
@@ -70,38 +98,36 @@ module GoodJob # :nodoc:
70
98
 
71
99
  def listen
72
100
  future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
73
- begin
74
- with_listen_connection do |conn|
75
- ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
76
- conn.async_exec "LISTEN #{CHANNEL}"
77
- end
101
+ with_listen_connection do |conn|
102
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
103
+ conn.async_exec "LISTEN #{CHANNEL}"
104
+ end
78
105
 
79
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
80
- while pool.running?
81
- listening.make_true
82
- conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
83
- listening.make_false
84
- next unless channel == CHANNEL
85
-
86
- ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
87
- parsed_payload = JSON.parse(payload, symbolize_names: true)
88
- recipients.each do |recipient|
89
- target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
90
- target.send(method_name, parsed_payload)
91
- end
92
- end
106
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
107
+ while pool.running?
108
+ listening.make_true
109
+ conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
93
110
  listening.make_false
111
+ next unless channel == CHANNEL
112
+
113
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
114
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
115
+ recipients.each do |recipient|
116
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
117
+ target.send(method_name, parsed_payload)
118
+ end
94
119
  end
120
+ listening.make_false
95
121
  end
96
122
  end
97
- rescue StandardError => e
98
- ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
99
- raise
100
- ensure
101
- @listening.make_false
102
- ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
103
- conn.async_exec "UNLISTEN *"
104
- end
123
+ end
124
+ rescue StandardError => e
125
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
126
+ raise
127
+ ensure
128
+ @listening.make_false
129
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
130
+ conn.async_exec "UNLISTEN *"
105
131
  end
106
132
  end
107
133