que 0.14.3 → 1.0.0.beta
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 +5 -5
- data/.gitignore +2 -0
- data/CHANGELOG.md +108 -14
- data/LICENSE.txt +1 -1
- data/README.md +49 -45
- data/bin/command_line_interface.rb +239 -0
- data/bin/que +8 -82
- data/docs/README.md +2 -0
- data/docs/active_job.md +6 -0
- data/docs/advanced_setup.md +7 -64
- data/docs/command_line_interface.md +45 -0
- data/docs/error_handling.md +65 -18
- data/docs/inspecting_the_queue.md +30 -80
- data/docs/job_helper_methods.md +27 -0
- data/docs/logging.md +3 -22
- data/docs/managing_workers.md +6 -61
- data/docs/middleware.md +15 -0
- data/docs/migrating.md +4 -7
- data/docs/multiple_queues.md +8 -4
- data/docs/shutting_down_safely.md +1 -1
- data/docs/using_plain_connections.md +39 -15
- data/docs/using_sequel.md +5 -3
- data/docs/writing_reliable_jobs.md +15 -24
- data/lib/que.rb +98 -182
- data/lib/que/active_job/extensions.rb +97 -0
- data/lib/que/active_record/connection.rb +51 -0
- data/lib/que/active_record/model.rb +48 -0
- data/lib/que/connection.rb +179 -0
- data/lib/que/connection_pool.rb +78 -0
- data/lib/que/job.rb +107 -156
- data/lib/que/job_cache.rb +240 -0
- data/lib/que/job_methods.rb +168 -0
- data/lib/que/listener.rb +176 -0
- data/lib/que/locker.rb +466 -0
- data/lib/que/metajob.rb +47 -0
- data/lib/que/migrations.rb +24 -17
- data/lib/que/migrations/4/down.sql +48 -0
- data/lib/que/migrations/4/up.sql +265 -0
- data/lib/que/poller.rb +267 -0
- data/lib/que/rails/railtie.rb +14 -0
- data/lib/que/result_queue.rb +35 -0
- data/lib/que/sequel/model.rb +51 -0
- data/lib/que/utils/assertions.rb +62 -0
- data/lib/que/utils/constantization.rb +19 -0
- data/lib/que/utils/error_notification.rb +68 -0
- data/lib/que/utils/freeze.rb +20 -0
- data/lib/que/utils/introspection.rb +50 -0
- data/lib/que/utils/json_serialization.rb +21 -0
- data/lib/que/utils/logging.rb +78 -0
- data/lib/que/utils/middleware.rb +33 -0
- data/lib/que/utils/queue_management.rb +18 -0
- data/lib/que/utils/transactions.rb +34 -0
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +128 -167
- data/que.gemspec +13 -2
- metadata +37 -80
- data/.rspec +0 -2
- data/.travis.yml +0 -64
- data/Gemfile +0 -24
- data/docs/customizing_que.md +0 -200
- data/lib/generators/que/install_generator.rb +0 -24
- data/lib/generators/que/templates/add_que.rb +0 -13
- data/lib/que/adapters/active_record.rb +0 -40
- data/lib/que/adapters/base.rb +0 -133
- data/lib/que/adapters/connection_pool.rb +0 -16
- data/lib/que/adapters/pg.rb +0 -21
- data/lib/que/adapters/pond.rb +0 -16
- data/lib/que/adapters/sequel.rb +0 -20
- data/lib/que/railtie.rb +0 -16
- data/lib/que/rake_tasks.rb +0 -59
- data/lib/que/sql.rb +0 -170
- data/spec/adapters/active_record_spec.rb +0 -175
- data/spec/adapters/connection_pool_spec.rb +0 -22
- data/spec/adapters/pg_spec.rb +0 -41
- data/spec/adapters/pond_spec.rb +0 -22
- data/spec/adapters/sequel_spec.rb +0 -57
- data/spec/gemfiles/Gemfile.current +0 -19
- data/spec/gemfiles/Gemfile.old +0 -19
- data/spec/gemfiles/Gemfile.older +0 -19
- data/spec/gemfiles/Gemfile.oldest +0 -19
- data/spec/spec_helper.rb +0 -129
- data/spec/support/helpers.rb +0 -25
- data/spec/support/jobs.rb +0 -35
- data/spec/support/shared_examples/adapter.rb +0 -42
- data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
- data/spec/unit/configuration_spec.rb +0 -31
- data/spec/unit/connection_spec.rb +0 -14
- data/spec/unit/customization_spec.rb +0 -251
- data/spec/unit/enqueue_spec.rb +0 -245
- data/spec/unit/helper_spec.rb +0 -12
- data/spec/unit/logging_spec.rb +0 -101
- data/spec/unit/migrations_spec.rb +0 -84
- data/spec/unit/pool_spec.rb +0 -365
- data/spec/unit/run_spec.rb +0 -14
- data/spec/unit/states_spec.rb +0 -50
- data/spec/unit/stats_spec.rb +0 -46
- data/spec/unit/transaction_spec.rb +0 -36
- data/spec/unit/work_spec.rb +0 -596
- data/spec/unit/worker_spec.rb +0 -167
- data/tasks/benchmark.rb +0 -3
- data/tasks/rspec.rb +0 -14
- data/tasks/safe_shutdown.rb +0 -67
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module ActiveJob
|
5
|
+
# A module that devs can include into their ApplicationJob classes to get
|
6
|
+
# access to Que-like job behavior.
|
7
|
+
module JobExtensions
|
8
|
+
include JobMethods
|
9
|
+
|
10
|
+
def run(*args)
|
11
|
+
raise Error, "Job class #{self.class} didn't define a run() method!"
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(*args)
|
15
|
+
Que.internal_log(:active_job_perform, self) do
|
16
|
+
{args: args}
|
17
|
+
end
|
18
|
+
|
19
|
+
_run(
|
20
|
+
args: Que.recursively_freeze(
|
21
|
+
que_filter_args(
|
22
|
+
args.map { |a| a.is_a?(Hash) ? a.deep_symbolize_keys : a }
|
23
|
+
)
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Have helper methods like `destroy` and `retry_in` delegate to the actual
|
31
|
+
# job object. If the current job is being run through an ActiveJob adapter
|
32
|
+
# other than Que's, this will return nil, which is fine.
|
33
|
+
def que_target
|
34
|
+
Thread.current[:que_current_job]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Filter out :_aj_symbol_keys constructs so that keywords work as
|
38
|
+
# expected.
|
39
|
+
def que_filter_args(thing)
|
40
|
+
case thing
|
41
|
+
when Array
|
42
|
+
thing.map { |t| que_filter_args(t) }
|
43
|
+
when Hash
|
44
|
+
thing.each_with_object({}) do |(k, v), hash|
|
45
|
+
hash[k] = que_filter_args(v) unless k == :_aj_symbol_keys
|
46
|
+
end
|
47
|
+
else
|
48
|
+
thing
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# A module that we mix into ActiveJob's wrapper for Que::Job, to maintain
|
54
|
+
# backwards-compatibility with internal changes we make.
|
55
|
+
module WrapperExtensions
|
56
|
+
# The Rails adapter (built against a pre-1.0 version of this gem)
|
57
|
+
# assumes that it can access a job's id via job.attrs["job_id"]. So,
|
58
|
+
# oblige it.
|
59
|
+
def attrs
|
60
|
+
{"job_id" => que_attrs[:id]}
|
61
|
+
end
|
62
|
+
|
63
|
+
def run(args)
|
64
|
+
# Our ActiveJob extensions expect to be able to operate on the actual
|
65
|
+
# job object, but there's no way to access it through ActiveJob. So,
|
66
|
+
# scope it to the current thread. It's a bit messy, but it's the best
|
67
|
+
# option under the circumstances (doesn't require hacking ActiveJob in
|
68
|
+
# any more extensive way).
|
69
|
+
|
70
|
+
# There's no reason this logic should ever nest, because it wouldn't
|
71
|
+
# make sense to run a worker inside of a job, but even so, assert that
|
72
|
+
# nothing absurd is going on.
|
73
|
+
Que.assert NilClass, Thread.current[:que_current_job]
|
74
|
+
|
75
|
+
begin
|
76
|
+
Thread.current[:que_current_job] = self
|
77
|
+
|
78
|
+
# We symbolize the args hash but ActiveJob doesn't like that :/
|
79
|
+
super(args.deep_stringify_keys)
|
80
|
+
ensure
|
81
|
+
# Also assert that the current job state was only removed now, but
|
82
|
+
# unset the job first so that an assertion failure doesn't mess up
|
83
|
+
# the state any more than it already has.
|
84
|
+
current = Thread.current[:que_current_job]
|
85
|
+
Thread.current[:que_current_job] = nil
|
86
|
+
Que.assert(self, current)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ActiveJob::QueueAdapters::QueAdapter
|
94
|
+
class JobWrapper < Que::Job
|
95
|
+
prepend Que::ActiveJob::WrapperExtensions
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module ActiveRecord
|
5
|
+
module Connection
|
6
|
+
class << self
|
7
|
+
private
|
8
|
+
|
9
|
+
# Check out a PG::Connection object from ActiveRecord's pool.
|
10
|
+
def checkout
|
11
|
+
wrap_in_rails_executor do
|
12
|
+
::ActiveRecord::Base.connection_pool.with_connection do |conn|
|
13
|
+
yield conn.raw_connection
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Use Rails' executor (if present) to make sure that the connection
|
19
|
+
# we're using isn't taken from us while the block runs. See
|
20
|
+
# https://github.com/chanks/que/issues/166#issuecomment-274218910
|
21
|
+
def wrap_in_rails_executor
|
22
|
+
if defined?(::Rails.application.executor)
|
23
|
+
::Rails.application.executor.wrap { yield }
|
24
|
+
else
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Middleware
|
31
|
+
class << self
|
32
|
+
def call(job)
|
33
|
+
yield
|
34
|
+
|
35
|
+
# ActiveRecord will check out connections to the current thread when
|
36
|
+
# queries are executed and not return them to the pool until
|
37
|
+
# explicitly requested to. I'm not wild about this API design, and
|
38
|
+
# it doesn't pose a problem for the typical case of workers using a
|
39
|
+
# single PG connection (since we ensure that connection is checked
|
40
|
+
# in and checked out responsibly), but since ActiveRecord supports
|
41
|
+
# connections to multiple databases, it's easy for people using that
|
42
|
+
# feature to unknowingly leak connections to other databases. So,
|
43
|
+
# take the additional step of telling ActiveRecord to check in all
|
44
|
+
# of the current thread's connections after each job is run.
|
45
|
+
::ActiveRecord::Base.clear_active_connections!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module ActiveRecord
|
5
|
+
class Model < ::ActiveRecord::Base
|
6
|
+
self.table_name = :que_jobs
|
7
|
+
|
8
|
+
t = arel_table
|
9
|
+
|
10
|
+
scope :errored, -> { where(t[:error_count].gt(0)) }
|
11
|
+
scope :not_errored, -> { where(t[:error_count].eq(0)) }
|
12
|
+
|
13
|
+
scope :expired, -> { where(t[:expired_at].not_eq(nil)) }
|
14
|
+
scope :not_expired, -> { where(t[:expired_at].eq(nil)) }
|
15
|
+
|
16
|
+
scope :finished, -> { where(t[:finished_at].not_eq(nil)) }
|
17
|
+
scope :not_finished, -> { where(t[:finished_at].eq(nil)) }
|
18
|
+
|
19
|
+
scope :scheduled, -> { where(t[:run_at].gt("now()")) }
|
20
|
+
scope :not_scheduled, -> { where(t[:run_at].lteq("now()")) }
|
21
|
+
|
22
|
+
scope :ready, -> { not_errored.not_expired.not_finished.not_scheduled }
|
23
|
+
scope :not_ready, -> { where(t[:error_count].gt(0).or(t[:expired_at].not_eq(nil)).or(t[:finished_at].not_eq(nil)).or(t[:run_at].gt("now()"))) }
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def by_job_class(job_class)
|
27
|
+
job_class = job_class.name if job_class.is_a?(Class)
|
28
|
+
where(
|
29
|
+
"que_jobs.job_class = ? OR (que_jobs.job_class = 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper' AND que_jobs.args->0->>'job_class' = ?)",
|
30
|
+
job_class, job_class,
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def by_queue(queue)
|
35
|
+
where(arel_table[:queue].eq(queue))
|
36
|
+
end
|
37
|
+
|
38
|
+
def by_tag(tag)
|
39
|
+
where("que_jobs.data @> ?", JSON.dump(tags: [tag]))
|
40
|
+
end
|
41
|
+
|
42
|
+
def by_args(*args)
|
43
|
+
where("que_jobs.args @> ?", JSON.dump(args))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A simple wrapper class around connections that basically just improves the
|
4
|
+
# query API a bit. Currently, our connection pool wrapper discards these
|
5
|
+
# connection wrappers once the connection is returned to the source connection
|
6
|
+
# pool, so this class isn't currently suitable for storing data about the
|
7
|
+
# connection long-term (like what statements it has prepared, for example).
|
8
|
+
|
9
|
+
# If we wanted to do that, we'd probably need to sneak a reference to the
|
10
|
+
# wrapper into the PG::Connection object itself, by just setting a instance
|
11
|
+
# variable that's something namespaced and hopefully safe, like
|
12
|
+
# `@que_connection_wrapper`. It's a bit ugly, but it should ensure that we don't
|
13
|
+
# cause any memory leaks in esoteric setups where one-off connections are being
|
14
|
+
# established and then garbage-collected.
|
15
|
+
|
16
|
+
require 'time' # For Time.parse
|
17
|
+
require 'set'
|
18
|
+
|
19
|
+
module Que
|
20
|
+
class Connection
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
attr_reader :wrapped_connection
|
24
|
+
|
25
|
+
def_delegators :wrapped_connection, :backend_pid, :wait_for_notify
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def wrap(conn)
|
29
|
+
case conn
|
30
|
+
when self
|
31
|
+
conn
|
32
|
+
when PG::Connection
|
33
|
+
conn.instance_variable_get(:@que_wrapper) ||
|
34
|
+
conn.instance_variable_set(:@que_wrapper, new(conn))
|
35
|
+
else
|
36
|
+
raise Error, "Unsupported input for Connection.wrap: #{conn.class}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(connection)
|
42
|
+
@wrapped_connection = connection
|
43
|
+
@prepared_statements = Set.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def execute(command, params = nil)
|
47
|
+
sql =
|
48
|
+
case command
|
49
|
+
when Symbol then SQL[command]
|
50
|
+
when String then command
|
51
|
+
else raise Error, "Bad command! #{command.inspect}"
|
52
|
+
end
|
53
|
+
|
54
|
+
params = convert_params(params) if params
|
55
|
+
result = execute_sql(sql, params)
|
56
|
+
|
57
|
+
Que.internal_log :connection_execute, self do
|
58
|
+
{
|
59
|
+
backend_pid: backend_pid,
|
60
|
+
command: command,
|
61
|
+
params: params,
|
62
|
+
ntuples: result.ntuples,
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
convert_result(result)
|
67
|
+
end
|
68
|
+
|
69
|
+
def execute_prepared(command, params = nil)
|
70
|
+
Que.assert(Symbol, command)
|
71
|
+
|
72
|
+
if !Que.use_prepared_statements || in_transaction?
|
73
|
+
return execute(command, params)
|
74
|
+
end
|
75
|
+
|
76
|
+
name = "que_#{command}"
|
77
|
+
|
78
|
+
begin
|
79
|
+
unless @prepared_statements.include?(command)
|
80
|
+
wrapped_connection.prepare(name, SQL[command])
|
81
|
+
@prepared_statements.add(command)
|
82
|
+
prepared_just_now = true
|
83
|
+
end
|
84
|
+
|
85
|
+
convert_result(
|
86
|
+
wrapped_connection.exec_prepared(name, params)
|
87
|
+
)
|
88
|
+
rescue ::PG::InvalidSqlStatementName => error
|
89
|
+
# Reconnections on ActiveRecord can cause the same connection
|
90
|
+
# objects to refer to new backends, so recover as well as we can.
|
91
|
+
|
92
|
+
unless prepared_just_now
|
93
|
+
Que.log level: :warn, event: :reprepare_statement, command: command
|
94
|
+
@prepared_statements.delete(command)
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
|
98
|
+
raise error
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def next_notification
|
103
|
+
wrapped_connection.notifies
|
104
|
+
end
|
105
|
+
|
106
|
+
def drain_notifications
|
107
|
+
loop { break if next_notification.nil? }
|
108
|
+
end
|
109
|
+
|
110
|
+
def in_transaction?
|
111
|
+
wrapped_connection.transaction_status != ::PG::PQTRANS_IDLE
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def convert_params(params)
|
117
|
+
params.map do |param|
|
118
|
+
case param
|
119
|
+
when Time
|
120
|
+
# The pg gem unfortunately doesn't convert fractions of time
|
121
|
+
# instances, so cast them to a string.
|
122
|
+
param.strftime('%Y-%m-%d %H:%M:%S.%6N %z')
|
123
|
+
when Array, Hash
|
124
|
+
# Handle JSON.
|
125
|
+
Que.serialize_json(param)
|
126
|
+
else
|
127
|
+
param
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def execute_sql(sql, params)
|
133
|
+
# Some versions of the PG gem dislike an empty/nil params argument.
|
134
|
+
if params && !params.empty?
|
135
|
+
wrapped_connection.async_exec(sql, params)
|
136
|
+
else
|
137
|
+
wrapped_connection.async_exec(sql)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Procs used to convert strings from Postgres into Ruby types.
|
142
|
+
CAST_PROCS = {
|
143
|
+
# Boolean
|
144
|
+
16 => 't'.method(:==),
|
145
|
+
# Timestamp with time zone
|
146
|
+
1184 => Time.method(:parse),
|
147
|
+
}
|
148
|
+
|
149
|
+
# JSON, JSONB
|
150
|
+
CAST_PROCS[114] = CAST_PROCS[3802] = -> (j) { Que.deserialize_json(j) }
|
151
|
+
|
152
|
+
# Integer, bigint, smallint
|
153
|
+
CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
|
154
|
+
|
155
|
+
CAST_PROCS.freeze
|
156
|
+
|
157
|
+
def convert_result(result)
|
158
|
+
output = result.to_a
|
159
|
+
|
160
|
+
result.fields.each_with_index do |field, index|
|
161
|
+
symbol = field.to_sym
|
162
|
+
|
163
|
+
if converter = CAST_PROCS[result.ftype(index)]
|
164
|
+
output.each do |hash|
|
165
|
+
value = hash.delete(field)
|
166
|
+
value = converter.call(value) if value
|
167
|
+
hash[symbol] = value
|
168
|
+
end
|
169
|
+
else
|
170
|
+
output.each do |hash|
|
171
|
+
hash[symbol] = hash.delete(field)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
output
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A wrapper around whatever connection pool we're using. Mainly just asserts
|
4
|
+
# that the source connection pool is reentrant and thread-safe.
|
5
|
+
|
6
|
+
module Que
|
7
|
+
class ConnectionPool
|
8
|
+
def initialize(&block)
|
9
|
+
@connection_proc = block
|
10
|
+
@checked_out = Set.new
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@thread_key = "que_connection_pool_#{object_id}".to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def checkout
|
16
|
+
# Do some asserting to ensure that the connection pool we're using is
|
17
|
+
# behaving properly.
|
18
|
+
@connection_proc.call do |conn|
|
19
|
+
# Did this pool already have a connection for this thread?
|
20
|
+
preexisting = wrapped = current_connection
|
21
|
+
|
22
|
+
begin
|
23
|
+
if preexisting
|
24
|
+
# If so, check that the connection we just got is the one we expect.
|
25
|
+
if preexisting.wrapped_connection.backend_pid != conn.backend_pid
|
26
|
+
raise Error, "Connection pool is not reentrant! previous: #{preexisting.wrapped_connection.inspect} now: #{conn.inspect}"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
# If not, make sure that it wasn't promised to any other threads.
|
30
|
+
sync do
|
31
|
+
Que.assert(@checked_out.add?(conn.backend_pid)) do
|
32
|
+
"Connection pool didn't synchronize access properly! (entrance: #{conn.backend_pid})"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
self.current_connection = wrapped = Connection.wrap(conn)
|
37
|
+
end
|
38
|
+
|
39
|
+
yield(wrapped)
|
40
|
+
ensure
|
41
|
+
if preexisting.nil?
|
42
|
+
# We're at the top level (about to return this connection to the
|
43
|
+
# pool we got it from), so mark it as no longer ours.
|
44
|
+
self.current_connection = nil
|
45
|
+
|
46
|
+
sync do
|
47
|
+
Que.assert(@checked_out.delete?(conn.backend_pid)) do
|
48
|
+
"Connection pool didn't synchronize access properly! (exit: #{conn.backend_pid})"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute(*args)
|
57
|
+
checkout { |conn| conn.execute(*args) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def in_transaction?
|
61
|
+
checkout { |conn| conn.in_transaction? }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def sync
|
67
|
+
@mutex.synchronize { yield }
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_connection
|
71
|
+
Thread.current[@thread_key]
|
72
|
+
end
|
73
|
+
|
74
|
+
def current_connection=(c)
|
75
|
+
Thread.current[@thread_key] = c
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|