em-pg-client 0.2.1 → 0.3.0

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