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