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/.yardopts +1 -0
- data/{BENCHMARKS.rdoc → BENCHMARKS.md} +15 -10
- data/{HISTORY.rdoc → HISTORY.md} +32 -5
- data/LICENSE +21 -0
- data/README.md +392 -0
- data/Rakefile +30 -14
- data/em-pg-client.gemspec +8 -7
- data/lib/em-pg-client.rb +1 -0
- data/lib/em-synchrony/pg.rb +2 -107
- data/lib/pg/em-version.rb +5 -0
- data/lib/pg/em.rb +638 -344
- data/lib/pg/em/client/connect_watcher.rb +65 -0
- data/lib/pg/em/client/watcher.rb +102 -0
- data/lib/pg/em/connection_pool.rb +448 -0
- data/lib/pg/em/featured_deferrable.rb +43 -0
- data/spec/connection_pool_helpers.rb +89 -0
- data/spec/{em_devel_client.rb → em_client.rb} +3 -2
- data/spec/em_client_autoreconnect.rb +268 -144
- data/spec/em_client_common.rb +55 -54
- data/spec/em_synchrony_client.rb +254 -5
- data/spec/em_synchrony_client_autoreconnect.rb +154 -130
- data/spec/pg_em_client_connect_finish.rb +54 -0
- data/spec/pg_em_client_connect_timeout.rb +91 -0
- data/spec/pg_em_client_options.rb +85 -0
- data/spec/pg_em_connection_pool.rb +655 -0
- data/spec/pg_em_featured_deferrable.rb +125 -0
- metadata +64 -34
- data/README.rdoc +0 -431
- data/spec/em_release_client.rb +0 -39
data/Rakefile
CHANGED
@@ -4,40 +4,56 @@ task :default => [:test]
|
|
4
4
|
|
5
5
|
$gem_name = "em-pg-client"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
32
|
+
desc "Run safe tests only"
|
33
|
+
task :safe => [:warn, :spec] do
|
19
34
|
%w[
|
20
|
-
spec/
|
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
|
-
|
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 "
|
81
|
+
sh "yardoc"
|
66
82
|
end
|
67
83
|
|
68
84
|
desc "Benchmark"
|
data/em-pg-client.gemspec
CHANGED
@@ -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 =
|
6
|
-
s.required_ruby_version = ">= 1.9.
|
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.
|
19
|
+
"--main" << "README.md"
|
18
20
|
s.has_rdoc = true
|
19
|
-
s.extra_rdoc_files = ["README.
|
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.
|
22
|
-
s.add_runtime_dependency "eventmachine", "
|
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
|
data/lib/em-pg-client.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'pg/em'
|
data/lib/em-synchrony/pg.rb
CHANGED
@@ -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
|
data/lib/pg/em.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
#
|
49
|
-
# which (re)defines methods:
|
29
|
+
# The following new methods:
|
50
30
|
#
|
51
|
-
# -
|
52
|
-
# -
|
53
|
-
# -
|
54
|
-
# -
|
55
|
-
# -
|
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
|
-
#
|
38
|
+
# are added to execute queries asynchronously,
|
39
|
+
# returning +Deferrable+ object.
|
58
40
|
#
|
59
|
-
#
|
41
|
+
# The following methods of PG::Connection[http://deveiate.org/code/pg/PG/Connection.html]
|
42
|
+
# are overloaded:
|
60
43
|
#
|
61
|
-
# -
|
62
|
-
# -
|
63
|
-
# -
|
64
|
-
# -
|
65
|
-
# -
|
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
|
-
#
|
68
|
-
# (
|
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
|
-
#
|
71
|
-
#
|
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
|
-
# -
|
74
|
-
#
|
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
|
89
|
-
# ::new
|
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
|
-
#
|
98
|
-
#
|
99
|
-
# If you are using a connection pool, make sure to acquire single
|
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
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
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
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
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
|
-
#
|
116
|
-
#
|
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
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
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
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
164
|
+
# You may pass this proc as an option to {new} or {connect_defer}.
|
145
165
|
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
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
|
-
|
158
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
244
|
-
|
245
|
-
|
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.
|
289
|
-
when
|
290
|
-
|
194
|
+
case key.to_sym
|
195
|
+
when :async_autoreconnect
|
196
|
+
options[:@async_autoreconnect] = value
|
291
197
|
true
|
292
|
-
when
|
293
|
-
raise ArgumentError
|
294
|
-
when
|
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
|
-
|
297
|
-
|
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
|
301
|
-
|
208
|
+
when :connect_timeout
|
209
|
+
options[:@connect_timeout] = value.to_f
|
302
210
|
false
|
303
|
-
when
|
304
|
-
|
211
|
+
when :query_timeout
|
212
|
+
options[:@query_timeout] = value.to_f
|
305
213
|
true
|
306
214
|
end
|
307
215
|
end
|
308
216
|
end
|
309
|
-
|
310
|
-
|
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
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
#
|
319
|
-
#
|
320
|
-
#
|
321
|
-
#
|
322
|
-
#
|
323
|
-
#
|
324
|
-
|
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 =
|
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.
|
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
|
-
#
|
339
|
-
#
|
340
|
-
#
|
341
|
-
|
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.
|
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
|
-
#
|
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
|
-
|
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
|
358
|
-
#
|
320
|
+
# Creates new instance of PG::EM::Client and attempts to establish
|
321
|
+
# connection.
|
359
322
|
#
|
360
|
-
#
|
361
|
-
#
|
362
|
-
#
|
363
|
-
#
|
364
|
-
#
|
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.
|
361
|
+
Client.parse_async_options(args).each {|k, v| instance_variable_set(k, v) }
|
367
362
|
super(*args)
|
368
363
|
end
|
369
364
|
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
#
|
403
|
+
# @!visibility private
|
404
|
+
# Perform auto re-connect. Used internally.
|
382
405
|
def async_autoreconnect!(deferrable, error, &send_proc)
|
383
|
-
if
|
384
|
-
|
385
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
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
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
class_eval <<-EOD
|
417
|
-
def
|
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
|
-
|
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
|
-
|
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 :
|
448
|
-
alias_method :
|
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
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
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
|
-
|
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
|
-
|
463
|
-
|
665
|
+
alias_method :query, :exec
|
666
|
+
alias_method :async_query, :exec
|
667
|
+
alias_method :async_exec, :exec
|
464
668
|
|
465
|
-
|
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
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
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
|
|