em-postgres 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+