em-pg-client 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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