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/Rakefile CHANGED
@@ -4,40 +4,56 @@ task :default => [:test]
4
4
 
5
5
  $gem_name = "em-pg-client"
6
6
 
7
- desc "Run spec tests"
8
- task :test, [:which] do |t, args|
9
- args.with_defaults(:which => 'safe')
7
+ def windows_os?
8
+ RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/
9
+ end
10
+
11
+ desc "Run tests"
12
+ task :test => :'test:safe'
10
13
 
14
+ namespace :test do
11
15
  env_common = {'PGDATABASE' => 'test'}
12
- env_pg_013 = {'EM_PG_CLIENT_TEST_PG_VERSION' => '= 0.13.2'}
13
16
  env_unix_socket = env_common.merge('PGHOST' => '/tmp')
14
17
  env_tcpip = env_common.merge('PGHOST' => 'localhost')
15
18
 
16
- puts "WARNING: The test needs to be run with an available local PostgreSQL server"
19
+ task :warn do
20
+ puts "WARNING: The test needs to be run with an available local PostgreSQL server"
21
+ end
22
+
23
+ desc "Run specs only"
24
+ task :spec do
25
+ sh "rspec spec/pg_em_featured_deferrable.rb"
26
+ sh "rspec spec/pg_em_client_options.rb"
27
+ sh "rspec spec/pg_em_client_connect_finish.rb"
28
+ sh "rspec spec/pg_em_client_connect_timeout.rb"
29
+ sh "rspec spec/pg_em_connection_pool.rb"
30
+ end
17
31
 
18
- if %w[all safe].include? args[:which]
32
+ desc "Run safe tests only"
33
+ task :safe => [:warn, :spec] do
19
34
  %w[
20
- spec/em_release_client.rb
21
- spec/em_devel_client.rb
35
+ spec/em_client.rb
22
36
  spec/em_synchrony_client.rb
23
37
  ].each do |spec|
24
- sh env_unix_socket, "rspec #{spec}"
38
+ sh env_unix_socket, "rspec #{spec}" unless windows_os?
25
39
  sh env_tcpip, "rspec #{spec}"
26
- sh env_pg_013.merge(env_unix_socket), "rspec #{spec}"
27
- sh env_pg_013.merge(env_tcpip), "rspec #{spec}"
28
40
  end
29
41
  end
30
42
 
31
- if %w[all unsafe dangerous autoreconnect].include? args[:which]
43
+ desc "Run unsafe tests only"
44
+ task :unsafe => :warn do
32
45
  raise "Set PGDATA environment variable before running the autoreconnect tests." unless ENV['PGDATA']
33
46
  %w[
34
47
  spec/em_client_autoreconnect.rb
35
48
  spec/em_synchrony_client_autoreconnect.rb
36
49
  ].each do |spec|
37
- sh env_unix_socket, "rspec #{spec}"
50
+ sh env_unix_socket, "rspec #{spec}" unless windows_os?
38
51
  sh env_tcpip, "rspec #{spec}"
39
52
  end
40
53
  end
54
+
55
+ desc "Run safe and unsafe tests"
56
+ task :all => [:spec, :safe, :unsafe]
41
57
  end
42
58
 
43
59
  desc "Build the gem"
@@ -62,7 +78,7 @@ end
62
78
 
63
79
  desc "Documentation"
64
80
  task :doc do
65
- sh "rdoc --encoding=UTF-8 --title=em-pg-client --main=README.rdoc README.rdoc BENCHMARKS.rdoc lib/*/*.rb"
81
+ sh "yardoc"
66
82
  end
67
83
 
68
84
  desc "Benchmark"
@@ -1,26 +1,27 @@
1
1
  $:.unshift "lib"
2
+ require 'pg/em-version'
2
3
 
3
4
  Gem::Specification.new do |s|
4
5
  s.name = "em-pg-client"
5
- s.version = "0.2.1"
6
- s.required_ruby_version = ">= 1.9.1"
6
+ s.version = PG::EM::VERSION
7
+ s.required_ruby_version = ">= 1.9.2"
7
8
  s.date = "#{Time.now.strftime("%Y-%m-%d")}"
8
9
  s.summary = "EventMachine PostgreSQL client"
9
10
  s.email = "rafal@yeondir.com"
10
11
  s.homepage = "http://github.com/royaltm/ruby-em-pg-client"
12
+ s.license = "MIT"
11
13
  s.require_path = "lib"
12
14
  s.description = "PostgreSQL asynchronous EventMachine client, based on pg interface (PG::Connection)"
13
15
  s.authors = ["Rafal Michalski"]
14
16
  s.files = `git ls-files`.split("\n") - ['.gitignore']
15
17
  s.test_files = Dir.glob("spec/**/*")
16
18
  s.rdoc_options << "--title" << "em-pg-client" <<
17
- "--main" << "README.rdoc"
19
+ "--main" << "README.md"
18
20
  s.has_rdoc = true
19
- s.extra_rdoc_files = ["README.rdoc", "BENCHMARKS.rdoc"]
21
+ s.extra_rdoc_files = ["README.md", "BENCHMARKS.md", "LICENSE", "HISTORY.md"]
20
22
  s.requirements << "PostgreSQL server"
21
- s.add_runtime_dependency "pg", ">= 0.13.2"
22
- s.add_runtime_dependency "eventmachine", ">= 0.12.10"
23
+ s.add_runtime_dependency "pg", ">= 0.17.0"
24
+ s.add_runtime_dependency "eventmachine", "~> 1.0.0"
23
25
  s.add_development_dependency "rspec", "~> 2.8.0"
24
- s.add_development_dependency "eventmachine", ">= 1.0.0.beta.1"
25
26
  s.add_development_dependency "em-synchrony", "~> 1.0.0"
26
27
  end
@@ -0,0 +1 @@
1
+ require 'pg/em'
@@ -1,108 +1,3 @@
1
+ # for backward compatibility with
2
+ # require 'em-synchrony/pg'
1
3
  require 'pg/em'
