good_job 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -2
  3. data/README.md +324 -160
  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 +61 -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 +59 -27
  23. data/lib/good_job/adapter.rb +38 -0
  24. data/lib/good_job/cli.rb +66 -13
  25. data/lib/good_job/configuration.rb +61 -2
  26. data/lib/good_job/job.rb +126 -36
  27. data/lib/good_job/lockable.rb +119 -6
  28. data/lib/good_job/log_subscriber.rb +70 -6
  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 +48 -40
  34. data/lib/good_job/version.rb +2 -1
  35. metadata +163 -7
  36. data/lib/good_job/pg_locks.rb +0 -21
@@ -1,14 +1,32 @@
1
1
  module GoodJob
2
+ #
3
+ # Represents a request to perform an +ActiveJob+ job.
4
+ #
2
5
  class Job < ActiveRecord::Base
3
6
  include Lockable
4
7
 
8
+ # Raised if something attempts to execute a previously completed Job again.
5
9
  PreviouslyPerformedError = Class.new(StandardError)
6
10
 
11
+ # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
7
12
  DEFAULT_QUEUE_NAME = 'default'.freeze
13
+ # ActiveJob jobs without a +priority+ attribute are given this priority.
8
14
  DEFAULT_PRIORITY = 0
9
15
 
10
16
  self.table_name = 'good_jobs'.freeze
11
17
 
18
+ # Parse a string representing a group of queues into a more readable data
19
+ # structure.
20
+ # @return [Hash]
21
+ # How to match a given queue. It can have the following keys and values:
22
+ # - +{ all: true }+ indicates that all queues match.
23
+ # - +{ exclude: Array<String> }+ indicates the listed queue names should
24
+ # not match.
25
+ # - +{ include: Array<String> }+ indicates the listed queue names should
26
+ # match.
27
+ # @example
28
+ # GoodJob::Job.queue_parser('-queue1,queue2')
29
+ # => { exclude: [ 'queue1', 'queue2' ] }
12
30
  def self.queue_parser(string)
13
31
  string = string.presence || '*'
14
32
 
@@ -28,6 +46,10 @@ module GoodJob
28
46
  end
29
47
  end
30
48
 
49
+ # Get Jobs that have not yet been completed.
50
+ # @!method unfinished
51
+ # @!scope class
52
+ # @return [ActiveRecord::Relation]
31
53
  scope :unfinished, (lambda do
32
54
  if column_names.include?('finished_at')
33
55
  where(finished_at: nil)
@@ -36,9 +58,42 @@ module GoodJob
36
58
  nil
37
59
  end
38
60
  end)
61
+
62
+ # Get Jobs that are not scheduled for a later time than now (i.e. jobs that
63
+ # are not scheduled or scheduled for earlier than the current time).
64
+ # @!method only_scheduled
65
+ # @!scope class
66
+ # @return [ActiveRecord::Relation]
39
67
  scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
68
+
69
+ # Order jobs by priority (highest priority first).
70
+ # @!method priority_ordered
71
+ # @!scope class
72
+ # @return [ActiveRecord::Relation]
40
73
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
74
+
75
+ # Get Jobs were completed before the given timestamp. If no timestamp is
76
+ # provided, get all jobs that have been completed. By default, GoodJob
77
+ # deletes jobs after they are completed and this will find no jobs.
78
+ # However, if you have changed {GoodJob.preserve_job_records}, this may
79
+ # find completed Jobs.
80
+ # @!method finished(timestamp = nil)
81
+ # @!scope class
82
+ # @param timestamp (Float)
83
+ # Get jobs that finished before this time (in epoch time).
84
+ # @return [ActiveRecord::Relation]
41
85
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
86
+
87
+ # Get Jobs on queues that match the given queue string.
88
+ # @!method queue_string(string)
89
+ # @!scope class
90
+ # @param string [String]
91
+ # A string expression describing what queues to select. See
92
+ # {Job.queue_parser} or
93
+ # {file:README.md#optimize-queues-threads-and-processes} for more details
94
+ # on the format of the string. Note this only handles individual
95
+ # semicolon-separated segments of that string format.
96
+ # @return [ActiveRecord::Relation]
42
97
  scope :queue_string, (lambda do |string|
43
98
  parsed = queue_parser(string)
44
99
 
@@ -51,6 +106,31 @@ module GoodJob
51
106
  end
52
107
  end)
