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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -2
- data/README.md +324 -160
- 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 +61 -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 +59 -27
- data/lib/good_job/adapter.rb +38 -0
- data/lib/good_job/cli.rb +66 -13
- data/lib/good_job/configuration.rb +61 -2
- data/lib/good_job/job.rb +126 -36
- data/lib/good_job/lockable.rb +119 -6
- data/lib/good_job/log_subscriber.rb +70 -6
- data/lib/good_job/multi_scheduler.rb +6 -0
- data/lib/good_job/notifier.rb +55 -29
- data/lib/good_job/performer.rb +38 -0
- data/lib/good_job/railtie.rb +1 -0
- data/lib/good_job/scheduler.rb +48 -40
- 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
|
@@ -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
|
-
|
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!
|
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
|
-
|
114
|
-
GoodJob::CurrentExecution.error_on_discard
|
192
|
+
result, unhandled_error = execute
|
115
193
|
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
126
|
-
|
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
|
207
|
+
if unhandled_error && GoodJob.retry_on_unhandled_error
|
129
208
|
save!
|
130
|
-
|
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
|
-
|
134
|
-
|
135
|
-
else
|
136
|
-
save!
|
137
|
-
end
|
211
|
+
save!
|
212
|
+
else
|
213
|
+
destroy!
|
138
214
|
end
|
139
215
|
|
140
|
-
[result,
|
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
|
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,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
|
-
|
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
|
-
|
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)
|