2
- module PG
3
- module EM
4
- class Client
5
- # Author:: Rafal Michalski (mailto:royaltm75@gmail.com)
6
- # Licence:: MIT License
7
- #
8
- # =PostgreSQL Client for EM-Synchrony/Fibered EventMachine
9
- #
10
-
11
- # conform to *standard*
12
- alias_method :aquery, :async_query
13
-
14
- # fiber aware methods:
15
- # - exec (aliased as query)
16
- # - exec_prepared
17
- # - prepare
18
- # - describe_prepared
19
- # - describe_portal
20
- # - reset
21
- # - Client.connect
22
- %w(exec
23
- exec_prepared
24
- prepare
25
- describe_prepared
26
- describe_portal
27
- reset
28
- self.connect).each do |name|
29
- async_name = "async_#{name.split('.').last}"
30
- blocking_call = case name
31
- when 'reset'
32
- '@async_command_aborted = false
33
- super(*args, &blk)'
34
- else
35
- 'super(*args, &blk)'
36
- end
37
- clear_method = case name
38
- when 'reset', 'self.connect'
39
- 'finish'
40
- else
41
- 'clear'
42
- end
43
- class_eval <<-EOD
44
- def #{name}(*args, &blk)
45
- if ::EM.reactor_running?
46
- f = Fiber.current
47
- #{async_name}(*args) do |res|
48
- f.resume(res)
49
- end
50
-
51
- result = Fiber.yield
52
- raise result if result.is_a?(::Exception)
53
- if block_given?
54
- begin
55
- yield result
56
- ensure
57
- result.#{clear_method}
58
- end
59
- else
60
- result
61
- end
62
- else
63
- #{blocking_call}
64
- end
65
- end
66
- EOD
67
- end
68
-
69
- class << self
70
- alias_method :new, :connect
71
- alias_method :open, :connect
72
- alias_method :setdb, :connect
73
- alias_method :setdblogin, :connect
74
- end
75
-
76
- alias_method :query, :exec
77
-
78
- def async_autoreconnect!(deferrable, error, &send_proc)
79
- if async_autoreconnect && self.status != PG::CONNECTION_OK
80
- reset_df = async_reset
81
- reset_df.errback { |ex| deferrable.fail(ex) }
82
- reset_df.callback do
83
- Fiber.new do
84
- if on_autoreconnect
85
- returned_df = on_autoreconnect.call(self, error)
86
- if returned_df == false
87
- ::EM.next_tick { deferrable.fail(error) }
88
- elsif returned_df.respond_to?(:callback) && returned_df.respond_to?(:errback)
89
- returned_df.callback { deferrable.protect(&send_proc) }
90
- returned_df.errback { |ex| deferrable.fail(ex) }
91
- elsif returned_df.is_a?(Exception)
92
- ::EM.next_tick { deferrable.fail(returned_df) }
93
- else
94
- deferrable.protect(&send_proc)
95
- end
96
- else
97
- deferrable.protect(&send_proc)
98
- end
99
- end.resume
100
- end
101
- else
102
- ::EM.next_tick { deferrable.fail(error) }
103
- end
104
- end
105
-
106
- end
107
- end
108
- end
@@ -0,0 +1,5 @@
1
+ module PG
2
+ module EM
3
+ VERSION = '0.3.0'
4
+ end
5
+ end
@@ -1,3 +1,4 @@
1
+ require 'fiber'
1
2
  begin
2
3
  require 'pg'
3
4
  rescue LoadError => error
@@ -10,366 +11,387 @@ unless defined? EventMachine
10
11
  raise 'Missing EventMachine: gem install eventmachine'
11
12
  end
12
13
  end
14
+ require 'pg/em-version'
15
+ require 'pg/em/featured_deferrable'
16
+ require 'pg/em/client/watcher'
17
+ require 'pg/em/client/connect_watcher'
13
18
 
14
19
  module PG
15
-
16
20
  module EM
17
- class FeaturedDeferrable < ::EM::DefaultDeferrable
18
- def initialize(&blk)
19
- if block_given?
20
- callback(&blk)
21
- errback(&blk)
22
- end
23
- end
24
-
25
- def protect(fail_value = nil)
26
- yield
27
- rescue Exception => e
28
- ::EM.next_tick { fail(e) }
29
- fail_value
30
- end
31
-
32
- def protect_and_succeed(fail_value = nil)
33
- ret = yield
34
- rescue Exception => e
35
- ::EM.next_tick { fail(e) }
36
- fail_value
37
- else
38
- ::EM.next_tick { succeed(ret) }
39
- ret
40
- end
41
- end
21
+
42
22
  # == PostgreSQL EventMachine client
43
23
  #
44
- # Author:: Rafal Michalski (mailto:royaltm75@gmail.com)
45
- # Licence:: MIT License
24
+ # Author:: Rafal Michalski
46
25
  #
26
+ # {PG::EM::Client} is a PG::Connection[http://deveiate.org/code/pg/PG/Connection.html]
27
+ # wrapper designed for EventMachine[http://rubyeventmachine.com/].
47
28
  #
48
- # PG::EM::Client is a wrapper for PG::Connection[http://deveiate.org/code/pg/PG/Connection.html]
49
- # which (re)defines methods:
29
+ # The following new methods:
50
30
  #
51
- # - +async_exec+ (alias: +async_query+)
52
- # - +async_prepare+
53
- # - +async_exec_prepared+
54
- # - +async_describe_prepared+
55
- # - +async_describe_portal+
31
+ # - {#exec_defer} (alias: +query_defer+)
32
+ # - {#exec_params_defer}
33
+ # - {#prepare_defer}
34
+ # - {#exec_prepared_defer}
35
+ # - {#describe_prepared_defer}
36
+ # - {#describe_portal_defer}
56
37
  #
57
- # which are suitable to run in EM event loop (they return +Deferrable+)
38
+ # are added to execute queries asynchronously,
39
+ # returning +Deferrable+ object.
58
40
  #
59
- # and following:
41
+ # The following methods of PG::Connection[http://deveiate.org/code/pg/PG/Connection.html]
42
+ # are overloaded:
60
43
  #
61
- # - +exec+ (alias: +query+)
62
- # - +prepare+
63
- # - +exec_prepared+
64
- # - +describe_prepared+
65
- # - +describe_portal+
44
+ # - {#exec} (alias: +query+, +async_exec+, +async_query+)
45
+ # - {#exec_params}
46
+ # - {#prepare}
47
+ # - {#exec_prepared}
48
+ # - {#describe_prepared}
49
+ # - {#describe_portal}
66
50
  #
67
- # autodetecting if EventMachine is running and using the appropriate
68
- # (async or sync) method version.
51
+ # and are now auto-detecting if EventMachine is running and
52
+ # performing commands asynchronously (blocking only current fiber) or
53
+ # calling parent thread-blocking methods.
69
54
  #
70
- # Additionally to the above, there are asynchronous methods defined for
71
- # establishing connection and re-connecting:
55
+ # If {#async_autoreconnect} option is set to +true+, all of the above
56
+ # methods (in asynchronous mode) try to re-connect after a connection
57
+ # error occurs. It's performed behind the scenes, so no error is raised,
58
+ # except if there was a transaction in progress. In such instance the error
59
+ # is raised after establishing connection to signal that
60
+ # the transaction was aborted.
72
61
  #
73
- # - +Client.async_connect+
74
- # - +async_reset+
75
- #
76
- # They are async equivalents of PG::Connection.connect (which is also
77
- # aliased by PG::Connection as +new+, +open+, +setdb+, +setdblogin+) and
78
- # +reset+.
79
- #
80
- # When #async_autoreconnect is +true+, async methods might try to
81
- # re-connect after a connection error. You won't even notice that
82
- # (except for warning message from PG).
83
- # If you want to detect such event use #on_autoreconnect property.
62
+ # If you want to detect auto re-connect event use {#on_autoreconnect}
63
+ # property/option.
84
64
  #