53
108
 
109
+ # Get Jobs in display order with optional keyset pagination.
110
+ # @!method display_all(after_scheduled_at: nil, after_id: nil)
111
+ # @!scope class
112
+ # @param after_scheduled_at [DateTime, String, nil]
113
+ # Display records scheduled after this time for keyset pagination
114
+ # @param after_id [Numeric, String, nil]
115
+ # Display records after this ID for keyset pagination
116
+ # @return [ActiveRecord::Relation]
117
+ scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
118
+ query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
119
+ if after_scheduled_at.present? && after_id.present?
120
+ query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
121
+ elsif after_scheduled_at.present?
122
+ query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
123
+ end
124
+ query
125
+ end)
126
+
127
+ # Finds the next eligible Job, acquire an advisory lock related to it, and
128
+ # executes the job.
129
+ # @return [Array<(GoodJob::Job, Object, Exception)>, nil]
130
+ # If a job was executed, returns an array with the {Job} record, the
131
+ # return value for the job's +#perform+ method, and the exception the job
132
+ # raised, if any (if the job raised, then the second array entry will be
133
+ # +nil+). If there were no jobs to execute, returns +nil+.
54
134
  def self.perform_with_advisory_lock
55
135
  good_job = nil
56
136
  result = nil
@@ -67,6 +147,15 @@ module GoodJob
67
147
  [good_job, result, error] if good_job
68
148
  end
69
149
 
150
+ # Places an ActiveJob job on a queue by creating a new {Job} record.
151
+ # @param active_job [ActiveJob::Base]
152
+ # The job to enqueue.
153
+ # @param scheduled_at [Float]
154
+ # Epoch timestamp when the job should be executed.
155
+ # @param create_with_advisory_lock [Boolean]
156
+ # Whether to establish a lock on the {Job} record after it is created.
157
+ # @return [Job]
158
+ # The new {Job} instance representing the queued ActiveJob job.
70
159
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
71
160
  good_job = nil
72
161
  ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
@@ -87,57 +176,58 @@ module GoodJob
87
176
  good_job
88
177
  end
89
178
 
90
- def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
179
+ # Execute the ActiveJob job this {Job} represents.
180
+ # @return [Array<(Object, Exception)>]
181
+ # An array of the return value of the job's +#perform+ method and the
182
+ # exception raised by the job, if any. If the job completed successfully,
183
+ # the second array entry (the exception) will be +nil+ and vice versa.
184
+ def perform
91
185
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
92
186
 
93
187
  GoodJob::CurrentExecution.reset
94
- result = nil
95
- rescued_error = nil
96
- error = nil
97
188
 
98
189
  self.performed_at = Time.current
99
- save! unless destroy_after
100
-
101
- params = serialized_params.merge(
102
- "provider_job_id" => id
103
- )
104
-
105
- begin
106
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
107
- result = ActiveJob::Base.execute(params)
108
- end
109
- rescue StandardError => e
110
- rescued_error = e
111
- end
190
+ save! if GoodJob.preserve_job_records
112
191
 
113
- retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
114
- GoodJob::CurrentExecution.error_on_discard
192
+ result, unhandled_error = execute
115
193
 
116
- if rescued_error
117
- error = rescued_error
118
- elsif result.is_a?(Exception)
119
- error = result
194
+ result_error = nil
195
+ if result.is_a?(Exception)
196
+ result_error = result
120
197
  result = nil
