em-pg-client 0.2.1 → 0.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.
- data/.yardopts +1 -0
- data/{BENCHMARKS.rdoc → BENCHMARKS.md} +15 -10
- data/{HISTORY.rdoc → HISTORY.md} +32 -5
- data/LICENSE +21 -0
- data/README.md +392 -0
- data/Rakefile +30 -14
- data/em-pg-client.gemspec +8 -7
- data/lib/em-pg-client.rb +1 -0
- data/lib/em-synchrony/pg.rb +2 -107
- data/lib/pg/em-version.rb +5 -0
- data/lib/pg/em.rb +638 -344
- data/lib/pg/em/client/connect_watcher.rb +65 -0
- data/lib/pg/em/client/watcher.rb +102 -0
- data/lib/pg/em/connection_pool.rb +448 -0
- data/lib/pg/em/featured_deferrable.rb +43 -0
- data/spec/connection_pool_helpers.rb +89 -0
- data/spec/{em_devel_client.rb → em_client.rb} +3 -2
- data/spec/em_client_autoreconnect.rb +268 -144
- data/spec/em_client_common.rb +55 -54
- data/spec/em_synchrony_client.rb +254 -5
- data/spec/em_synchrony_client_autoreconnect.rb +154 -130
- data/spec/pg_em_client_connect_finish.rb +54 -0
- data/spec/pg_em_client_connect_timeout.rb +91 -0
- data/spec/pg_em_client_options.rb +85 -0
- data/spec/pg_em_connection_pool.rb +655 -0
- data/spec/pg_em_featured_deferrable.rb +125 -0
- metadata +64 -34
- data/README.rdoc +0 -431
- data/spec/em_release_client.rb +0 -39
@@ -0,0 +1,65 @@
|
|
1
|
+
module PG
|
2
|
+
module EM
|
3
|
+
class Client < PG::Connection
|
4
|
+
|
5
|
+
# This module is used as a handler to ::EM.watch connection socket and
|
6
|
+
# it performs connection handshake with postgres server asynchronously.
|
7
|
+
#
|
8
|
+
# Author:: Rafal Michalski
|
9
|
+
module ConnectWatcher
|
10
|
+
|
11
|
+
def initialize(client, deferrable, is_reset)
|
12
|
+
@client = client
|
13
|
+
@deferrable = deferrable
|
14
|
+
@is_reset = is_reset
|
15
|
+
@poll_method = is_reset ? :reset_poll : :connect_poll
|
16
|
+
if (timeout = client.connect_timeout) > 0
|
17
|
+
@timer = ::EM::Timer.new(timeout) do
|
18
|
+
detach
|
19
|
+
@deferrable.protect do
|
20
|
+
error = ConnectionBad.new("timeout expired (async)")
|
21
|
+
error.instance_variable_set(:@connection, @client)
|
22
|
+
raise error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reconnecting?
|
29
|
+
@is_reset
|
30
|
+
end
|
31
|
+
|
32
|
+
def poll_connection_and_check
|
33
|
+
case @client.__send__(@poll_method)
|
34
|
+
when PG::PGRES_POLLING_READING
|
35
|
+
self.notify_readable = true
|
36
|
+
self.notify_writable = false
|
37
|
+
return
|
38
|
+
when PG::PGRES_POLLING_WRITING
|
39
|
+
self.notify_writable = true
|
40
|
+
self.notify_readable = false
|
41
|
+
return
|
42
|
+
when PG::PGRES_POLLING_OK
|
43
|
+
polling_ok = true if @client.status == PG::CONNECTION_OK
|
44
|
+
end
|
45
|
+
@timer.cancel if @timer
|
46
|
+
detach
|
47
|
+
@deferrable.protect_and_succeed do
|
48
|
+
unless polling_ok
|
49
|
+
error = ConnectionBad.new(@client.error_message)
|
50
|
+
error.instance_variable_set(:@connection, @client)
|
51
|
+
raise error
|
52
|
+
end
|
53
|
+
@client.set_default_encoding unless reconnecting?
|
54
|
+
@client
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :notify_writable, :poll_connection_and_check
|
59
|
+
alias_method :notify_readable, :poll_connection_and_check
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module PG
|
2
|
+
module EM
|
3
|
+
class Client < PG::Connection
|
4
|
+
|
5
|
+
# This module is used as a handler to ::EM.watch connection socket and
|
6
|
+
# it extracts query results in a non-blocking manner.
|
7
|
+
#
|
8
|
+
# Author:: Rafal Michalski
|
9
|
+
module Watcher
|
10
|
+
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
@is_connected = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def watching?
|
17
|
+
@is_connected
|
18
|
+
end
|
19
|
+
|
20
|
+
def watch_query(deferrable, send_proc)
|
21
|
+
self.notify_readable = true
|
22
|
+
@last_result = nil
|
23
|
+
@deferrable = deferrable
|
24
|
+
@send_proc = send_proc
|
25
|
+
@timer.cancel if @timer
|
26
|
+
if (timeout = @client.query_timeout) > 0
|
27
|
+
@notify_timestamp = Time.now
|
28
|
+
setup_timer timeout
|
29
|
+
else
|
30
|
+
@timer = nil
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_timer(timeout, adjustment = 0)
|
36
|
+
@timer = ::EM::Timer.new(timeout - adjustment) do
|
37
|
+
if (last_interval = Time.now - @notify_timestamp) >= timeout
|
38
|
+
@timer = nil
|
39
|
+
self.notify_readable = false
|
40
|
+
@client.async_command_aborted = true
|
41
|
+
@deferrable.protect do
|
42
|
+
error = ConnectionBad.new("query timeout expired (async)")
|
43
|
+
error.instance_variable_set(:@connection, @client)
|
44
|
+
raise error
|
45
|
+
end
|
46
|
+
else
|
47
|
+
setup_timer timeout, last_interval
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def cancel_timer
|
53
|
+
if @timer
|
54
|
+
@timer.cancel
|
55
|
+
@timer = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Carefully extract the last result without
|
60
|
+
# blocking the EventMachine reactor.
|
61
|
+
def notify_readable
|
62
|
+
result = false
|
63
|
+
@client.consume_input
|
64
|
+
until @client.is_busy
|
65
|
+
if (single_result = @client.get_result).nil?
|
66
|
+
if (result = @last_result).nil?
|
67
|
+
error = Error.new(@client.error_message)
|
68
|
+
error.instance_variable_set(:@connection, @client)
|
69
|
+
raise error
|
70
|
+
end
|
71
|
+
result.check
|
72
|
+
cancel_timer
|
73
|
+
break
|
74
|
+
end
|
75
|
+
@last_result.clear if @last_result
|
76
|
+
@last_result = single_result
|
77
|
+
end
|
78
|
+
rescue Exception => e
|
79
|
+
self.notify_readable = false
|
80
|
+
cancel_timer
|
81
|
+
if e.is_a?(PG::Error)
|
82
|
+
@client.async_autoreconnect!(@deferrable, e, &@send_proc)
|
83
|
+
else
|
84
|
+
@deferrable.fail(e)
|
85
|
+
end
|
86
|
+
else
|
87
|
+
if result == false
|
88
|
+
@notify_timestamp = Time.now if @timer
|
89
|
+
else
|
90
|
+
self.notify_readable = false
|
91
|
+
@deferrable.succeed(result)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def unbind
|
96
|
+
@is_connected = false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,448 @@
|
|
1
|
+
require 'pg/em'
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module EM
|
5
|
+
|
6
|
+
# Connection pool for PG::EM::Client
|
7
|
+
#
|
8
|
+
# Author:: Rafal Michalski
|
9
|
+
#
|
10
|
+
# The ConnectionPool allocates new connections asynchronously when
|
11
|
+
# there are no free connections left up to the {#max_size} number.
|
12
|
+
#
|
13
|
+
# If {Client#async_autoreconnect} option is not set or the re-connect fails
|
14
|
+
# the failed connection is dropped from the pool.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# pg = PG::EM::ConnectionPool.new size: 10, dbname: 'foo'
|
18
|
+
# res = pg.query 'select * from bar'
|
19
|
+
#
|
20
|
+
# The list of {Client} command methods that are available in {ConnectionPool}:
|
21
|
+
#
|
22
|
+
# Fiber synchronized methods:
|
23
|
+
#
|
24
|
+
# * {Client#exec}
|
25
|
+
# * {Client#query}
|
26
|
+
# * {Client#async_exec}
|
27
|
+
# * {Client#async_query}
|
28
|
+
# * {Client#exec_params}
|
29
|
+
# * {Client#exec_prepared}
|
30
|
+
# * {Client#prepare}
|
31
|
+
# * {Client#describe_prepared}
|
32
|
+
# * {Client#describe_portal}
|
33
|
+
#
|
34
|
+
# The asynchronous command methods:
|
35
|
+
#
|
36
|
+
# * {Client#exec_defer}
|
37
|
+
# * {Client#query_defer}
|
38
|
+
# * {Client#async_exec_defer}
|
39
|
+
# * {Client#async_query_defer}
|
40
|
+
# * {Client#exec_params_defer}
|
41
|
+
# * {Client#exec_prepared_defer}
|
42
|
+
# * {Client#prepare_defer}
|
43
|
+
# * {Client#describe_prepared_defer}
|
44
|
+
# * {Client#describe_portal_defer}
|
45
|
+
#
|
46
|
+
# The pool will only allow for {#max_size} commands (both deferred and
|
47
|
+
# fiber synchronized) to be performed concurrently. The pending requests
|
48
|
+
# will be queued and executed when connections become available.
|
49
|
+
#
|
50
|
+
# Please keep in mind, that the above methods may send commands to
|
51
|
+
# different clients from the pool each time they are called. You can't
|
52
|
+
# assume anything about which connection is acquired even if the
|
53
|
+
# {#max_size} of the pool is set to one. This is because no connection
|
54
|
+
# will be shared between two concurrent requests and the connections
|
55
|
+
# maight occasionally fail and they will be dropped from the pool.
|
56
|
+
#
|
57
|
+
# This prevents the `*_defer` commands to execute transactions.
|
58
|
+
#
|
59
|
+
# For transactions use {#transaction} and fiber synchronized methods.
|
60
|
+
class ConnectionPool
|
61
|
+
|
62
|
+
DEFAULT_SIZE = 4
|
63
|
+
|
64
|
+
# Maximum number of connections in the connection pool
|
65
|
+
# @return [Integer]
|
66
|
+
attr_reader :max_size
|
67
|
+
|
68
|
+
attr_reader :available, :allocated
|
69
|
+
|
70
|
+
# Creates and initializes a new connection pool.
|
71
|
+
#
|
72
|
+
# The connection pool allocates its first connection upon initialization
|
73
|
+
# unless +lazy: true+ option is given.
|
74
|
+
#
|
75
|
+
# Pass PG::EM::Client +options+ together with ConnectionPool +options+:
|
76
|
+
#
|
77
|
+
# - +:size+ = +4+ - the maximum number of concurrent connections
|
78
|
+
# - +:lazy+ = false - should lazy allocate first connection
|
79
|
+
# - +:connection_class+ = {PG::EM::Client}
|
80
|
+
#
|
81
|
+
# @raise [PG::Error]
|
82
|
+
# @raise [ArgumentError]
|
83
|
+
def initialize(options = {})
|
84
|
+
@available = []
|
85
|
+
@pending = []
|
86
|
+
@allocated = {}
|
87
|
+
@connection_class = Client
|
88
|
+
|
89
|
+
lazy = false
|
90
|
+
@options = options.reject do |key, value|
|
91
|
+
case key.to_sym
|
92
|
+
when :size, :max_size
|
93
|
+
@max_size = value.to_i
|
94
|
+
true
|
95
|
+
when :connection_class
|
96
|
+
@connection_class = value
|
97
|
+
true
|
98
|
+
when :lazy
|
99
|
+
lazy = value
|
100
|
+
true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
@max_size ||= DEFAULT_SIZE
|
105
|
+
|
106
|
+
raise ArgumentError, "#{self.class}.new: pool size must be > 1" if @max_size < 1
|
107
|
+
|
108
|
+
# allocate first connection, unless we are lazy
|
109
|
+
hold unless lazy
|
110
|
+
end
|
111
|
+
|
112
|
+
# Creates and initializes new connection pool.
|
113
|
+
#
|
114
|
+
# Attempts to establish the first connection asynchronously.
|
115
|
+
#
|
116
|
+
# @return [FeaturedDeferrable]
|
117
|
+
# @yieldparam pg [Client|PG::Error] new and connected client instance
|
118
|
+
# on success or a raised PG::Error
|
119
|
+
#
|
120
|
+
# Use the returned deferrable's +callback+ hook to obtain newly created
|
121
|
+
# {ConnectionPool}.
|
122
|
+
# In case of a connection error +errback+ hook is called with
|
123
|
+
# a raised error object as its argument.
|
124
|
+
#
|
125
|
+
# If the block is provided it's bound to both +callback+ and +errback+
|
126
|
+
# hooks of the returned deferrable.
|
127
|
+
#
|
128
|
+
# Pass PG::EM::Client +options+ together with ConnectionPool +options+:
|
129
|
+
#
|
130
|
+
# - +:size+ = +4+ - the maximum number of concurrent connections
|
131
|
+
# - +:connection_class+ = {PG::EM::Client}
|
132
|
+
#
|
133
|
+
# @raise [ArgumentError]
|
134
|
+
def self.connect_defer(options = {}, &blk)
|
135
|
+
pool = new options.merge(lazy: true)
|
136
|
+
pool.__send__(:hold_deferred, blk) do
|
137
|
+
::EM::DefaultDeferrable.new.tap { |df| df.succeed pool }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class << self
|
142
|
+
alias_method :connect, :new
|
143
|
+
alias_method :async_connect, :connect_defer
|
144
|
+
end
|
145
|
+
|
146
|
+
# Current number of connections in the connection pool
|
147
|
+
#
|
148
|
+
# @return [Integer]
|
149
|
+
def size
|
150
|
+
@available.length + @allocated.length
|
151
|
+
end
|
152
|
+
|
153
|
+
# Finishes all available connections and clears the available pool.
|
154
|
+
#
|
155
|
+
# After call to this method the pool is still usable and will try to
|
156
|
+
# allocate new client connections on subsequent query commands.
|
157
|
+
def finish
|
158
|
+
@available.each { |c| c.finish }
|
159
|
+
@available.clear
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
alias_method :close, :finish
|
164
|
+
|
165
|
+
class DeferredOptions < Hash
|
166
|
+
def apply(conn)
|
167
|
+
each_pair { |n,v| conn.__send__(n, v) }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
# @!attribute [rw] connect_timeout
|
171
|
+
# @return [Float] connection timeout in seconds
|
172
|
+
# Set {Client#connect_timeout} on all present and future connections
|
173
|
+
# in this pool or read value from options
|
174
|
+
# @!attribute [rw] query_timeout
|
175
|
+
# @return [Float] query timeout in seconds
|
176
|
+
# Set {Client#query_timeout} on all present and future connections
|
177
|
+
# in this pool or read value from options
|
178
|
+
# @!attribute [rw] async_autoreconnect
|
179
|
+
# @return [Boolean] asynchronous auto re-connect status
|
180
|
+
# Set {Client#async_autoreconnect} on all present and future connections
|
181
|
+
# in this pool or read value from options
|
182
|
+
# @!attribute [rw] on_autoreconnect
|
183
|
+
# @return [Proc<Client, Error>] auto re-connect hook
|
184
|
+
# Set {Client#on_autoreconnect} on all present and future connections
|
185
|
+
# in this pool or read value from options
|
186
|
+
%w[connect_timeout
|
187
|
+
query_timeout
|
188
|
+
async_autoreconnect
|
189
|
+
on_autoreconnect].each do |name|
|
190
|
+
class_eval <<-EOD, __FILE__, __LINE__
|
191
|
+
def #{name}=(value)
|
192
|
+
@options[:#{name}] = value
|
193
|
+
b = proc { |c| c.#{name} = value }
|
194
|
+
@available.each(&b)
|
195
|
+
@allocated.each_value(&b)
|
196
|
+
end
|
197
|
+
|
198
|
+
def #{name}
|
199
|
+
@options[:#{name}] || @options['#{name}']
|
200
|
+
end
|
201
|
+
EOD
|
202
|
+
DeferredOptions.class_eval <<-EOD, __FILE__, __LINE__
|
203
|
+
def #{name}=(value)
|
204
|
+
self[:#{name}=] = value
|
205
|
+
end
|
206
|
+
EOD
|
207
|
+
end
|
208
|
+
|
209
|
+
%w(
|
210
|
+
exec
|
211
|
+
query
|
212
|
+
async_exec
|
213
|
+
async_query
|
214
|
+
exec_params
|
215
|
+
exec_prepared
|
216
|
+
prepare
|
217
|
+
describe_prepared
|
218
|
+
describe_portal
|
219
|
+
).each do |name|
|
220
|
+
|
221
|
+
class_eval <<-EOD, __FILE__, __LINE__
|
222
|
+
def #{name}(*args, &blk)
|
223
|
+
hold { |c| c.#{name}(*args, &blk) }
|
224
|
+
end
|
225
|
+
EOD
|
226
|
+
end
|
227
|
+
|
228
|
+
%w(
|
229
|
+
exec_defer
|
230
|
+
query_defer
|
231
|
+
async_query_defer
|
232
|
+
async_exec_defer
|
233
|
+
exec_params_defer
|
234
|
+
exec_prepared_defer
|
235
|
+
prepare_defer
|
236
|
+
describe_prepared_defer
|
237
|
+
describe_portal_defer
|
238
|
+
).each do |name|
|
239
|
+
|
240
|
+
class_eval <<-EOD, __FILE__, __LINE__
|
241
|
+
def #{name}(*args, &blk)
|
242
|
+
hold_deferred(blk) { |c| c.#{name}(*args) }
|
243
|
+
end
|
244
|
+
EOD
|
245
|
+
end
|
246
|
+
|
247
|
+
# Executes a BEGIN at the start of the block
|
248
|
+
# and a COMMIT at the end of the block
|
249
|
+
# or ROLLBACK if any exception occurs.
|
250
|
+
# Calls to transaction may be nested,
|
251
|
+
# however without sub-transactions (save points).
|
252
|
+
#
|
253
|
+
# @example Transactions
|
254
|
+
# pg = PG::EM::ConnectionPool.new size: 10
|
255
|
+
# pg.transaction do
|
256
|
+
# pg.exec('insert into animals (family, species) values ($1,$2)',
|
257
|
+
# [family, species])
|
258
|
+
# num = pg.query('select count(*) from people where family=$1',
|
259
|
+
# [family]).get_value(0,0)
|
260
|
+
# pg.exec('update stats set count = $1 where family=$2',
|
261
|
+
# [num, family])
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# @see Client#transaction
|
265
|
+
# @see #hold
|
266
|
+
def transaction(&blk)
|
267
|
+
hold do |pg|
|
268
|
+
pg.transaction(&blk)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Acquires {Client} connection and passes it to the given block.
|
273
|
+
#
|
274
|
+
# The connection is allocated to the current fiber and ensures that
|
275
|
+
# any subsequent query from the same fiber will be performed on
|
276
|
+
# the connection.
|
277
|
+
#
|
278
|
+
# It is possible to nest hold calls from the same fiber,
|
279
|
+
# so each time the block will be given the same {Client} instance.
|
280
|
+
# This feature is needed e.g. for nesting transaction calls.
|
281
|
+
# @yieldparam [Client] pg
|
282
|
+
def hold
|
283
|
+
fiber = Fiber.current
|
284
|
+
id = fiber.object_id
|
285
|
+
|
286
|
+
if conn = @allocated[id]
|
287
|
+
skip_release = true
|
288
|
+
else
|
289
|
+
conn = acquire(fiber) until conn
|
290
|
+
end
|
291
|
+
|
292
|
+
begin
|
293
|
+
yield conn if block_given?
|
294
|
+
|
295
|
+
rescue PG::Error
|
296
|
+
if conn.status != PG::CONNECTION_OK
|
297
|
+
conn.finish unless conn.finished?
|
298
|
+
drop_failed(id)
|
299
|
+
skip_release = true
|
300
|
+
end
|
301
|
+
raise
|
302
|
+
ensure
|
303
|
+
release(id) unless skip_release
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
alias_method :execute, :hold
|
308
|
+
|
309
|
+
def method_missing(*a, &b)
|
310
|
+
hold { |c| c.__send__(*a, &b) }
|
311
|
+
end
|
312
|
+
|
313
|
+
def respond_to_missing?(m, priv = false)
|
314
|
+
hold { |c| c.respond_to?(m, priv) }
|
315
|
+
end
|
316
|
+
|
317
|
+
private
|
318
|
+
|
319
|
+
# Get available connection or create a new one, or put on hold
|
320
|
+
# @return [Client] on success
|
321
|
+
# @return [nil] when dropped connection creates a free slot
|
322
|
+
def acquire(fiber)
|
323
|
+
if conn = @available.pop
|
324
|
+
@allocated[fiber.object_id] = conn
|
325
|
+
else
|
326
|
+
if size < max_size
|
327
|
+
begin
|
328
|
+
id = fiber.object_id
|
329
|
+
# mark allocated pool for proper #size value
|
330
|
+
# the connecting process will yield from fiber
|
331
|
+
@allocated[id] = opts = DeferredOptions.new
|
332
|
+
conn = @connection_class.new(@options)
|
333
|
+
ensure
|
334
|
+
if conn
|
335
|
+
opts.apply conn
|
336
|
+
@allocated[id] = conn
|
337
|
+
else
|
338
|
+
drop_failed(id)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
else
|
342
|
+
@pending << fiber
|
343
|
+
Fiber.yield
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Asynchronously acquires {Client} connection and passes it to the
|
349
|
+
# given block on success.
|
350
|
+
#
|
351
|
+
# The block will receive the acquired connection as its argument and
|
352
|
+
# should return a deferrable object which is either returned from
|
353
|
+
# this method or is being status-bound to another deferrable returned
|
354
|
+
# from this method.
|
355
|
+
#
|
356
|
+
# @param blk [Proc] optional block passed to +callback+ and +errback+
|
357
|
+
# of the returned deferrable object
|
358
|
+
# @yieldparam pg [Client] a connected client instance
|
359
|
+
# @yieldreturn [EM::Deferrable]
|
360
|
+
# @return [EM::Deferrable]
|
361
|
+
def hold_deferred(blk = nil)
|
362
|
+
if conn = @available.pop
|
363
|
+
id = conn.object_id
|
364
|
+
@allocated[id] = conn
|
365
|
+
df = yield conn
|
366
|
+
else
|
367
|
+
df = FeaturedDeferrable.new
|
368
|
+
id = df.object_id
|
369
|
+
acquire_deferred(df) do |nc|
|
370
|
+
@allocated[id] = conn = nc
|
371
|
+
df.bind_status yield conn
|
372
|
+
end
|
373
|
+
end
|
374
|
+
df.callback { release(id) }
|
375
|
+
df.errback do |err|
|
376
|
+
if conn
|
377
|
+
if err.is_a?(PG::Error) &&
|
378
|
+
conn.status != PG::CONNECTION_OK
|
379
|
+
conn.finish unless conn.finished?
|
380
|
+
drop_failed(id)
|
381
|
+
else
|
382
|
+
release(id)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
df.completion(&blk) if blk
|
387
|
+
df
|
388
|
+
end
|
389
|
+
|
390
|
+
# Asynchronously create a new connection or get the released one
|
391
|
+
#
|
392
|
+
# @param df [EM::Deferrable] - the acquiring object and the one to fail
|
393
|
+
# when establishing connection fails
|
394
|
+
# @return [EM::Deferrable] the deferrable that will succeed with either
|
395
|
+
# new or released connection
|
396
|
+
def acquire_deferred(df, &blk)
|
397
|
+
id = df.object_id
|
398
|
+
if size < max_size
|
399
|
+
# mark allocated pool for proper #size value
|
400
|
+
# the connection is made asynchronously
|
401
|
+
@allocated[id] = opts = DeferredOptions.new
|
402
|
+
@connection_class.connect_defer(@options).callback {|conn|
|
403
|
+
opts.apply conn
|
404
|
+
}.errback do |err|
|
405
|
+
drop_failed(id)
|
406
|
+
df.fail(err)
|
407
|
+
end
|
408
|
+
else
|
409
|
+
@pending << (conn_df = ::EM::DefaultDeferrable.new)
|
410
|
+
conn_df.errback do
|
411
|
+
# a dropped connection made a free slot
|
412
|
+
acquire_deferred(df, &blk)
|
413
|
+
end
|
414
|
+
end.callback(&blk)
|
415
|
+
end
|
416
|
+
|
417
|
+
# drop a failed connection (or a mark) from the pool and
|
418
|
+
# ensure that the pending requests won't starve
|
419
|
+
def drop_failed(id)
|
420
|
+
@allocated.delete(id)
|
421
|
+
if pending = @pending.shift
|
422
|
+
if pending.is_a?(Fiber)
|
423
|
+
pending.resume
|
424
|
+
else
|
425
|
+
pending.fail
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# release connection and pass it to the next pending
|
431
|
+
# request or back to the free pool
|
432
|
+
def release(id)
|
433
|
+
conn = @allocated.delete(id)
|
434
|
+
if pending = @pending.shift
|
435
|
+
if pending.is_a?(Fiber)
|
436
|
+
@allocated[pending.object_id] = conn
|
437
|
+
pending.resume conn
|
438
|
+
else
|
439
|
+
pending.succeed conn
|
440
|
+
end
|
441
|
+
else
|
442
|
+
@available << conn
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|