85
65
  # To enable auto-reconnecting set:
86
66
  # client.async_autoreconnect = true
87
67
  #
88
- # or pass as new() hash argument:
89
- # ::new database: 'bar', async_autoreconnect: true
68
+ # or pass as {new} hash argument:
69
+ # PG::EM::Client.new dbname: 'bar', async_autoreconnect: true
70
+ #
71
+ # There are also new methods:
72
+ #
73
+ # - {Client.connect_defer}
74
+ # - {#reset_defer}
75
+ #
76
+ # which are asynchronous versions of PG::Connection.new and
77
+ # PG:Connection#reset.
78
+ #
79
+ # Additionally the following methods are overloaded:
80
+ #
81
+ # - {new} (alias: +connect+, +open+, +setdb+, +setdblogin+ )
82
+ # - {#reset}
83
+ #
84
+ # providing auto-detecting asynchronous (fiber-synchronized) or
85
+ # thread-blocking methods for (re)connecting.
90
86
  #
91
87
  # Otherwise nothing changes in PG::Connection API.
92
88
  # See PG::Connection[http://deveiate.org/code/pg/PG/Connection.html] docs
93
- # for arguments to above methods.
89
+ # for explanation of arguments to the above methods.
94
90
  #
95
91
  # *Warning:*
96
92
  #
97
- # +describe_prepared+ and +exec_prepared+ after
98
- # +prepare+ should only be invoked on the *same* connection.
99
- # If you are using a connection pool, make sure to acquire single connection first.
93
+ # {#describe_prepared} and {#exec_prepared} after
94
+ # {#prepare} should only be invoked on the *same* connection.
95
+ # If you are using a connection pool, make sure to acquire a single
96
+ # connection first.
100
97
  #
101
98
  class Client < PG::Connection
102
99
 
100
+ ROOT_FIBER = Fiber.current
103
101
 
104
- # Connection timeout. Changing this property only affects
105
- # ::async_connect and #async_reset.
106
- # However if passed as initialization option, it also affects blocking
107
- # ::new and #reset.
102
+ # @!attribute connect_timeout
103
+ # @return [Float] connection timeout in seconds
104
+ # Connection timeout. Affects {#reset} and {#reset_defer}.
105
+ #
106
+ # Changing this property does not affect thread-blocking {#reset}.
107
+ #
108
+ # However if passed as initialization option, it also affects blocking
109
+ # {#reset}.
110
+ #
111
+ # To enable it set to some positive value. To disable it: set to 0.
112
+ # You can also specify this as an option to {new} or {connect_defer}.
108
113
  attr_accessor :connect_timeout
109
114
 
110
- # Aborts async command processing if waiting for response from server
111
- # exceedes +query_timeout+ seconds. This does not apply to
112
- # ::async_connect and #async_reset. For them
113
- # use +connect_timeout+ instead.
115
+ # @!attribute query_timeout
116
+ # @return [Float] query timeout in seconds
117
+ # Aborts async command processing if server response time
118
+ # exceedes +query_timeout+ seconds. This does not apply to
119
+ # {#reset} and {#reset_defer}.
114
120
  #
115
- # To enable it set to seconds (> 0). To disable: set to 0.
116
- # You can also specify this as initialization option.
121
+ # To enable it set to some positive value. To disable it: set to 0.
122
+ # You can also specify this as an option to {new} or {connect_defer}.
117
123
  attr_accessor :query_timeout
118
124
 
119
- # Enable/disable auto-reconnect feature (+true+/+false+).
120
- # Defaults to +false+. However it is implicitly set to +true+
121
- # if #on_autoreconnect is specified as initialization option.
122
- # Changing #on_autoreconnect with accessor method doesn't change
123
- # #async_autoreconnect.
125
+ # @!attribute async_autoreconnect
126
+ # @return [Boolean] asynchronous auto re-connect status
127
+ # Enable/disable auto re-connect feature (+true+/+false+).
128
+ # Defaults to +false+ unless {#on_autoreconnect} is specified
129
+ # as an initialization option.
130
+ #
131
+ # Changing {#on_autoreconnect} with accessor method doesn't change
132
+ # the state of {#async_autoreconnect}.
133
+ #
134
+ # You can also specify this as an option to {new} or {connect_defer}.
124
135
  attr_accessor :async_autoreconnect
125
136
 
126
- # +on_autoreconnect+ is a user defined Proc that is called after a connection
127
- # with the server has been re-established.
128
- # It's invoked with two arguments. First one is the +connection+.
129
- # The second is the original +exception+ that caused the reconnecting process.
137
+ # @!attribute on_autoreconnect
138
+ # @return [Proc<Client, Error>] auto re-connect hook
139
+ # Proc that is called after a connection with the server has been
140
+ # automatically re-established. It's being invoked just before the
141
+ # pending command is sent to the server.
142
+ #
143
+ # The first argument it receives is the +connection+ instance.
144
+ # The second is the original +exception+ that caused the reconnecting
145
+ # process.
146
+ #
147
+ # The proc can control the later action with its return value:
130
148
  #
131
- # Certain rules should apply to #on_autoreconnect proc:
149
+ # - +false+ (explicitly, +nil+ is ignored) - the original +exception+
150
+ # is raised/passed back and the pending query command is not sent
151
+ # again to the server.
152
+ # - +true+ (explicitly, truish values are ignored), the pending command
153
+ # is called regardless of the connection's last transaction status.
154
+ # - Exception object - is raised/passed back and the pending command
155
+ # is not sent.
156
+ # - Deferrable object - the chosen action will depend on the deferred
157
+ # status.
158
+ # - Other values are ignored and the pending query command is
159
+ # immediately sent to the server unless there was a pending
160
+ # transaction before the connection was reset.
132
161
  #
133
- # - If proc returns +false+ (explicitly, +nil+ is ignored),
134
- # the original +exception+ is passed to Defferable's +errback+ and
135
- # the send query command is not invoked at all.
136
- # - If return value is an instance of exception, it is passed to
137
- # Defferable's +errback+ and the send query command is not invoked at all.
138
- # - If return value responds to +callback+ and +errback+ methods,
139
- # the send query command will be bound to value's success +callback+
140
- # and the original Defferable's +errback+ or value's +errback+.
141
- # - Other return values are ignored and the send query command is called
142
- # immediately after #on_autoreconnect proc is executed.
162
+ # It's possible to execute queries from inside of the proc.
143
163
  #
144
- # You may pass this proc as +:on_autoreconnect+ option to ::new.
164
+ # You may pass this proc as an option to {new} or {connect_defer}.
145
165
  #
