em-pg-client-12 0.3.4
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.
- 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
|