em-postgres 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,12 @@
1
+ Async PostgreSQL driver for Ruby/EventMachine
2
+ based off Aman Gupta's mysql driver
3
+
4
+
5
+ Requires pg
6
+
7
+
8
+
9
+ TODOS:
10
+ get a working java driver
11
+ ability to use different MRI postgres drivers (postgres-pr, pg, postgres)
12
+ add working adaptors for activerecord and sequel
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "em-postgres"
7
+ gemspec.summary = "Async PostgreSQL driver for Ruby/Eventmachine"
8
+ gemspec.description = gemspec.summary
9
+ gemspec.email = "jtoy@jtoy.net"
10
+ gemspec.homepage = "http://github.com/jtoy/em-postgres"
11
+ gemspec.authors = ["Jason Toy"]
12
+ gemspec.add_dependency('eventmachine', '>= 0.12.9')
13
+ gemspec.rubyforge_project = "em-postgres"
14
+ end
15
+
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
@@ -0,0 +1,24 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'em-postgres'
3
+ s.version = '0.0.1'
4
+ s.date = '2011-08-25'
5
+ s.summary = 'Async PostgreSQL client API for Ruby/EventMachine'
6
+ s.email = "jtoy@jtoy.net"
7
+ s.homepage = "http://github.com/jtoy/em-postgres"
8
+ s.description = 'Async PostgreSQL client API for Ruby/EventMachine'
9
+ s.has_rdoc = false
10
+ s.authors = ["Jason Toy"]
11
+ s.add_dependency('eventmachine', '>= 0.12.9')
12
+
13
+ # git ls-files
14
+ s.files = %w[
15
+ README
16
+ Rakefile
17
+ em-postgres.gemspec
18
+ lib/em-postgres/postgres.rb
19
+ lib/em-postgres/connection.rb
20
+ lib/em-postgres.rb
21
+ spec/helper.rb
22
+ spec/postgres_spec.rb
23
+ ]
24
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require "eventmachine"
4
+
5
+ %w[ postgres connection ].each do |file|
6
+ require "em-postgres/#{file}"
7
+ end
@@ -0,0 +1,224 @@
1
+
2
+ class Postgres
3
+ def result
4
+ @cur_result
5
+ end
6
+ end
7
+
8
+ module EventMachine
9
+ class PostgresConnection < EventMachine::Connection
10
+
11
+ attr_reader :processing, :connected, :opts
12
+ alias :settings :opts
13
+
14
+ MAX_RETRIES_ON_DEADLOCKS = 10
15
+
16
+ DisconnectErrors = [
17
+ 'query: not connected',
18
+ 'Postgres server has gone away',
19
+ 'Lost connection to Postgres server during query'
20
+ ] unless defined? DisconnectErrors
21
+
22
+ def initialize(postgres,opts,conn)
23
+ #def initialize(postgres,opts)
24
+
25
+ begin
26
+ @conn = conn
27
+ @postgres = postgres
28
+ @fd = postgres.socket
29
+ @opts = opts
30
+ @current = nil
31
+ @queue = []
32
+ @processing = false
33
+ @connected = true
34
+
35
+ self.notify_readable = true
36
+ EM.add_timer(0){ next_query }
37
+ rescue => e
38
+ puts e.inspect
39
+ end
40
+ end
41
+
42
+ def self.connect(opts)
43
+ if conn = connect_socket(opts)
44
+ #debug [:connect, conn.socket, opts]
45
+ EM.watch(conn.socket, EventMachine::PostgresConnection,conn,opts)
46
+ else
47
+ # invokes :errback callback in opts before firing again
48
+ debug [:reconnect]
49
+ EM.add_timer(5) { connect opts }
50
+ end
51
+ end
52
+
53
+ # stolen from sequel
54
+ def self.connect_socket(opts)
55
+ begin
56
+ conn = PGconn.connect(
57
+ opts[:host],
58
+ (opts[:port]), #TODO deal with host and port
59
+ nil,nil,
60
+ opts[:database],
61
+ opts[:user],
62
+ opts[:password]
63
+ )
64
+ # set encoding _before_ connecting
65
+ if encoding = opts[:encoding] || opts[:charset]
66
+ if conn.respond_to?(:set_client_encoding)
67
+ conn.set_client_encoding(encoding)
68
+ else
69
+ conn.async_exec("set client_encoding to '#{encoding}'")
70
+ end
71
+ end
72
+ conn
73
+ rescue Exception => e
74
+ puts "#{e} exception"
75
+ if cb = opts[:errback]
76
+ cb.call(e)
77
+ nil
78
+ else
79
+ raise e
80
+ end
81
+ end
82
+ end
83
+
84
+ def notify_readable
85
+ if item = @current
86
+ sql, cblk, eblk, retries = item
87
+ #results = []
88
+ #result = nil
89
+ #@postgres.get_result{|r| result = r}
90
+ #@postgres.get_result #TODO remove this, I can't process anymore code without this.
91
+ result = nil
92
+ loop do
93
+ # Fetch the next result. If there isn't one, the query is
94
+ # finished
95
+ item = @postgres.get_result
96
+ if item
97
+ result = item
98
+ else
99
+ break
100
+ end
101
+ #puts "\n\nQuery result:\n%p\n" % [ result.values ]
102
+ end
103
+
104
+ unless @postgres.error_message == ""
105
+ #TODO this is wrong
106
+ eb = (eblk || @opts[:on_error])
107
+ eb.call(result) if eb
108
+ result.clear
109
+ #reconnect
110
+ @processing = false
111
+ #@current = nil
112
+ return next_query
113
+ end
114
+ # kick off next query in the background
115
+ # as we process the current results
116
+ @current = nil
117
+ @processing = false
118
+ cblk.call(result) if cblk
119
+ result.clear
120
+ next_query
121
+ else
122
+ return close
123
+ end
124
+
125
+ rescue Exception => e
126
+ puts "error #{e}"
127
+ if e.message =~ /Deadlock/ and retries < MAX_RETRIES_ON_DEADLOCKS
128
+ @queue << [sql, cblk, eblk, retries + 1]
129
+ @processing = false
130
+ next_query
131
+
132
+ elsif DisconnectErrors.include? e.message
133
+ @queue << [sql, cblk, eblk, retries + 1]
134
+ return #close
135
+
136
+ elsif cb = (eblk || @opts[:on_error])
137
+ cb.call(e)
138
+ @processing = false
139
+ next_query
140
+
141
+ else
142
+ raise e
143
+ end
144
+ end
145
+
146
+ def unbind
147
+
148
+ # wait for the next tick until the current fd is removed completely from the reactor
149
+ #
150
+ # in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
151
+ # without next_tick in these cases, unbind will get fired on the newly attached signature as well
152
+ #
153
+ # do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
154
+ # reconnects to happen after all the unbinds have been processed
155
+
156
+ #@connected = false
157
+ EM.next_tick { reconnect }
158
+ end
159
+
160
+ def reconnect
161
+ puts "DDDDD"
162
+ @processing = false
163
+ @postgres = @conn.connect_socket(@opts)
164
+ @fd = @postgres.socket
165
+
166
+ @signature = EM.attach_fd(@postgres.socket, true)
167
+ EM.set_notify_readable(@signature, true)
168
+ EM.instance_variable_get('@conns')[@signature] = self
169
+ @connected = true
170
+ next_query
171
+
172
+ rescue Exception => e
173
+ EM.add_timer(1) { reconnect }
174
+ end
175
+
176
+
177
+ def execute(sql, cblk = nil, eblk = nil, retries = 0)
178
+
179
+ begin
180
+ if not @processing or not @connected
181
+ #if !@processing || !@connected
182
+ @processing = true
183
+
184
+ @postgres.send_query(sql)
185
+ else
186
+ @queue << [sql, cblk, eblk, retries]
187
+ return
188
+ end
189
+
190
+ rescue Exception => e
191
+ puts "error in execute #{e}"
192
+ if DisconnectErrors.include? e.message
193
+ @queue << [sql, cblk, eblk, retries]
194
+ return #close
195
+ else
196
+ raise e
197
+ end
198
+ end
199
+ @current = [sql, cblk, eblk, retries]
200
+ end
201
+
202
+ # act like the pg driver
203
+ def method_missing(method, *args, &blk)
204
+ @postgres.send(method, *args, &blk) if @postres.respond_to? method
205
+ end
206
+
207
+ def close
208
+ return unless @connected
209
+ detach
210
+ @postgres.finish
211
+ @connected = false
212
+ end
213
+
214
+ private
215
+
216
+ def next_query
217
+ if @connected and !@processing and pending = @queue.shift
218
+ sql, cblk, eblk = pending
219
+ execute(sql, cblk, eblk)
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -0,0 +1,143 @@
1
+ require "eventmachine"
2
+ require "pg"
3
+ require "fcntl"
4
+
5
+ =begin
6
+ module EventMachine
7
+ class Postgres
8
+
9
+ def self.settings
10
+ @settings ||= { :connections => 1, :logging => false,:database=>"test" }
11
+ end
12
+
13
+ def self.execute query, cblk = nil, eblk = nil, &blk
14
+ @n ||= 0
15
+ connection = connection_pool[@n]
16
+ @n = 0 if (@n+=1) >= connection_pool.size
17
+
18
+ #connection.execute(query, type, cblk, eblk, &blk)
19
+
20
+ df = EventMachine::DefaultDeferrable.new
21
+ cb = blk || Proc.new { |r| df.succeed(r) }
22
+ eb = Proc.new { |r| df.fail(r) }
23
+ connection.execute(query,cb, eb)
24
+ df
25
+ end
26
+ #class << self
27
+ # alias query execute
28
+ #end
29
+ def self.connection_pool
30
+ @connection_pool ||= (1..settings[:connections]).map{ EventMachine::PostgresConnection.connect(settings) }
31
+
32
+ end
33
+ end
34
+ end
35
+ =end
36
+
37
+
38
+ module EventMachine
39
+ class Postgres
40
+
41
+ #self::Postgres = ::Postgres unless defined? self::Postgres
42
+
43
+ attr_reader :connection
44
+
45
+ def initialize(opts)
46
+ unless EM.respond_to?(:watch) and PGconn.method_defined?(:socket)
47
+
48
+ raise RuntimeError, 'pg and EM.watch are required for EventedPostgres'
49
+ end
50
+
51
+ @settings = { :debug => false }.merge!(opts)
52
+ @connection = connect(@settings)
53
+ end
54
+
55
+ def close
56
+ @connection.close
57
+ end
58
+
59
+ def query(sql, &blk)
60
+ df = EventMachine::DefaultDeferrable.new
61
+ cb = blk || Proc.new { |r| df.succeed(r) }
62
+ eb = Proc.new { |r| df.fail(r) }
63
+
64
+ @connection.execute(sql, cb, eb)
65
+
66
+ df
67
+ end
68
+ alias :real_query :query
69
+ alias :execute :query
70
+ # behave as a normal postgres connection
71
+ def method_missing(method, *args, &blk)
72
+ @connection.send(method, *args)
73
+ end
74
+
75
+ def connect(opts)
76
+ if conn = connect_socket(opts)
77
+ #debug [:connect, conn.socket, opts]
78
+ #EM.watch(conn.socket, EventMachine::PostgresConnection, conn, opts, self)
79
+
80
+ EM.watch(conn.socket, EventMachine::PostgresConnection,conn,opts,self)
81
+ else
82
+ # invokes :errback callback in opts before firing again
83
+ debug [:reconnect]
84
+ EM.add_timer(5) { connect opts }
85
+ end
86
+ end
87
+
88
+ # stolen from sequel
89
+ def connect_socket(opts)
90
+ begin
91
+ conn = PGconn.connect(
92
+ opts[:host],
93
+ (opts[:port]), #TODO deal with host and port
94
+ nil,nil,
95
+ opts[:database],
96
+ opts[:user],
97
+ opts[:password]
98
+ )
99
+ # set encoding _before_ connecting
100
+ if encoding = opts[:encoding] || opts[:charset]
101
+ if conn.respond_to?(:set_client_encoding)
102
+ conn.set_client_encoding(encoding)
103
+ else
104
+ conn.async_exec("set client_encoding to '#{encoding}'")
105
+ end
106
+ end
107
+
108
+ #conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
109
+
110
+ # increase timeout so mysql server doesn't disconnect us
111
+ # this is especially bad if we're disconnected while EM.attach is
112
+ # still in progress, because by the time it gets to EM, the FD is
113
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
114
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
115
+ #conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
116
+
117
+ # we handle reconnecting (and reattaching the new fd to EM)
118
+ #conn.reconnect = false
119
+
120
+ # By default, MySQL 'where id is null' selects the last inserted id
121
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
122
+ #conn.query("set SQL_AUTO_IS_NULL=0")
123
+
124
+ # get results for queries
125
+ #conn.query_with_result = true
126
+
127
+ conn
128
+ rescue Exception => e
129
+ puts "#{e} exception"
130
+ if cb = opts[:errback]
131
+ cb.call(e)
132
+ nil
133
+ else
134
+ raise e
135
+ end
136
+ end
137
+ end
138
+
139
+ def debug(data)
140
+ p data if @settings[:debug]
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require "rubygems"
3
+ require "eventmachine"
4
+ require "rspec"
5
+ require "em-postgres"
6
+ require "ruby-debug"
@@ -0,0 +1,155 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__))
2
+ require "helper"
3
+ require "ruby-debug"
4
+ describe EventMachine::Postgres do
5
+
6
+ it "should be true" do
7
+ true.should be_true
8
+ end
9
+ it "should create a new connection" do
10
+ EventMachine.run {
11
+ lambda {
12
+ conn = EventMachine::Postgres.new(:database => "test")
13
+ EventMachine.stop
14
+ }.should_not raise_error
15
+ }
16
+ end
17
+
18
+ it "should invoke errback on connection failure" do
19
+ EventMachine.run {
20
+ lambda {
21
+ conn = EventMachine::Postgres.new({
22
+ :host => 'localhost',
23
+ :port => 20000,
24
+ :socket => '',
25
+ :errback => Proc.new {
26
+ EventMachine.stop
27
+ }
28
+ })
29
+ }.should_not raise_error
30
+ }
31
+ end
32
+
33
+
34
+ it "should execute sql" do
35
+ EventMachine.run {
36
+ #EM.add_periodic_timer(1){ puts }
37
+ conn = EventMachine::Postgres.new(:database => "test")
38
+ query = conn.execute("select 1;")
39
+
40
+ query.callback{ |res|
41
+ res.first["?column?"].should == "1"
42
+ EventMachine.stop
43
+ }
44
+ }
45
+ end
46
+
47
+ it "should accept block as query callback" do
48
+ EventMachine.run {
49
+ conn = EventMachine::Postgres.new(:database => 'test')
50
+ conn.execute("select 1;") { |res|
51
+ res.first["?column?"].should == "1"
52
+ EventMachine.stop
53
+ }
54
+ }
55
+ end
56
+
57
+
58
+
59
+ it "allow custom error callbacks for each query" do
60
+ EventMachine.run {
61
+ conn = EventMachine::Postgres.new(:database => "test")
62
+ query = conn.execute("select 1 from")
63
+ query.errback { |res|
64
+ #res.class.should == Mysql::Error
65
+ 1.should == 1
66
+ EventMachine.stop
67
+ 1.should == 2 #we should never get here
68
+ }
69
+ }
70
+ end
71
+
72
+
73
+ it "queue up queries and execute them in order" do
74
+ EventMachine.run {
75
+ conn = EventMachine::Postgres.new(:database => 'test')
76
+
77
+ results = []
78
+ #debugger
79
+ conn.execute("select 1 AS x;") {|res| puts res.inspect; results.push(res.first["x"].to_i)}
80
+ conn.execute("select 2 AS x;") {|res| puts res.inspect;results.push(res.first["x"].to_i)}
81
+ conn.execute("select 3 AS x;") {|res| puts res.inspect;results.push(res.first["x"].to_i)}
82
+ #debugger
83
+ EventMachine.add_timer(0.05) {
84
+ results.should == [1,2,3]
85
+ #conn.connection_pool.first.close
86
+
87
+ EventMachine.stop
88
+ }
89
+ }
90
+ end
91
+
92
+
93
+ it "queue up large amount of queries and execute them in order" do
94
+ EventMachine.run {
95
+
96
+ conn = EventMachine::Postgres.new(:database => 'test')
97
+
98
+ results = []
99
+ (1..100).each do |i|
100
+ conn.execute("select #{i} AS x;") {|res| results.push(res.first["x"].to_i)}
101
+
102
+ end
103
+ EventMachine.add_timer(1) {
104
+ results.should == (1..100).to_a
105
+ EventMachine.stop
106
+ }
107
+ }
108
+ end
109
+
110
+
111
+ it "should continue processing queries after hitting an error" do
112
+ EventMachine.run {
113
+ conn = EventMachine::Postgres.new(:database=> 'test')
114
+ #errorback = Proc.new{
115
+ # true.should == true
116
+ #EventMachine.stop
117
+ #}
118
+ q = conn.execute("select 1+ from table;")
119
+ q.errback{|r| puts "hi"; true.should == true }
120
+ conn.execute("select 1+1;"){ |res|
121
+ res.first["?column?"].to_i.should == 2
122
+ EventMachine.stop
123
+ }
124
+ }
125
+ end
126
+
127
+ =begin
128
+ it "should work with synchronous commands" do
129
+ EventMachine.run {
130
+ conn = EventMachine::Postgres #.new(:database => 'test')
131
+
132
+ conn.list_dbs.class.should == Array
133
+ conn.list_tables.class.should == Array
134
+ conn.quote("select '1'").should == "select \\'1\\'"
135
+
136
+ EventMachine.stop
137
+ }
138
+ end
139
+ =end
140
+ # it "should reconnect when disconnected" do
141
+ # # to test, run:
142
+ # # mysqladmin5 -u root kill `mysqladmin -u root processlist | grep "select sleep(5)" | cut -d'|' -f2`
143
+ #
144
+ # EventMachine.run {
145
+ # conn = EventMachine::MySQL.new(:host => 'localhost')
146
+ #
147
+ # query = conn.query("select sleep(5)")
148
+ # query.callback {|res|
149
+ # res.fetch_row.first.to_i.should == 0
150
+ # EventMachine.stop
151
+ # }
152
+ # }
153
+ # end
154
+
155
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-postgres
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Jason Toy
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-08-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: eventmachine
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 12
31
+ - 9
32
+ version: 0.12.9
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Async PostgreSQL client API for Ruby/EventMachine
36
+ email: jtoy@jtoy.net
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - README
45
+ - Rakefile
46
+ - em-postgres.gemspec
47
+ - lib/em-postgres/postgres.rb
48
+ - lib/em-postgres/connection.rb
49
+ - lib/em-postgres.rb
50
+ - spec/helper.rb
51
+ - spec/postgres_spec.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/jtoy/em-postgres
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.7
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Async PostgreSQL client API for Ruby/EventMachine
84
+ test_files: []
85
+