146
- # Example:
147
- # pg.on_autoreconnect = proc do |conn, ex|
148
- # conn.prepare("species_by_name",
149
- # "select id, name from animals where species=$1 order by name")
150
- # end
166
+ # @example How to use prepare in on_autoreconnect hook
167
+ # pg.on_autoreconnect = proc do |conn, ex|
168
+ # conn.prepare("species_by_name",
169
+ # "select id, name from animals where species=$1 order by name")
170
+ # end
151
171
  #
152
172
  attr_accessor :on_autoreconnect
153
173
 
174
+ # @!visibility private
154
175
  # Used internally for marking connection as aborted on query timeout.
155
176
  attr_accessor :async_command_aborted
156
177
 
157
- module Watcher
158
- def initialize(client, deferrable, send_proc)
159
- @last_result = nil
160
- @client = client
161
- @deferrable = deferrable
162
- @send_proc = send_proc
163
- if (timeout = client.query_timeout) > 0
164
- @notify_timestamp = Time.now
165
- setup_timer timeout
166
- else
167
- @timer = nil
168
- end
169
- end
178
+ # environment variable name for connect_timeout fallback value
179
+ @@connect_timeout_envvar = conndefaults.find{|d| d[:keyword] == "connect_timeout" }[:envvar]
170
180
 
171
- def setup_timer(timeout, adjustment = 0)
172
- @timer = ::EM::Timer.new(timeout - adjustment) do
173
- if (last_interval = Time.now - @notify_timestamp) >= timeout
174
- detach
175
- @client.async_command_aborted = true
176
- @deferrable.protect do
177
- raise PG::Error, "query timeout expired (async)"
178
- end
179
- else
180
- setup_timer timeout, last_interval
181
- end
182
- end
183
- end
184
-
185
- def notify_readable
186
- result = false
187
- @client.consume_input
188
- until @client.is_busy
189
- if (single_result = @client.get_result).nil?
190
- if (result = @last_result).nil?
191
- error = PG::Error.new(@client.error_message)
192
- error.instance_variable_set(:@connection, @client)
193
- raise error
194
- end
195
- result.check
196
- detach
197
- @timer.cancel if @timer
198
- break
199
- end
200
- @last_result.clear if @last_result
201
- @last_result = single_result
202
- end
203
- rescue Exception => e
204
- detach
205
- @timer.cancel if @timer
206
- if e.is_a?(PG::Error)
207
- @client.async_autoreconnect!(@deferrable, e, &@send_proc)
208
- else
209
- @deferrable.fail(e)
210
- end
211
- else
212
- if result == false
213
- @notify_timestamp = Time.now if @timer
214
- else
215
- @deferrable.succeed(result)
216
- end
217
- end
218
- end
219
-
220
- module ConnectWatcher
221
- def initialize(client, deferrable, poll_method)
222
- @client = client
223
- @deferrable = deferrable
224
- @poll_method = :"#{poll_method}_poll"
225
- if (timeout = client.connect_timeout) > 0
226
- @timer = ::EM::Timer.new(timeout) do
227
- begin
228
- detach
229
- @deferrable.protect do
230
- raise PG::Error, "timeout expired (async)"
231
- end
232
- ensure
233
- @client.finish unless reconnecting?
234
- end
235
- end
236
- end
237
- end
238
-
239
- def reconnecting?
240
- @poll_method == :reset_poll
241
- end
181
+ DEFAULT_ASYNC_VARS = {
182
+ :@async_autoreconnect => nil,
183
+ :@connect_timeout => nil,
184
+ :@query_timeout => 0,
185
+ :@on_autoreconnect => nil,
186
+ :@async_command_aborted => false,
187
+ }.freeze
242
188
 
243
- def notify_writable
244
- poll_connection_and_check
245
- end
246
-
247
- def notify_readable
248
- poll_connection_and_check
249
- end
250
-
251
- def poll_connection_and_check
252
- case @client.__send__(@poll_method)
253
- when PG::PGRES_POLLING_READING
254
- self.notify_readable = true
255
- self.notify_writable = false
256
- when PG::PGRES_POLLING_WRITING
257
- self.notify_writable = true
258
- self.notify_readable = false
259
- when PG::PGRES_POLLING_OK, PG::PGRES_POLLING_FAILED
260
- @timer.cancel if @timer
261
- detach
262
- @deferrable.protect_and_succeed do
263
- unless @client.status == PG::CONNECTION_OK
264
- begin
265
- raise PG::Error, @client.error_message
266
- ensure
267
- @client.finish unless reconnecting?
268
- end
269
- end
270
- # mimic blocking connect behavior
271
- @client.set_default_encoding unless reconnecting?
272
- @client
273
- end
274
- end
275
- end
276
- end
277
-
278
- def self.parse_async_args(args)
279
- async_args = {
280
- :@async_autoreconnect => nil,
281
- :@connect_timeout => 0,
282
- :@query_timeout => 0,
283
- :@on_autoreconnect => nil,
284
- :@async_command_aborted => false,
285
- }
189
+ # @!visibility private
190
+ def self.parse_async_options(args)
191
+ options = DEFAULT_ASYNC_VARS.dup
286
192
  if args.last.is_a? Hash
287
193
  args[-1] = args.last.reject do |key, value|
288
- case key.to_s
289
- when 'async_autoreconnect'
290
- async_args[:@async_autoreconnect] = !!value
194
+ case key.to_sym
195
+ when :async_autoreconnect
196
+ options[:@async_autoreconnect] = value
291
197
  true
292
- when 'on_reconnect'
293
- raise ArgumentError.new("on_reconnect is no longer supported, use on_autoreconnect")
294
- when 'on_autoreconnect'
198
+ when :on_reconnect
199
+ raise ArgumentError, "on_reconnect is no longer supported, use on_autoreconnect"
200
+ when :on_autoreconnect
295
201
  if value.respond_to? :call
296
- async_args[:@on_autoreconnect] = value
297
- async_args[:@async_autoreconnect] = true if async_args[:@async_autoreconnect].nil?
202
+ options[:@on_autoreconnect] = value
203
+ options[:@async_autoreconnect] = true if options[:@async_autoreconnect].nil?
204
+ else
205
+ raise ArgumentError, "on_autoreconnect must respond to `call'"
298
206
  end
299
207
  true
300
- when 'connect_timeout'
301
- async_args[:@connect_timeout] = value.to_f
208
+ when :connect_timeout
209
+ options[:@connect_timeout] = value.to_f
302
210
  false
303
- when 'query_timeout'
304
- async_args[:@query_timeout] = value.to_f
211
+ when :query_timeout
212
+ options[:@query_timeout] = value.to_f
305
213
  true
306
214
  end
307
215
  end
308
216
  end
309
- async_args[:@async_autoreconnect] = false if async_args[:@async_autoreconnect].nil?
310
- async_args
217
+ options[:@async_autoreconnect] = !!options[:@async_autoreconnect]
218
+ options[:@connect_timeout] ||= ENV[@@connect_timeout_envvar].to_f
219
+ options
311
220
  end
312
221
 