121
- elsif retry_or_discard_error
122
- error = retry_or_discard_error
123
198
  end
124
199
 
125
- error_message = "#{error.class}: #{error.message}" if error
126
- self.error = error_message
200
+ job_error = unhandled_error ||
201
+ result_error ||
202
+ GoodJob::CurrentExecution.error_on_retry ||
203
+ GoodJob::CurrentExecution.error_on_discard
204
+
205
+ self.error = "#{job_error.class}: #{job_error.message}" if job_error
127
206
 
128
- if rescued_error && reperform_on_standard_error
207
+ if unhandled_error && GoodJob.retry_on_unhandled_error
129
208
  save!
130
- else
209
+ elsif GoodJob.preserve_job_records == true || (unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
131
210
  self.finished_at = Time.current
132
-
133
- if destroy_after
134
- destroy!
135
- else
136
- save!
137
- end
211
+ save!
212
+ else
213
+ destroy!
138
214
  end
139
215
 
140
- [result, error]
216
+ [result, job_error]
217
+ end
218
+
219
+ private
220
+
221
+ def execute
222
+ params = serialized_params.merge(
223
+ "provider_job_id" => id
224
+ )
225
+
226
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
227
+ [ActiveJob::Base.execute(params), nil]
228
+ end
229
+ rescue StandardError => e
230
+ [nil, e]
141
231
  end
142
232
  end
143
233
  end
@@ -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 || "#{table_name}"."#{primary_key}"::text), 1, 16))::bit(32)::int
29
- AND pg_locks.objid = (('x'||substr(md5(:table_name || "#{table_name}"."#{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,8 @@ module GoodJob
8
24
  end
9
25
  end
10
26
 
11
- def timer_task_finished(event)
27
+ # @macro notification_responder
28
+ def finished_timer_task(event)
12
29
  exception = event.payload[:error]
13
30
  return unless exception
14
31
 
@@ -17,7 +34,8 @@ module GoodJob
17
34
  end
18
35
  end
19
36
 
20
- def job_finished(event)
37
+ # @macro notification_responder
38
+ def finished_job_task(event)
21
39
  exception = event.payload[:error]
22
40
  return unless exception
23
41
 
@@ -26,6 +44,7 @@ module GoodJob
26
44
  end
27
45
  end
28
46
 
47
+ # @macro notification_responder
29
48
  def scheduler_create_pools(event)
30
49
  max_threads = event.payload[:max_threads]
31
50
  poll_interval = event.payload[:poll_interval]
@@ -37,6 +56,7 @@ module GoodJob
37
56
  end
38
57
  end
39
58
 
59
+ # @macro notification_responder
40
60
  def scheduler_shutdown_start(event)
41
61
  process_id = event.payload[:process_id]
42
62
 
@@ -45,6 +65,7 @@ module GoodJob
45
65
  end
46
66
  end
47
67
 
68
+ # @macro notification_responder
48
69
  def scheduler_shutdown(event)
49
70
  process_id = event.payload[:process_id]
50
71
 
@@ -53,6 +74,7 @@ module GoodJob
53
74
  end
54
75
  end
55
76
 
77
+ # @macro notification_responder
56
78
  def scheduler_restart_pools(event)
57
79
  process_id = event.payload[:process_id]
58
80
 
@@ -61,6 +83,7 @@ module GoodJob
61
83
  end
62
84
  end
63
85
 
86
+ # @macro notification_responder
64
87
  def perform_job(event)
65
88
  good_job = event.payload[:good_job]
66
89
  process_id = event.payload[:process_id]
@@ -71,12 +94,14 @@ module GoodJob
71
94
  end
72
95
  end
73
96
 
97
+ # @macro notification_responder
74
98
  def notifier_listen(_event)
75
99
  info do
76
100
  "Notifier subscribed with LISTEN"
77
101
  end
78
102
  end
79
103
 
104
+ # @macro notification_responder
80
105
  def notifier_notified(event)
81
106
  payload = event.payload[:payload]
82
107
 
@@ -85,6 +110,7 @@ module GoodJob
85
110
  end
86
111
  end
87
112
 
113
+ # @macro notification_responder
88
114
  def notifier_notify_error(event)
89
115
  error = event.payload[:error]
90
116
 
@@ -93,12 +119,14 @@ module GoodJob
93
119
  end
94
120
  end
95
121
 
122
+ # @macro notification_responder
96
123
  def notifier_unlisten(_event)
97
124
  info do
98
125
  "Notifier unsubscribed with UNLISTEN"
99
126
  end
100
127
  end
101
128
 
129
+ # @macro notification_responder
102
130
  def cleanup_preserved_jobs(event)
103
131
  timestamp = event.payload[:timestamp]
104
132
  deleted_records_count = event.payload[:deleted_records_count]
@@ -108,11 +136,34 @@ module GoodJob
108
136
  end
109
137
  end
110
138
 
139
+ # @!endgroup
140
+
141
+ # Get the logger associated with this {LogSubscriber} instance.
142
+ # @return [Logger]
143
+ def logger
144
+ GoodJob::LogSubscriber.logger
145
+ end
146
+
111
147
  class << self
148
+ # Tracks all loggers that {LogSubscriber} is writing to. You can write to
149
+ # multiple logs by appending to this array. After updating it, you should
150
+ # usually call {LogSubscriber.reset_logger} to make sure they are all
151
+ # written to.
152
+ #
153
+ # Defaults to {GoodJob.logger}.
154
+ # @return [Array<Logger>]
155
+ # @example Write to STDOUT and to a file:
156
+ # GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
157
+ # GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
158
+ # GoodJob::LogSubscriber.reset_logger
112
159
  def loggers
113
160
  @_loggers ||= [GoodJob.logger]
114
161
  end
115
162
 
163
+ # Represents all the loggers attached to {LogSubscriber} with a single
164
+ # logging interface. Writing to this logger is a shortcut for writing to
165
+ # each of the loggers in {LogSubscriber.loggers}.
166
+ # @return [Logger]
116
167
  def logger
117
168
  @_logger ||= begin
118
169
  logger = Logger.new(StringIO.new)
@@ -123,17 +174,22 @@ module GoodJob
123
174
  end
124
175
  end
125
176
 
177
+ # Reset {LogSubscriber.logger} and force it to rebuild a new shortcut to
178
+ # all the loggers in {LogSubscriber.loggers}. You should usually call
179
+ # this after modifying the {LogSubscriber.loggers} array.
180
+ # @return [void]
126
181
  def reset_logger
127
182
  @_logger = nil
128
183
  end
129
184
  end
130
185
 
131
- def logger
132
- GoodJob::LogSubscriber.logger
133
- end
134
-
135
186
  private
136
187
 
188
+ # Add "GoodJob" plus any specified tags to every
189
+ # {ActiveSupport::TaggedLogging} logger in {LogSubscriber.loggers}. Tags
190
+ # are only applicable inside the block passed to this method.
191
+ # @yield [void]
192
+ # @return [void]
137
193
  def tag_logger(*tags, &block)
138
194
  tags = tags.dup.unshift("GoodJob").compact
139
195
 
@@ -152,6 +208,14 @@ module GoodJob
152
208
  end.call
153
209
  end
154
210
 
211
+ # Ensure that the standard logging methods include "GoodJob" as a tag and
212
+ # that they include a second argument allowing callers to specify ad-hoc
213
+ # tags to include in the message.
214
+ #
215
+ # For example, to include the tag "ForFunsies" on an +info+ message:
216
+ #
217
+ # self.info("Some message", tags: ["ForFunsies"])
218
+ #
155
219
  %w(info debug warn error fatal unknown).each do |level|
156
220
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
157
221
  def #{level}(progname = nil, tags: [], &block)