good_job 1.2.0 → 1.2.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 +4 -4
- data/CHANGELOG.md +118 -2
- data/README.md +280 -152
- data/engine/app/controllers/good_job/active_jobs_controller.rb +8 -0
- data/engine/app/controllers/good_job/base_controller.rb +5 -0
- data/engine/app/controllers/good_job/dashboards_controller.rb +50 -0
- data/engine/app/helpers/good_job/application_helper.rb +4 -0
- data/engine/app/views/assets/_style.css.erb +16 -0
- data/engine/app/views/good_job/active_jobs/show.html.erb +1 -0
- data/engine/app/views/good_job/dashboards/index.html.erb +19 -0
- data/engine/app/views/layouts/good_job/base.html.erb +50 -0
- data/engine/app/views/shared/_chart.erb +51 -0
- data/engine/app/views/shared/_jobs_table.erb +26 -0
- data/engine/app/views/vendor/bootstrap/_bootstrap-native.js.erb +1662 -0
- data/engine/app/views/vendor/bootstrap/_bootstrap.css.erb +10258 -0
- data/engine/app/views/vendor/chartist/_chartist.css.erb +613 -0
- data/engine/app/views/vendor/chartist/_chartist.js.erb +4516 -0
- data/engine/config/routes.rb +4 -0
- data/engine/lib/good_job/engine.rb +5 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -2
- data/lib/generators/good_job/install_generator.rb +8 -0
- data/lib/good_job.rb +40 -25
- data/lib/good_job/adapter.rb +44 -4
- data/lib/good_job/cli.rb +66 -13
- data/lib/good_job/configuration.rb +52 -0
- data/lib/good_job/current_execution.rb +2 -0
- data/lib/good_job/job.rb +118 -21
- data/lib/good_job/lockable.rb +125 -14
- data/lib/good_job/log_subscriber.rb +70 -4
- data/lib/good_job/multi_scheduler.rb +6 -0
- data/lib/good_job/notifier.rb +64 -27
- data/lib/good_job/performer.rb +38 -0
- data/lib/good_job/railtie.rb +1 -0
- data/lib/good_job/scheduler.rb +34 -20
- data/lib/good_job/version.rb +2 -1
- metadata +163 -7
- data/lib/good_job/pg_locks.rb +0 -21
data/lib/good_job/job.rb
CHANGED
@@ -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
|
@@ -58,7 +138,8 @@ module GoodJob
|
|
58
138
|
|
59
139
|
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
60
140
|
good_job = good_jobs.first
|
61
|
-
|
141
|
+
# TODO: Determine why some records are fetched without an advisory lock at all
|
142
|
+
break unless good_job&.owns_advisory_lock?
|
62
143
|
|
63
144
|
result, error = good_job.perform
|
64
145
|
end
|
@@ -66,6 +147,15 @@ module GoodJob
|
|
66
147
|
[good_job, result, error] if good_job
|
67
148
|
end
|
68
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.
|
69
159
|
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
70
160
|
good_job = nil
|
71
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|
|
@@ -86,32 +176,25 @@ module GoodJob
|
|
86
176
|
good_job
|
87
177
|
end
|
88
178
|
|
89
|
-
|
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
|
90
185
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
91
186
|
|
92
187
|
GoodJob::CurrentExecution.reset
|
93
|
-
result = nil
|
94
|
-
rescued_error = nil
|
95
|
-
error = nil
|
96
188
|
|
97
189
|
self.performed_at = Time.current
|
98
|
-
save!
|
190
|
+
save! if GoodJob.preserve_job_records
|
99
191
|
|
100
|
-
|
101
|
-
"provider_job_id" => id
|
102
|
-
)
|
103
|
-
|
104
|
-
begin
|
105
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
106
|
-
result = ActiveJob::Base.execute(params)
|
107
|
-
end
|
108
|
-
rescue StandardError => e
|
109
|
-
rescued_error = e
|
110
|
-
end
|
192
|
+
result, rescued_error = execute
|
111
193
|
|
112
194
|
retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
|
113
195
|
GoodJob::CurrentExecution.error_on_discard
|
114
196
|
|
197
|
+
error = nil
|
115
198
|
if rescued_error
|
116
199
|
error = rescued_error
|
117
200
|
elsif result.is_a?(Exception)
|
@@ -124,19 +207,33 @@ module GoodJob
|
|
124
207
|
error_message = "#{error.class}: #{error.message}" if error
|
125
208
|
self.error = error_message
|
126
209
|
|
127
|
-
if rescued_error &&
|
210
|
+
if rescued_error && GoodJob.reperform_jobs_on_standard_error
|
128
211
|
save!
|
129
212
|
else
|
130
213
|
self.finished_at = Time.current
|
131
214
|
|
132
|
-
if
|
133
|
-
destroy!
|
134
|
-
else
|
215
|
+
if GoodJob.preserve_job_records
|
135
216
|
save!
|
217
|
+
else
|
218
|
+
destroy!
|
136
219
|
end
|
137
220
|
end
|
138
221
|
|
139
222
|
[result, error]
|
140
223
|
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def execute
|
228
|
+
params = serialized_params.merge(
|
229
|
+
"provider_job_id" => id
|
230
|
+
)
|
231
|
+
|
232
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
233
|
+
[ActiveJob::Base.execute(params), nil]
|
234
|
+
end
|
235
|
+
rescue StandardError => e
|
236
|
+
[nil, e]
|
237
|
+
end
|
141
238
|
end
|
142
239
|
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -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 ||
|
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[
|
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 ||
|
29
|
-
AND pg_locks.objid = (('x'||substr(md5(:table_name ||
|
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,27 +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
|
-
|
59
|
-
|
60
|
-
WHERE pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
|
143
|
+
where_sql = <<~SQL
|
144
|
+
pg_try_advisory_lock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
|
61
145
|
SQL
|
62
|
-
self.class.
|
146
|
+
self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
|
63
147
|
end
|
64
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.
|
65
153
|
def advisory_unlock
|
66
|
-
|
67
|
-
|
68
|
-
WHERE pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
|
154
|
+
where_sql = <<~SQL
|
155
|
+
pg_advisory_unlock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
|
69
156
|
SQL
|
70
|
-
self.class.
|
157
|
+
self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
|
71
158
|
end
|
72
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+
|
73
165
|
def advisory_lock!
|
74
166
|
result = advisory_lock
|
75
167
|
result || raise(RecordAlreadyAdvisoryLockedError)
|
76
168
|
end
|
77
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
|
78
182
|
def with_advisory_lock
|
79
183
|
raise ArgumentError, "Must provide a block" unless block_given?
|
80
184
|
|
@@ -84,14 +188,21 @@ module GoodJob
|
|
84
188
|
advisory_unlock unless $ERROR_INFO.is_a? RecordAlreadyAdvisoryLockedError
|
85
189
|
end
|
86
190
|
|
191
|
+
# Tests whether this record has an advisory lock on it.
|
192
|
+
# @return [Boolean]
|
87
193
|
def advisory_locked?
|
88
|
-
self.class.advisory_locked.where(id: send(self.class.primary_key)).
|
194
|
+
self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
|
89
195
|
end
|
90
196
|
|
197
|
+
# Tests whether this record is locked by the current database session.
|
198
|
+
# @return [Boolean]
|
91
199
|
def owns_advisory_lock?
|
92
|
-
self.class.owns_advisory_locked.where(id: send(self.class.primary_key)).
|
200
|
+
self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
|
93
201
|
end
|
94
202
|
|
203
|
+
# Releases all advisory locks on the record that are held by the current
|
204
|
+
# database session.
|
205
|
+
# @return [void]
|
95
206
|
def advisory_unlock!
|
96
207
|
advisory_unlock while advisory_locked?
|
97
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)
|