222
+ # @!group Deferrable connection methods
223
+
313
224
  # Attempts to establish the connection asynchronously.
314
- # For args see PG::Connection.new[http://deveiate.org/code/pg/PG/Connection.html#method-c-new].
315
- # Returns +Deferrable+. Use its +callback+ to obtain newly created and
316
- # already connected PG::EM::Client object.
317
- # If block is provided, it's bound to +callback+ and +errback+ of returned
318
- # +Deferrable+.
319
- #
320
- # Special PG::EM::Client options (e.g.: +async_autoreconnect+) must be provided
321
- # as +connection_hash+ argument variant. They will be ignored in +connection_string+.
322
- #
323
- # +client_encoding+ *will* be set for you according to Encoding.default_internal.
324
- def self.async_connect(*args, &blk)
225
+ #
226
+ # @return [FeaturedDeferrable]
227
+ # @yieldparam pg [Client|PG::Error] new and connected client instance on
228
+ # success or an instance of raised PG::Error
229
+ #
230
+ # Pass the block to the returned deferrable's +callback+ to obtain newly
231
+ # created and already connected {Client} object. In case of connection
232
+ # error +errback+ hook receives an error object as an argument.
233
+ # If the block is provided it's bound to both +callback+ and +errback+
234
+ # hooks of the returned deferrable.
235
+ #
236
+ # Special {Client} options (e.g.: {#async_autoreconnect}) must be
237
+ # provided as +connection_hash+ argument variant. They will be ignored
238
+ # if passed as a +connection_string+.
239
+ #
240
+ # +client_encoding+ *will* be set according to +Encoding.default_internal+.
241
+ #
242
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-c-new PG::Connection.new
243
+ def self.connect_defer(*args, &blk)
325
244
  df = PG::EM::FeaturedDeferrable.new(&blk)
326
- async_args = parse_async_args(args)
245
+ async_args = parse_async_options(args)
327
246
  conn = df.protect { connect_start(*args) }
328
247
  if conn
329
248
  async_args.each {|k, v| conn.instance_variable_set(k, v) }
330
- ::EM.watch(conn.socket, ConnectWatcher, conn, df, :connect).poll_connection_and_check
249
+ ::EM.watch(conn.socket_io, ConnectWatcher, conn, df, false).
250
+ poll_connection_and_check
331
251
  end
332
252
  df
333
253
  end
334
254
 
255
+ class << self
256
+ # @deprecated Use {connect_defer} instead.
257
+ alias_method :async_connect, :connect_defer
258
+ end
259
+
335
260
  # Attempts to reset the connection asynchronously.
336
- # There are no arguments, except block argument.
337
261
  #
338
- # Returns +Deferrable+. Use it's +callback+ to handle success.
339
- # If block is provided, it's bound to +callback+ and +errback+ of returned
340
- # +Deferrable+.
341
- def async_reset(&blk)
262
+ # @return [FeaturedDeferrable]
263
+ # @yieldparam pg [Client|PG::Error] reconnected client instance on
264
+ # success or an instance of raised PG::Error
265
+ #
266
+ # Pass the block to the returned deferrable's +callback+ to execute
267
+ # after successfull reset.
268
+ # If the block is provided it's bound to +callback+ and +errback+ hooks
269
+ # of the returned deferrable.
270
+ def reset_defer(&blk)
342
271
  @async_command_aborted = false
343
272
  df = PG::EM::FeaturedDeferrable.new(&blk)
273
+ # there can be only one watch handler over the socket
274
+ # apparently eventmachine has hard time dealing with more than one
275
+ # for blocking reset this is not needed
276
+ if @watcher
277
+ @watcher.detach if @watcher.watching?
278
+ @watcher = nil
279
+ end
344
280
  ret = df.protect(:fail) { reset_start }
345
281
  unless ret == :fail
346
- ::EM.watch(self.socket, ConnectWatcher, self, df, :reset).poll_connection_and_check
282
+ ::EM.watch(self.socket_io, ConnectWatcher, self, df, true).
283
+ poll_connection_and_check
347
284
  end
348
285
  df
349
286
  end
350
287
 
351
- # Uncheck #async_command_aborted on blocking reset.
288
+ # @deprecated Use {reset_defer} instead.
289
+ alias_method :async_reset, :reset_defer
290
+
291
+ # @!endgroup
292
+
293
+ # @!group Auto-sensing fiber-synchronized connection methods
294
+
295
+ # Attempts to reset the connection.
296
+ #
297
+ # Performs command asynchronously yielding from current fiber
298
+ # if EventMachine reactor is running and current fiber isn't the root
299
+ # fiber. Other fibers can process while waiting for the server to
300
+ # complete the request.
301
+ #
302
+ # Otherwise performs a thread-blocking call to the parent method.
303
+ #
304
+ # @raise [PG::Error]
305
+ #
306
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-reset PG::Connection#reset
352
307
  def reset
353
308
  @async_command_aborted = false
354
- super
309
+ if ::EM.reactor_running? && !(f = Fiber.current).equal?(ROOT_FIBER)
310
+ reset_defer {|r| f.resume(r) }
311
+
312
+ conn = Fiber.yield
313
+ raise conn if conn.is_a?(::Exception)
314
+ conn
315
+ else
316
+ super
317
+ end
355
318
  end
356
319
 
357
- # Creates new instance of PG::EM::Client and attempts to establish connection.
358
- # See PG::Connection.new[http://deveiate.org/code/pg/PG/Connection.html#method-c-new].
320
+ # Creates new instance of PG::EM::Client and attempts to establish
321
+ # connection.
359
322
  #
360
- # Special PG::EM::Client options (e.g.: +async_autoreconnect+) must be provided
361
- # as +connection_hash+ argument variant. They will be ignored in +connection_string+.
362
- #
363
- # +em-synchrony+ version *will* do set +client_encoding+ for you according to
364
- # Encoding.default_internal.
323
+ # Performs command asynchronously yielding from current fiber
324
+ # if EventMachine reactor is running and current fiber isn't the root
325
+ # fiber. Other fibers can process while waiting for the server to
326
+ # complete the request.
327
+ #
328
+ # Otherwise performs a thread-blocking call to the parent method.
329
+ #
330
+ # @raise [PG::Error]
331
+ #
332
+ # Special {Client} options (e.g.: {#async_autoreconnect}) must be
333
+ # provided as +connection_hash+ argument variant. They will be ignored
334
+ # if passed as a +connection_string+.
335
+ #
336
+ # +client_encoding+ *will* be set according to +Encoding.default_internal+.
337
+ #
338
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-c-new PG::Connection.new
339
+ def self.new(*args, &blk)
340
+ if ::EM.reactor_running? && !(f = Fiber.current).equal?(ROOT_FIBER)
341
+ connect_defer(*args) {|r| f.resume(r) }
342
+
343
+ conn = Fiber.yield
344
+ raise conn if conn.is_a?(::Exception)
345
+ if block_given?
346
+ begin
347
+ yield conn
348
+ ensure
349
+ conn.finish
350
+ end
351
+ else
352
+ conn
353
+ end
354
+ else
355
+ super(*args)
356
+ end
357
+ end
358
+
359
+ # @!visibility private
365
360
  def initialize(*args)
