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.
@@ -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