que 0.14.3 → 1.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- 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
|