good_job 1.3.5 → 1.6.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 +95 -10
- data/README.md +59 -11
- data/engine/app/controllers/good_job/dashboards_controller.rb +2 -2
- data/engine/app/views/good_job/dashboards/index.html.erb +19 -7
- data/engine/app/views/layouts/good_job/base.html.erb +5 -5
- data/engine/app/views/shared/_chart.erb +1 -1
- data/engine/app/views/shared/_jobs_table.erb +27 -25
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -3
- data/lib/good_job.rb +7 -8
- data/lib/good_job/adapter.rb +18 -15
- data/lib/good_job/cli.rb +15 -1
- data/lib/good_job/configuration.rb +39 -13
- data/lib/good_job/daemon.rb +59 -0
- data/lib/good_job/job.rb +7 -1
- data/lib/good_job/job_performer.rb +63 -0
- data/lib/good_job/lockable.rb +42 -11
- data/lib/good_job/log_subscriber.rb +7 -7
- data/lib/good_job/notifier.rb +26 -14
- data/lib/good_job/poller.rb +3 -0
- data/lib/good_job/railtie.rb +6 -2
- data/lib/good_job/scheduler.rb +11 -20
- data/lib/good_job/version.rb +1 -1
- metadata +5 -46
- data/lib/good_job/performer.rb +0 -60
@@ -2,9 +2,9 @@ module ActiveJob # :nodoc:
|
|
2
2
|
module QueueAdapters # :nodoc:
|
3
3
|
# See {GoodJob::Adapter} for details.
|
4
4
|
class GoodJobAdapter < GoodJob::Adapter
|
5
|
-
def initialize(
|
6
|
-
configuration = GoodJob::Configuration.new(
|
7
|
-
super(execution_mode: configuration.rails_execution_mode
|
5
|
+
def initialize(**options)
|
6
|
+
configuration = GoodJob::Configuration.new(options, env: ENV)
|
7
|
+
super(**options.merge(execution_mode: configuration.rails_execution_mode))
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
data/lib/good_job.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
require "rails"
|
2
|
-
|
3
2
|
require "active_job"
|
4
3
|
require "active_job/queue_adapters"
|
5
4
|
|
6
5
|
require "zeitwerk"
|
7
|
-
|
8
|
-
loader
|
9
|
-
|
10
|
-
|
11
|
-
)
|
12
|
-
loader.
|
13
|
-
|
6
|
+
Zeitwerk::Loader.for_gem.tap do |loader|
|
7
|
+
loader.inflector.inflect({
|
8
|
+
"cli" => "CLI",
|
9
|
+
})
|
10
|
+
loader.ignore(File.join(File.dirname(__FILE__), "generators"))
|
11
|
+
loader.setup
|
12
|
+
end
|
14
13
|
|
15
14
|
require "good_job/railtie"
|
16
15
|
|
data/lib/good_job/adapter.rb
CHANGED
@@ -20,13 +20,22 @@ module GoodJob
|
|
20
20
|
# @param max_threads [nil, Integer] sets the number of threads per scheduler to use when +execution_mode+ is set to +:async+. The +queues+ parameter can specify a number of threads for each group of queues which will override this value. You can also set this with the environment variable +GOOD_JOB_MAX_THREADS+. Defaults to +5+.
|
21
21
|
# @param queues [nil, String] determines which queues to execute jobs from when +execution_mode+ is set to +:async+. See {file:README.md#optimize-queues-threads-and-processes} for more details on the format of this string. You can also set this with the environment variable +GOOD_JOB_QUEUES+. Defaults to +"*"+.
|
22
22
|
# @param poll_interval [nil, Integer] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil)
|
24
|
+
if caller[0..4].find { |c| c.include?("/config/application.rb") || c.include?("/config/environments/") }
|
25
|
+
ActiveSupport::Deprecation.warn(<<~DEPRECATION)
|
26
|
+
GoodJob no longer recommends creating a GoodJob::Adapter instance:
|
27
|
+
|
28
|
+
config.active_job.queue_adapter = GoodJob::Adapter.new...
|
29
|
+
|
30
|
+
Instead, configure GoodJob through configuration:
|
31
|
+
|
32
|
+
config.active_job.queue_adapter = :good_job
|
33
|
+
config.good_job.execution_mode = :#{execution_mode}
|
34
|
+
config.good_job.max_threads = #{max_threads}
|
35
|
+
config.good_job.poll_interval = #{poll_interval}
|
36
|
+
# etc...
|
37
|
+
|
38
|
+
DEPRECATION
|
30
39
|
end
|
31
40
|
|
32
41
|
configuration = GoodJob::Configuration.new(
|
@@ -42,9 +51,9 @@ module GoodJob
|
|
42
51
|
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
|
43
52
|
|
44
53
|
if @execution_mode == :async # rubocop:disable Style/GuardClause
|
45
|
-
@notifier =
|
54
|
+
@notifier = GoodJob::Notifier.new
|
46
55
|
@poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
47
|
-
@scheduler =
|
56
|
+
@scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
48
57
|
@notifier.recipients << [@scheduler, :create_thread]
|
49
58
|
@poller.recipients << [@scheduler, :create_thread]
|
50
59
|
end
|
@@ -108,11 +117,5 @@ module GoodJob
|
|
108
117
|
def execute_inline?
|
109
118
|
@execution_mode == :inline
|
110
119
|
end
|
111
|
-
|
112
|
-
# (deprecated) Whether in +:inline+ execution mode.
|
113
|
-
def inline?
|
114
|
-
ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
|
115
|
-
execute_inline?
|
116
|
-
end
|
117
120
|
end
|
118
121
|
end
|
data/lib/good_job/cli.rb
CHANGED
@@ -15,6 +15,11 @@ module GoodJob
|
|
15
15
|
# Requiring this loads the application's configuration and classes.
|
16
16
|
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
|
17
17
|
|
18
|
+
# @!visibility private
|
19
|
+
def self.exit_on_failure?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
18
23
|
# @!macro thor.desc
|
19
24
|
# @!method $1
|
20
25
|
# @return [void]
|
@@ -27,7 +32,8 @@ module GoodJob
|
|
27
32
|
See option descriptions for the matching environment variable name.
|
28
33
|
|
29
34
|
== Configuring queues
|
30
|
-
|
35
|
+
|
36
|
+
Separate multiple queues with commas; exclude queues with a leading minus;
|
31
37
|
separate isolated execution pools with semicolons and threads with colons.
|
32
38
|
|
33
39
|
DESCRIPTION
|
@@ -43,10 +49,18 @@ module GoodJob
|
|
43
49
|
type: :numeric,
|
44
50
|
banner: 'SECONDS',
|
45
51
|
desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
|
52
|
+
method_option :daemonize,
|
53
|
+
type: :boolean,
|
54
|
+
desc: "Run as a background daemon (default: false)"
|
55
|
+
method_option :pidfile,
|
56
|
+
type: :string,
|
57
|
+
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
46
58
|
def start
|
47
59
|
set_up_application!
|
48
60
|
configuration = GoodJob::Configuration.new(options)
|
49
61
|
|
62
|
+
Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
|
63
|
+
|
50
64
|
notifier = GoodJob::Notifier.new
|
51
65
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
52
66
|
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
@@ -12,16 +12,15 @@ module GoodJob
|
|
12
12
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
|
13
13
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
14
14
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
attr_reader :options, :env
|
15
|
+
# The options that were explicitly set when initializing +Configuration+.
|
16
|
+
# @return [Hash]
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
# The environment from which to read GoodJob's environment variables. By
|
20
|
+
# default, this is the current process's environment, but it can be set
|
21
|
+
# to something else in {#initialize}.
|
22
|
+
# @return [Hash]
|
23
|
+
attr_reader :env
|
25
24
|
|
26
25
|
# @param options [Hash] Any explicitly specified configuration options to
|
27
26
|
# use. Keys are symbols that match the various methods on this class.
|
@@ -45,6 +44,8 @@ module GoodJob
|
|
45
44
|
def execution_mode(default: :external)
|
46
45
|
if options[:execution_mode]
|
47
46
|
options[:execution_mode]
|
47
|
+
elsif rails_config[:execution_mode]
|
48
|
+
rails_config[:execution_mode]
|
48
49
|
elsif env['GOOD_JOB_EXECUTION_MODE'].present?
|
49
50
|
env['GOOD_JOB_EXECUTION_MODE'].to_sym
|
50
51
|
else
|
@@ -58,9 +59,7 @@ module GoodJob
|
|
58
59
|
def rails_execution_mode
|
59
60
|
if execution_mode(default: nil)
|
60
61
|
execution_mode
|
61
|
-
elsif Rails.env.development?
|
62
|
-
:inline
|
63
|
-
elsif Rails.env.test?
|
62
|
+
elsif Rails.env.development? || Rails.env.test?
|
64
63
|
:inline
|
65
64
|
else
|
66
65
|
:external
|
@@ -74,6 +73,7 @@ module GoodJob
|
|
74
73
|
def max_threads
|
75
74
|
(
|
76
75
|
options[:max_threads] ||
|
76
|
+
rails_config[:max_threads] ||
|
77
77
|
env['GOOD_JOB_MAX_THREADS'] ||
|
78
78
|
env['RAILS_MAX_THREADS'] ||
|
79
79
|
DEFAULT_MAX_THREADS
|
@@ -87,6 +87,7 @@ module GoodJob
|
|
87
87
|
# @return [String]
|
88
88
|
def queue_string
|
89
89
|
options[:queues] ||
|
90
|
+
rails_config[:queues] ||
|
90
91
|
env['GOOD_JOB_QUEUES'] ||
|
91
92
|
'*'
|
92
93
|
end
|
@@ -98,17 +99,42 @@ module GoodJob
|
|
98
99
|
def poll_interval
|
99
100
|
(
|
100
101
|
options[:poll_interval] ||
|
102
|
+
rails_config[:poll_interval] ||
|
101
103
|
env['GOOD_JOB_POLL_INTERVAL'] ||
|
102
104
|
DEFAULT_POLL_INTERVAL
|
103
105
|
).to_i
|
104
106
|
end
|
105
107
|
|
108
|
+
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
109
|
+
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
110
|
+
# @return [Integer]
|
106
111
|
def cleanup_preserved_jobs_before_seconds_ago
|
107
112
|
(
|
108
113
|
options[:before_seconds_ago] ||
|
114
|
+
rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
|
109
115
|
env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
|
110
116
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
|
111
117
|
).to_i
|
112
118
|
end
|
119
|
+
|
120
|
+
# Tests whether to daemonize the process.
|
121
|
+
# @return [Boolean]
|
122
|
+
def daemonize?
|
123
|
+
options[:daemonize] || false
|
124
|
+
end
|
125
|
+
|
126
|
+
# Path of the pidfile to create when running as a daemon.
|
127
|
+
# @return [Pathname,String]
|
128
|
+
def pidfile
|
129
|
+
options[:pidfile] ||
|
130
|
+
env['GOOD_JOB_PIDFILE'] ||
|
131
|
+
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def rails_config
|
137
|
+
Rails.application.config.good_job
|
138
|
+
end
|
113
139
|
end
|
114
140
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module GoodJob
|
2
|
+
#
|
3
|
+
# Manages daemonization of the current process.
|
4
|
+
#
|
5
|
+
class Daemon
|
6
|
+
# The path of the generated pidfile.
|
7
|
+
# @return [Pathname,String]
|
8
|
+
attr_reader :pidfile
|
9
|
+
|
10
|
+
# @param pidfile [Pathname,String] Pidfile path
|
11
|
+
def initialize(pidfile:)
|
12
|
+
@pidfile = pidfile
|
13
|
+
end
|
14
|
+
|
15
|
+
# Daemonizes the current process and writes out a pidfile.
|
16
|
+
def daemonize
|
17
|
+
check_pid
|
18
|
+
Process.daemon
|
19
|
+
write_pid
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def write_pid
|
25
|
+
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
|
26
|
+
at_exit { File.delete(pidfile) if File.exist?(pidfile) }
|
27
|
+
rescue Errno::EEXIST
|
28
|
+
check_pid
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_pid
|
33
|
+
File.delete(pidfile) if File.exist?(pidfile)
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_pid
|
37
|
+
case pid_status(pidfile)
|
38
|
+
when :running, :not_owned
|
39
|
+
abort "A server is already running. Check #{pidfile}"
|
40
|
+
when :dead
|
41
|
+
File.delete(pidfile)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def pid_status(pidfile)
|
46
|
+
return :exited unless File.exist?(pidfile)
|
47
|
+
|
48
|
+
pid = ::File.read(pidfile).to_i
|
49
|
+
return :dead if pid.zero?
|
50
|
+
|
51
|
+
Process.kill(0, pid) # check process status
|
52
|
+
:running
|
53
|
+
rescue Errno::ESRCH
|
54
|
+
:dead
|
55
|
+
rescue Errno::EPERM
|
56
|
+
:not_owned
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -139,7 +139,7 @@ module GoodJob
|
|
139
139
|
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
140
140
|
good_job = good_jobs.first
|
141
141
|
# TODO: Determine why some records are fetched without an advisory lock at all
|
142
|
-
break unless good_job&.
|
142
|
+
break unless good_job&.executable?
|
143
143
|
|
144
144
|
result, error = good_job.perform
|
145
145
|
end
|
@@ -216,6 +216,12 @@ module GoodJob
|
|
216
216
|
[result, job_error]
|
217
217
|
end
|
218
218
|
|
219
|
+
# Tests whether this job is safe to be executed by this thread.
|
220
|
+
# @return [Boolean]
|
221
|
+
def executable?
|
222
|
+
self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
|
223
|
+
end
|
224
|
+
|
219
225
|
private
|
220
226
|
|
221
227
|
def execute
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'concurrent/delay'
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
#
|
5
|
+
# JobPerformer queries the database for jobs and performs them on behalf of a
|
6
|
+
# {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
|
7
|
+
# it should be executing.
|
8
|
+
#
|
9
|
+
# The JobPerformer must be safe to execute across multiple threads.
|
10
|
+
#
|
11
|
+
class JobPerformer
|
12
|
+
# @param queue_string [String] Queues to execute jobs from
|
13
|
+
def initialize(queue_string)
|
14
|
+
@queue_string = queue_string
|
15
|
+
|
16
|
+
@job_query = Concurrent::Delay.new { GoodJob::Job.queue_string(queue_string) }
|
17
|
+
@parsed_queues = Concurrent::Delay.new { GoodJob::Job.queue_parser(queue_string) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# A meaningful name to identify the performer in logs and for debugging.
|
21
|
+
# @return [String] The queues from which Jobs are worked
|
22
|
+
def name
|
23
|
+
@queue_string
|
24
|
+
end
|
25
|
+
|
26
|
+
# Perform the next eligible job
|
27
|
+
# @return [nil, Object] Returns job result or +nil+ if no job was found
|
28
|
+
def next
|
29
|
+
job_query.perform_with_advisory_lock
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tests whether this performer should be used in GoodJob's current state.
|
33
|
+
#
|
34
|
+
# For example, state will be a LISTEN/NOTIFY message that is passed down
|
35
|
+
# from the Notifier to the Scheduler. The Scheduler is able to ask
|
36
|
+
# its performer "does this message relate to you?", and if not, ignore it
|
37
|
+
# to minimize thread wake-ups, database queries, and thundering herds.
|
38
|
+
#
|
39
|
+
# @return [Boolean] whether the performer's {#next} method should be
|
40
|
+
# called in the current state.
|
41
|
+
def next?(state = {})
|
42
|
+
if parsed_queues[:exclude]
|
43
|
+
parsed_queues[:exclude].exclude?(state[:queue_name])
|
44
|
+
elsif parsed_queues[:include]
|
45
|
+
parsed_queues[:include].include?(state[:queue_name])
|
46
|
+
else
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_reader :queue_string
|
54
|
+
|
55
|
+
def job_query
|
56
|
+
@job_query.value
|
57
|
+
end
|
58
|
+
|
59
|
+
def parsed_queues
|
60
|
+
@parsed_queues.value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -92,8 +92,6 @@ module GoodJob
|
|
92
92
|
# @return [ActiveRecord::Relation]
|
93
93
|
scope :owns_advisory_locked, -> { joins_advisory_locks.where('"pg_locks"."pid" = pg_backend_pid()') }
|
94
94
|
|
95
|
-
# @!attribute [r] create_with_advisory_lock
|
96
|
-
# @return [Boolean]
|
97
95
|
# Whether an advisory lock should be acquired in the same transaction
|
98
96
|
# that created the record.
|
99
97
|
#
|
@@ -107,6 +105,8 @@ module GoodJob
|
|
107
105
|
# record = MyLockableRecord.create(create_with_advisory_lock: true)
|
108
106
|
# record.advisory_locked?
|
109
107
|
# => true
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
110
|
attr_accessor :create_with_advisory_lock
|
111
111
|
|
112
112
|
after_create -> { advisory_lock }, if: :create_with_advisory_lock
|
@@ -141,9 +141,9 @@ module GoodJob
|
|
141
141
|
end
|
142
142
|
|
143
143
|
def supports_cte_materialization_specifiers?
|
144
|
-
return @
|
144
|
+
return @_supports_cte_materialization_specifiers if defined?(@_supports_cte_materialization_specifiers)
|
145
145
|
|
146
|
-
@
|
146
|
+
@_supports_cte_materialization_specifiers = ActiveRecord::Base.connection.postgresql_version >= 120000
|
147
147
|
end
|
148
148
|
end
|
149
149
|
|
@@ -153,10 +153,12 @@ module GoodJob
|
|
153
153
|
# all remaining locks).
|
154
154
|
# @return [Boolean] whether the lock was acquired.
|
155
155
|
def advisory_lock
|
156
|
-
|
157
|
-
|
156
|
+
query = <<~SQL.squish
|
157
|
+
SELECT 1 AS one
|
158
|
+
WHERE pg_try_advisory_lock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
|
158
159
|
SQL
|
159
|
-
|
160
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
161
|
+
ActiveRecord::Base.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
|
160
162
|
end
|
161
163
|
|
162
164
|
# Releases an advisory lock on this record if it is locked by this database
|
@@ -166,9 +168,10 @@ module GoodJob
|
|
166
168
|
def advisory_unlock
|
167
169
|
query = <<~SQL.squish
|
168
170
|
SELECT 1 AS one
|
169
|
-
WHERE pg_advisory_unlock(('x'||substr(md5(
|
171
|
+
WHERE pg_advisory_unlock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
|
170
172
|
SQL
|
171
|
-
|
173
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
174
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).any?
|
172
175
|
end
|
173
176
|
|
174
177
|
# Acquires an advisory lock on this record or raises
|
@@ -205,13 +208,32 @@ module GoodJob
|
|
205
208
|
# Tests whether this record has an advisory lock on it.
|
206
209
|
# @return [Boolean]
|
207
210
|
def advisory_locked?
|
208
|
-
|
211
|
+
query = <<~SQL.squish
|
212
|
+
SELECT 1 AS one
|
213
|
+
FROM pg_locks
|
214
|
+
WHERE pg_locks.locktype = 'advisory'
|
215
|
+
AND pg_locks.objsubid = 1
|
216
|
+
AND pg_locks.classid = ('x' || substr(md5($1 || $2::text), 1, 16))::bit(32)::int
|
217
|
+
AND pg_locks.objid = (('x' || substr(md5($3 || $4::text), 1, 16))::bit(64) << 32)::bit(32)::int
|
218
|
+
SQL
|
219
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)], [nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
220
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
|
209
221
|
end
|
210
222
|
|
211
223
|
# Tests whether this record is locked by the current database session.
|
212
224
|
# @return [Boolean]
|
213
225
|
def owns_advisory_lock?
|
214
|
-
|
226
|
+
query = <<~SQL.squish
|
227
|
+
SELECT 1 AS one
|
228
|
+
FROM pg_locks
|
229
|
+
WHERE pg_locks.locktype = 'advisory'
|
230
|
+
AND pg_locks.objsubid = 1
|
231
|
+
AND pg_locks.classid = ('x' || substr(md5($1 || $2::text), 1, 16))::bit(32)::int
|
232
|
+
AND pg_locks.objid = (('x' || substr(md5($3 || $4::text), 1, 16))::bit(64) << 32)::bit(32)::int
|
233
|
+
AND pg_locks.pid = pg_backend_pid()
|
234
|
+
SQL
|
235
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)], [nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
236
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
|
215
237
|
end
|
216
238
|
|
217
239
|
# Releases all advisory locks on the record that are held by the current
|
@@ -227,5 +249,14 @@ module GoodJob
|
|
227
249
|
# Made public in Rails 5.2
|
228
250
|
self.class.send(:sanitize_sql_for_conditions, *args)
|
229
251
|
end
|
252
|
+
|
253
|
+
def pg_or_jdbc_query(query)
|
254
|
+
if Concurrent.on_jruby?
|
255
|
+
# Replace $1 bind parameters with ?
|
256
|
+
query.gsub(/\$\d*/, '?')
|
257
|
+
else
|
258
|
+
query
|
259
|
+
end
|
260
|
+
end
|
230
261
|
end
|
231
262
|
end
|