366
- Client.parse_async_args(args).each {|k, v| self.instance_variable_set(k, v) }
361
+ Client.parse_async_options(args).each {|k, v| instance_variable_set(k, v) }
367
362
  super(*args)
368
363
  end
369
364
 
370
- # Return +CONNECTION_BAD+ for connections with +async_command_aborted+
371
- # flag set by expired query timeout. Otherwise return whatever
372
- # PG::Connection#status[http://deveiate.org/code/pg/PG/Connection.html#method-i-status] provides.
365
+ class << self
366
+ alias_method :connect, :new
367
+ alias_method :open, :new
368
+ alias_method :setdb, :new
369
+ alias_method :setdblogin, :new
370
+ end
371
+
372
+ # @!endgroup
373
+
374
+ # Closes the backend connection.
375
+ #
376
+ # Detaches watch handler to prevent memory leak then
377
+ # calls parent PG::Connection#finish[http://deveiate.org/code/pg/PG/Connection.html#method-i-finish].
378
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-finish PG::Connection#finish
379
+ def finish
380
+ super
381
+ if @watcher
382
+ @watcher.detach if @watcher.watching?
383
+ @watcher = nil
384
+ end
385
+ end
386
+
387
+ alias_method :close, :finish
388
+
389
+ # Returns status of connection: PG::CONNECTION_OK or PG::CONNECTION_BAD.
390
+ #
391
+ # @return [Number]
392
+ # Returns +PG::CONNECTION_BAD+ for connections with +async_command_aborted+
393
+ # flag set by expired query timeout. Otherwise return whatever PG::Connection#status returns.
394
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-status PG::Connection#status
373
395
  def status
374
396
  if @async_command_aborted
375
397
  PG::CONNECTION_BAD
@@ -378,53 +400,155 @@ module PG
378
400
  end
379
401
  end
380
402
 
381
- # Perform autoreconnect. Used internally.
403
+ # @!visibility private
404
+ # Perform auto re-connect. Used internally.
382
405
  def async_autoreconnect!(deferrable, error, &send_proc)
383
- if async_autoreconnect && self.status != PG::CONNECTION_OK
384
- reset_df = async_reset
385
- reset_df.errback { |ex| deferrable.fail(ex) }
406
+ # reconnect only if connection is bad and flag is set
407
+ if self.status != PG::CONNECTION_OK && async_autoreconnect
408
+ # check if transaction was active
409
+ was_in_transaction = case @last_transaction_status
410
+ when PG::PQTRANS_IDLE, PG::PQTRANS_UNKNOWN
411
+ false
412
+ else
413
+ true
414
+ end
415
+ # reset asynchronously
416
+ reset_df = reset_defer
417
+ # just fail on reset failure
418
+ reset_df.errback { |ex| deferrable.fail ex }
419
+ # reset succeeds
386
420
  reset_df.callback do
421
+ # handle on_autoreconnect
387
422
  if on_autoreconnect
388
- returned_df = on_autoreconnect.call(self, error)
389
- if returned_df == false
390
- ::EM.next_tick { deferrable.fail(error) }
391
- elsif returned_df.respond_to?(:callback) && returned_df.respond_to?(:errback)
392
- returned_df.callback { deferrable.protect(&send_proc) }
393
- returned_df.errback { |ex| deferrable.fail(ex) }
394
- elsif returned_df.is_a?(Exception)
395
- ::EM.next_tick { deferrable.fail(returned_df) }
396
- else
397
- deferrable.protect(&send_proc)
398
- end
423
+ # wrap in a fiber, so on_autoreconnect code may yield from it
424
+ Fiber.new do
425
+ # call on_autoreconnect handler and fail if it raises an error
426
+ returned_df = begin
427
+ on_autoreconnect.call(self, error)
428
+ rescue => ex
429
+ ex
430
+ end
431
+ if returned_df.respond_to?(:callback) && returned_df.respond_to?(:errback)
432
+ # the handler returned a deferrable
433
+ returned_df.callback do
434
+ if was_in_transaction
435
+ # there was a transaction in progress, fail anyway
436
+ deferrable.fail error
437
+ else
438
+ # try to call failed query command again
439
+ deferrable.protect(&send_proc)
440
+ end
441
+ end
442
+ # fail when handler's deferrable fails
443
+ returned_df.errback { |ex| deferrable.fail ex }
444
+ elsif returned_df.is_a?(Exception)
445
+ # tha handler returned an exception object, so fail with it
446
+ deferrable.fail returned_df
447
+ elsif returned_df == false || (was_in_transaction && returned_df != true)
448
+ # tha handler returned false or raised an exception
449
+ # or there was an active transaction and handler didn't return true
450
+ deferrable.fail error
451
+ else
452
+ # try to call failed query command again
453
+ deferrable.protect(&send_proc)
454
+ end
455
+ end.resume
456
+ elsif was_in_transaction
457
+ # there was a transaction in progress, fail anyway
458
+ deferrable.fail error
399
459
  else
460
+ # no on_autoreconnect handler, no transaction, then
461
+ # try to call failed query command again
400
462
  deferrable.protect(&send_proc)
401
463
  end
402
464
  end
403
465
  else
404
- ::EM.next_tick { deferrable.fail(error) }
466
+ # connection is good, or the async_autoreconnect is not set
467
+ deferrable.fail error
405
468
  end
406
469
  end
407
470
 
471
+ # @!macro deferrable_api
472
+ # @yieldparam result [PG::Result|Error] command result on success or a PG::Error instance on error.
473
+ # @return [FeaturedDeferrable]
474
+ # Use the returned deferrable's +callback+ and +errback+ method to get the result.
475
+ # If the block is provided it's bound to both the +callback+ and +errback+ hooks
476
+ # of the returned deferrable.
477
+
478
+ # @!group Deferrable command methods
479
+
480
+ # @!method exec_defer(sql, params=nil, result_format=nil, &blk)
481
+ # Sends SQL query request specified by +sql+ to PostgreSQL for asynchronous processing,
482
+ # and immediately returns with +deferrable+.
483
+ #
484
+ # @macro deferrable_api
485
+ #
486
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec PG::Connection#exec
487
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec_params PG::Connection#exec_params
488
+ #
489
+ # @!method prepare_defer(stmt_name, sql, param_types=nil, &blk)
490
+ # Prepares statement +sql+ with name +stmt_name+ to be executed later asynchronously,
491
+ # and immediately returns with deferrable.
492
+ #
493
+ # @macro deferrable_api
494
+ #
495
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-prepare PG::Connection#prepare
496
+ #
497
+ # @!method exec_prepared_defer(statement_name, params=nil, result_format=nil, &blk)
498
+ # Execute prepared named statement specified by +statement_name+ asynchronously,
499
+ # and immediately returns with deferrable.
500
+ #
501
+ # @macro deferrable_api
502
+ #
503
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-send_query_prepared PG::Connection#send_query_prepared
504
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared PG::Connection#send_exec_prepared
505
+ #
506
+ # @!method describe_prepared_defer(statement_name, &blk)
507
+ # Asynchronously sends command to retrieve information about the prepared statement +statement_name+,
508
+ # and immediately returns with deferrable.
509
+ #
510
+ # @macro deferrable_api
511
+ #
512
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-describe_prepared PG::Connection#describe_prepared
513
+ #
514
+ # @!method describe_portal_defer(portal_name, &blk)
515
+ # Asynchronously sends command to retrieve information about the portal +portal_name+,
516
+ # and immediately returns with deferrable.
517
+ #
518
+ # @macro deferrable_api
519
+ #
520
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-describe_portal PG::Connection#describe_portal
521
+ #
408
522
  %w(
409
- exec send_query
410
- prepare send_prepare
411
- exec_prepared send_query_prepared
412
- describe_prepared send_describe_prepared
413
- describe_portal send_describe_portal
414
- ).each_slice(2) do |name, send_name|
415
-
416
- class_eval <<-EOD
417
- def async_#{name}(*args, &blk)
523
+ exec_defer send_query
524
+ prepare_defer send_prepare
525
+ exec_prepared_defer send_query_prepared
526
+ describe_prepared_defer send_describe_prepared
527
+ describe_portal_defer send_describe_portal
528
+ ).each_slice(2) do |defer_name, send_name|
529
+
530
+ class_eval <<-EOD, __FILE__, __LINE__
531
+ def #{defer_name}(*args, &blk)
418
532
  df = PG::EM::FeaturedDeferrable.new(&blk)
419
533
  send_proc = proc do
420
534
  #{send_name}(*args)
421
- ::EM.watch(self.socket, Watcher, self, df, send_proc).notify_readable = true
535
+ if @watcher && @watcher.watching?
536
+ @watcher.watch_query(df, send_proc)
537
+ else
538
+ @watcher = ::EM.watch(self.socket_io, Watcher, self).
539
+ watch_query(df, send_proc)
540
+ end
422
541
  end
423
542
  begin
424
- raise PG::Error, "previous query expired, need connection reset" if @async_command_aborted
543
+ if @async_command_aborted
544
+ error = ConnectionBad.new("previous query expired, need connection reset")
545
+ error.instance_variable_set(:@connection, self)
546
+ raise error
547
+ end
548
+ @last_transaction_status = transaction_status
425
549
  send_proc.call
426
550
  rescue PG::Error => e
427
- async_autoreconnect!(df, e, &send_proc)
551
+ ::EM.next_tick { async_autoreconnect!(df, e, &send_proc) }
428
552
  rescue Exception => e
429
553
  ::EM.next_tick { df.fail(e) }
430
554
  end
@@ -432,61 +556,231 @@ module PG
432
556
  end
433
557
  EOD
434
558
 
435
- class_eval <<-EOD
436
- def #{name}(*args, &blk)
437
- if ::EM.reactor_running?
438
- async_#{name}(*args, &blk)
439
- else
440
- super(*args, &blk)
441
- end
442
- end
443
- EOD
444
-
445
559
  end
446
560
 
447
- alias_method :query, :exec
448
- alias_method :async_query, :async_exec
561
+ alias_method :query_defer, :exec_defer
562
+ alias_method :async_query_defer, :exec_defer
563
+ alias_method :async_exec_defer, :exec_defer
564
+ alias_method :exec_params_defer, :exec_defer
565
+
566
+ # @!endgroup
567
+
568
+ # @!macro auto_synchrony_api
569
+ # Performs command asynchronously yielding current fiber
570
+ # if EventMachine reactor is running and the current fiber isn't the
571
+ # root fiber. Other fibers can process while waiting for the server
572
+ # to complete the request.
573
+ #
574
+ # Otherwise performs a blocking call to parent method.
575
+ #
576
+ # @yieldparam result [PG::Result] command result on success
577
+ # @return [PG::Result] if block wasn't given
578
+ # @return [Object] result of the given block
579
+ # @raise [PG::Error]
580
+
581
+ # @!group Auto-sensing thread or fiber blocking command methods
582
+
583
+ # @!method exec(sql, &blk)
584
+ # Sends SQL query request specified by +sql+ to PostgreSQL.
585
+ #
586
+ # @macro auto_synchrony_api
587
+ #
588
+ # @see PG::EM::Client#exec_defer
589
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec PG::Connection#exec
590
+ #
591
+ # @!method exec_params(sql, params=nil, result_format=nil, &blk)
592
+ # Sends SQL query request specified by +sql+ with optional +params+ and +result_format+ to PostgreSQL.
593
+ #
594
+ # @macro auto_synchrony_api
595
+ #
596
+ # @see PG::EM::Client#exec_params_defer
597
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec_params PG::Connection#exec_params
598
+ #
599
+ # @!method prepare(stmt_name, sql, param_types=nil, &blk)
600
+ # Prepares statement +sql+ with name +stmt_name+ to be executed later.
601
+ #
602
+ # @macro auto_synchrony_api
603
+ #
604
+ # @see PG::EM::Client#prepare_defer
605
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-prepare PG::Connection#prepare
606
+ #
607
+ # @!method exec_prepared(statement_name, params=nil, result_format=nil, &blk)
608
+ # Execute prepared named statement specified by +statement_name+.
609
+ #
610
+ # @macro auto_synchrony_api
611
+ #
612
+ # @see PG::EM::Client#exec_prepared_defer
613
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared PG::Connection#exec_prepared
614
+ #
615
+ # @!method describe_prepared(statement_name, &blk)
616
+ # Retrieve information about the prepared statement +statement_name+,
617
+ #
618
+ # @macro auto_synchrony_api
619
+ #
620
+ # @see PG::EM::Client#describe_prepared_defer
621
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-describe_prepared PG::Connection#describe_prepared
622
+ #
623
+ # @!method describe_portal(portal_name, &blk)
624
+ # Retrieve information about the portal +portal_name+,
625
+ #
626
+ # @macro auto_synchrony_api
627
+ #
628
+ # @see PG::EM::Client#describe_portal_defer
629
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-describe_portal PG::Connection#describe_portal
630
+ #
631
+ %w(
632
+ exec exec_defer
633
+ exec_params exec_defer
634
+ exec_prepared exec_prepared_defer
635
+ prepare prepare_defer
636
+ describe_prepared describe_prepared_defer
637
+ describe_portal describe_portal_defer
638
+ ).each_slice(2) do |name, defer_name|
639
+
640
+ class_eval <<-EOD, __FILE__, __LINE__
641
+ def #{name}(*args, &blk)
642
+ if ::EM.reactor_running? && !(f = Fiber.current).equal?(ROOT_FIBER)
643
+ #{defer_name}(*args) do |res|
644
+ f.resume(res)
645
+ end
449
646
 
450
- # support for pg < 0.14.0
451
- unless method_defined? :set_default_encoding
452
- def set_default_encoding
453
- unless Encoding.default_internal.nil?
454
- self.internal_encoding = Encoding.default_internal
647
+ result = Fiber.yield
648
+ raise result if result.is_a?(::Exception)
649
+ if block_given?
650
+ begin
651
+ yield result
652
+ ensure
653
+ result.clear
654
+ end
655
+ else
656
+ result
657
+ end
658
+ else
659
+ super
660
+ end
455
661
  end
456
- rescue EncodingError
457
- warn "warning: Failed to set the default_internal encoding to #{Encoding.default_internal}: '#{self.error_message}'"
458
- Encoding.default_internal
459
- end
662
+ EOD
460
663
  end
461
664
 
462
- end
463
- end
665
+ alias_method :query, :exec
666
+ alias_method :async_query, :exec
667
+ alias_method :async_exec, :exec
464
668
 
465
- # support for pg < 0.14.0
466
- unless Result.method_defined? :check
467
- class Result
468
- def check
469
- case result_status
470
- when PG::PGRES_BAD_RESPONSE,
471
- PG::PGRES_FATAL_ERROR,
472
- PG::PGRES_NONFATAL_ERROR
473
- error = PG::Error.new(error_message)
474
- error.instance_variable_set(:@result, self)
475
- error.instance_variable_set(:@connection, @connection)
476
- raise error
477
- end
478
- end
479
- alias_method :check_result, :check
480
- end
669
+ # @!endgroup
481
670
 
482
- module EM
483
- class Client < PG::Connection
484
- def get_result(&blk)
485
- result = super(&blk)
486
- result.instance_variable_set(:@connection, self) unless block_given?
671
+ TRAN_BEGIN_QUERY = 'BEGIN'
672
+ TRAN_ROLLBACK_QUERY = 'ROLLBACK'
673
+ TRAN_COMMIT_QUERY = 'COMMIT'
674
+ # Executes a BEGIN at the start of the block and a COMMIT at the end
675
+ # of the block or ROLLBACK if any exception occurs.
676
+ #
677
+ # @note Avoid using PG::EM::Client#*_defer calls inside the block or make sure
678
+ # all queries are completed before the provided block terminates.
679
+ # @return [Object] result of the block
680
+ # @yieldparam client [self]
681
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-transaction PG::Connection#transaction
682
+ #
683
+ # Calls to {#transaction} may be nested, however without sub-transactions
684
+ # (save points). If the innermost transaction block raises an error
685
+ # the transaction is rolled back to the state before the outermost
686
+ # transaction began.
687
+ #
688
+ # This is an extension to the +PG::Connection#transaction+ method
689
+ # as it does not support nesting in this way.
690
+ #
691
+ # The method is sensitive to the transaction status and will safely
692
+ # rollback on any sql error even when it was catched by some rescue block.
693
+ # But consider that rescuing any sql error within an utility method
694
+ # is a bad idea.
695
+ #
696
+ # This method works in both blocking/async modes (regardles of the reactor state)
697
+ # and is considered as a generic extension to the +PG::Connection#transaction+
698
+ # method.
699
+ #
700
+ # @example Nested transaction example
701
+ # def add_comment(user_id, text)
702
+ # db.transaction do
703
+ # cmt_id = db.query(
704
+ # 'insert into comments (text) where user_id=$1 values ($2) returning id',
705
+ # [user_id, text]).getvalue(0,0)
706
+ # db.query(
707
+ # 'update users set last_comment_id=$2 where id=$1', [user_id, cmt_id])
708
+ # cmt_id
709
+ # end
710
+ # end
711
+ #
712
+ # def update_comment_count(page_id)
713
+ # db.transaction do
714
+ # count = db.query('select count(*) from comments where page_id=$1', [page_id]).getvalue(0,0)
715
+ # db.query('update pages set comment_count=$2 where id=$1', [page_id, count])
716
+ # end
717
+ # end
718
+ #
719
+ # # to run add_comment and update_comment_count within the same transaction
720
+ # db.transaction do
721
+ # add_comment(user_id, some_text)
722
+ # update_comment_count(page_id)
723
+ # end
724
+ #
725
+ def transaction
726
+ raise ArgumentError, 'Must supply block for PG::EM::Client#transaction' unless block_given?
727
+ tcount = @client_tran_count.to_i
728
+
729
+ case transaction_status
730
+ when PG::PQTRANS_IDLE
731
+ # there is no transaction yet, so let's begin
732
+ exec(TRAN_BEGIN_QUERY)
733
+ # reset transaction count in case user code rolled it back before
734
+ tcount = 0 if tcount != 0
735
+ when PG::PQTRANS_INTRANS
736
+ # transaction in progress, leave it be
737
+ else
738
+ # transaction failed, is in unknown state or command is active
739
+ # in any case calling begin will raise server transaction error
740
+ exec(TRAN_BEGIN_QUERY) # raises PG::InFailedSqlTransaction
741
+ end
742
+ # memoize nested count
743
+ @client_tran_count = tcount + 1
744
+ begin
745
+
746
+ result = yield self
747
+
748
+ rescue
749
+ # error was raised
750
+ case transaction_status
751
+ when PG::PQTRANS_INTRANS, PG::PQTRANS_INERROR
752
+ # do not rollback if transaction was rolled back before
753
+ # or is in unknown state, which means connection reset is needed
754
+ # and rollback only from the outermost transaction block
755
+ exec(TRAN_ROLLBACK_QUERY) if tcount.zero?
756
+ end
757
+ # raise again
758
+ raise
759
+ else
760
+ # we are good (but not out of woods yet)
761
+ case transaction_status
762
+ when PG::PQTRANS_INTRANS
763
+ # commit only from the outermost transaction block
764
+ exec(TRAN_COMMIT_QUERY) if tcount.zero?
765
+ when PG::PQTRANS_INERROR
766
+ # no ruby error was raised (or an error was rescued in code block)
767
+ # but there was an sql error anyway
768
+ # so rollback after the outermost block
769
+ exec(TRAN_ROLLBACK_QUERY) if tcount.zero?
770
+ when PG::PQTRANS_IDLE
771
+ # the code block has terminated the transaction on its own
772
+ # so just reset the counter
773
+ tcount = 0
774
+ else
775
+ # something isn't right, so provoke an error just in case
776
+ exec(TRAN_ROLLBACK_QUERY) if tcount.zero?
777
+ end
487
778
  result
779
+ ensure
780
+ @client_tran_count = tcount
488
781
  end
489
782
  end
783
+
490
784
  end
491
785
  end
492
786