├── VERSION ├── lib ├── em-postgres.rb ├── em-postgres │ ├── postgres.rb │ └── connection.rb └── sequel │ └── async.rb ├── spec ├── helper.rb └── postgres_spec.rb ├── README ├── Rakefile ├── em-postgres.gemspec └── test.rb /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.2 -------------------------------------------------------------------------------- /lib/em-postgres.rb: -------------------------------------------------------------------------------- 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 8 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 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" -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Async PostgreSQL driver for Ruby/EventMachine 2 | 3 | Requires pg 4 | 5 | To run the specs, you will need rspec, run "rspec spec/postgres_spec.rb" 6 | 7 | 8 | TODOS: 9 | get a working java driver 10 | ability to use different MRI postgres drivers (postgres-pr, pg, postgres) 11 | add working adaptors for activerecord and sequel -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /em-postgres.gemspec: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require 'lib/em/postgres' 2 | 3 | # EM.kqueue 4 | # EM.epoll 5 | EM.run{ 6 | EM.start_server '127.0.0.1', 12345 do |c| 7 | def c.receive_data data 8 | p 'sending http response' 9 | send_data "hello" 10 | close_connection_after_writing 11 | end 12 | end 13 | 14 | SQL = EventedPostgres 15 | def SQL(query, &blk) SQL.select(query, &blk) end 16 | 17 | if true 18 | 19 | SQL.settings.update :logging => true, 20 | :database => 'test', 21 | :connections => 1 22 | 23 | SQL.execute('select 1+2') 24 | 25 | EM.add_timer(1){ 26 | 3.times do SQL.select('select sleep(0.5)+1'){|r| p(r) } end 27 | } 28 | 29 | elsif false 30 | 31 | SQL.settings.update :logging => true, 32 | :database => 'test', 33 | :connections => 10 34 | 35 | EM.add_timer(2.5){ SQL.all('use test') } 36 | 37 | else 38 | 39 | SQL.settings.update :logging => true, 40 | :database => 'test', 41 | :connections => 10, 42 | :timeout => 1 43 | 44 | n = 0 45 | 46 | SQL.execute('drop table if exists testingabc'){ 47 | SQL.execute('create table testingabc (a int, b int, c int)'){ 48 | EM.add_periodic_timer(0.2) do 49 | cur_num = n+=1 50 | SQL.execute("insert into testingabc values (1,2,#{cur_num})"){ 51 | SQL("select * from testingabc where c = #{cur_num} limit 1"){ |res| puts;puts } 52 | } 53 | end 54 | } 55 | } 56 | 57 | end 58 | 59 | } -------------------------------------------------------------------------------- /lib/em-postgres/postgres.rb: -------------------------------------------------------------------------------- 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,params=[], &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,params,cb,eb) 65 | df 66 | end 67 | alias :real_query :query 68 | alias :execute :query 69 | # behave as a normal postgres connection 70 | def method_missing(method, *args, &blk) 71 | @connection.send(method, *args) 72 | end 73 | 74 | def connect(opts) 75 | if conn = connect_socket(opts) 76 | #debug [:connect, conn.socket, opts] 77 | #EM.watch(conn.socket, EventMachine::PostgresConnection, conn, opts, self) 78 | 79 | EM.watch(conn.socket, EventMachine::PostgresConnection,conn,opts,self) 80 | else 81 | # invokes :errback callback in opts before firing again 82 | debug [:reconnect] 83 | EM.add_timer(5) { connect opts } 84 | end 85 | end 86 | 87 | # stolen from sequel 88 | def connect_socket(opts) 89 | begin 90 | conn = PGconn.connect( 91 | opts[:host], 92 | (opts[:port]), #TODO deal with host and port 93 | nil,nil, 94 | opts[:database], 95 | opts[:user], 96 | opts[:password] 97 | ) 98 | # set encoding _before_ connecting 99 | if encoding = opts[:encoding] || opts[:charset] 100 | if conn.respond_to?(:set_client_encoding) 101 | conn.set_client_encoding(encoding) 102 | else 103 | conn.async_exec("set client_encoding to '#{encoding}'") 104 | end 105 | end 106 | 107 | #conn.options(Mysql::OPT_LOCAL_INFILE, 'client') 108 | 109 | # increase timeout so mysql server doesn't disconnect us 110 | # this is especially bad if we're disconnected while EM.attach is 111 | # still in progress, because by the time it gets to EM, the FD is 112 | # no longer valid, and it throws a c++ 'bad file descriptor' error 113 | # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60) 114 | #conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}") 115 | 116 | # we handle reconnecting (and reattaching the new fd to EM) 117 | #conn.reconnect = false 118 | 119 | # By default, MySQL 'where id is null' selects the last inserted id 120 | # Turn this off. http://dev.rubyonrails.org/ticket/6778 121 | #conn.query("set SQL_AUTO_IS_NULL=0") 122 | 123 | # get results for queries 124 | #conn.query_with_result = true 125 | 126 | conn 127 | rescue Exception => e 128 | puts "#{e} exception" 129 | if cb = opts[:errback] 130 | cb.call(e) 131 | nil 132 | else 133 | raise e 134 | end 135 | end 136 | end 137 | 138 | def debug(data) 139 | p data if @settings[:debug] 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/sequel/async.rb: -------------------------------------------------------------------------------- 1 | # async sequel extensions, for use with em-mysql 2 | # 3 | # require 'em/mysql' 4 | # DB = Sequel.connect(:adapter => 'mysql', :user => 'root', :database => 'test', ...) 5 | # EventedMysql.settings.update(..., :on_error => proc{|e| log 'error', e }) 6 | # 7 | # def log *args 8 | # p [Time.now, *args] 9 | # end 10 | # 11 | # DB[:table].where(:id < 100).async_update(:field => 'new value') do |num_updated| 12 | # log "done updating #{num_updated} rows" 13 | # end 14 | # 15 | # DB[:table].async_insert(:field => 'value') do |insert_id| 16 | # log "inserted row #{insert_id}" 17 | # end 18 | # 19 | # DB[:table].async_multi_insert([:field], [ ['one'], ['two'], ['three'] ]) do 20 | # log "done inserting 3 rows" 21 | # end 22 | # 23 | # DB[:table].limit(10).async_each do |row| 24 | # log "got a row", row 25 | # end; log "this will be printed before the query returns" 26 | # 27 | # DB[:table].async_all do |rows| 28 | # DB[:table].async_multi_insert([:field], rows.map{|r| "new_#{r[:field]}" }) 29 | # end 30 | # 31 | # DB[:table].async_all do |rows| 32 | # num = rows.size 33 | # 34 | # rows.each{ |r| 35 | # DB[:table].where(:id => r[:id]).async_update(:field => rand(10000).to_s) do 36 | # num = num-1 37 | # if num == 0 38 | # log "last update completed" 39 | # end 40 | # end 41 | # } 42 | # end 43 | # 44 | # DB[:table].async_count do |num_rows| 45 | # log "table has #{num_rows} rows" 46 | # end 47 | 48 | require 'sequel' 49 | raise 'need Sequel >= 3.2.0' unless Sequel::MAJOR == 3 and Sequel::MINOR >= 2 50 | 51 | module Sequel 52 | class Dataset 53 | def async_insert *args, &cb 54 | EventedMysql.insert insert_sql(*args), &cb 55 | nil 56 | end 57 | 58 | def async_insert_ignore *args, &cb 59 | EventedMysql.insert insert_ignore.insert_sql(*args), &cb 60 | nil 61 | end 62 | 63 | def async_update *args, &cb 64 | EventedMysql.update update_sql(*args), &cb 65 | nil 66 | end 67 | 68 | def async_delete &cb 69 | EventedMysql.execute delete_sql, &cb 70 | nil 71 | end 72 | 73 | def async_multi_insert *args, &cb 74 | EventedMysql.execute multi_insert_sql(*args).first, &cb 75 | nil 76 | end 77 | 78 | def async_multi_insert_ignore *args, &cb 79 | EventedMysql.execute insert_ignore.multi_insert_sql(*args).first, &cb 80 | nil 81 | end 82 | 83 | def async_fetch_rows sql, iter = :each 84 | EventedMysql.raw(sql) do |m| 85 | r = m.result 86 | 87 | i = -1 88 | cols = r.fetch_fields.map{|f| [output_identifier(f.name), Sequel::MySQL::MYSQL_TYPES[f.type], i+=1]} 89 | @columns = cols.map{|c| c.first} 90 | rows = [] 91 | while row = r.fetch_row 92 | h = {} 93 | cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v} 94 | if iter == :each 95 | yield h 96 | else 97 | rows << h 98 | end 99 | end 100 | yield rows if iter == :all 101 | end 102 | nil 103 | end 104 | 105 | def async_each 106 | async_fetch_rows(select_sql, :each) do |r| 107 | if row_proc = @row_proc 108 | yield row_proc.call(r) 109 | else 110 | yield r 111 | end 112 | end 113 | nil 114 | end 115 | 116 | def async_all 117 | async_fetch_rows(sql, :all) do |rows| 118 | if row_proc = @row_proc 119 | yield(rows.map{|r| row_proc.call(r) }) 120 | else 121 | yield(rows) 122 | end 123 | end 124 | nil 125 | end 126 | 127 | def async_count &cb 128 | if options_overlap(COUNT_FROM_SELF_OPTS) 129 | from_self.async_count(&cb) 130 | else 131 | clone(STOCK_COUNT_OPTS).async_each{|r| 132 | yield r.is_a?(Hash) ? r.values.first.to_i : r.values.values.first.to_i 133 | } 134 | end 135 | nil 136 | end 137 | end 138 | 139 | class Model 140 | def async_update *args, &cb 141 | this.async_update(*args, &cb) 142 | set(*args) 143 | self 144 | end 145 | 146 | def async_delete &cb 147 | this.async_delete(&cb) 148 | nil 149 | end 150 | 151 | class << self 152 | [ :async_insert, 153 | :async_insert_ignore, 154 | :async_multi_insert, 155 | :async_multi_insert_ignore, 156 | :async_each, 157 | :async_all, 158 | :async_update, 159 | :async_count ].each do |method| 160 | class_eval %[ 161 | def #{method} *args, &cb 162 | dataset.#{method}(*args, &cb) 163 | end 164 | ] 165 | end 166 | 167 | # async version of Model#[] 168 | def async_lookup args 169 | unless Hash === args 170 | args = primary_key_hash(args) 171 | end 172 | 173 | dataset.where(args).limit(1).async_all{ |rows| 174 | if rows.any? 175 | yield rows.first 176 | else 177 | yield nil 178 | end 179 | } 180 | nil 181 | end 182 | end 183 | end 184 | end -------------------------------------------------------------------------------- /spec/postgres_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(File.dirname(__FILE__)) 2 | require "helper" 3 | describe EventMachine::Postgres do 4 | 5 | it "should be true" do 6 | true.should be_true 7 | end 8 | it "should create a new connection" do 9 | EventMachine.run { 10 | lambda { 11 | conn = EventMachine::Postgres.new(:database => "test") 12 | EventMachine.stop 13 | }.should_not raise_error 14 | } 15 | end 16 | 17 | it "should invoke errback on connection failure" do 18 | EventMachine.run { 19 | lambda { 20 | conn = EventMachine::Postgres.new({ 21 | :host => 'localhost', 22 | :port => 20000, 23 | :socket => '', 24 | :errback => Proc.new { 25 | EventMachine.stop 26 | } 27 | }) 28 | }.should_not raise_error 29 | } 30 | end 31 | 32 | 33 | it "should execute sql" do 34 | EventMachine.run { 35 | #EM.add_periodic_timer(1){ puts } 36 | conn = EventMachine::Postgres.new(:database => "test") 37 | query = conn.execute("select 1;") 38 | 39 | query.callback{ |res| 40 | res.first["?column?"].should == "1" 41 | EventMachine.stop 42 | } 43 | } 44 | end 45 | 46 | it "should accept block as query callback" do 47 | EventMachine.run { 48 | conn = EventMachine::Postgres.new(:database => 'test') 49 | conn.execute("select 1;") { |res| 50 | res.first["?column?"].should == "1" 51 | EventMachine.stop 52 | } 53 | } 54 | end 55 | 56 | it "should accept paramaters" do 57 | EventMachine.run { 58 | conn = EventMachine::Postgres.new(:database => 'test') 59 | conn.execute("select $1::int AS first,$2::int AS second,$3::varchar AS third;",[1,nil,'']) { |res| 60 | res.first["first"].should == "1" 61 | res.first["second"].should == nil 62 | res.first["third"].should == "" 63 | EventMachine.stop 64 | } 65 | } 66 | end 67 | 68 | it "allow custom error callbacks for each query" do 69 | EventMachine.run { 70 | conn = EventMachine::Postgres.new(:database => "test") 71 | query = conn.execute("select 1 from") 72 | query.errback { |res| 73 | #res.class.should == Mysql::Error 74 | 1.should == 1 75 | EventMachine.stop 76 | 1.should == 2 #we should never get here 77 | } 78 | } 79 | end 80 | 81 | 82 | it "queue up queries and execute them in order" do 83 | EventMachine.run { 84 | conn = EventMachine::Postgres.new(:database => 'test') 85 | 86 | results = [] 87 | conn.execute("select 1 AS x;") {|res| puts res.inspect; results.push(res.first["x"].to_i)} 88 | conn.execute("select 2 AS x;") {|res| puts res.inspect;results.push(res.first["x"].to_i)} 89 | conn.execute("select 3 AS x;") {|res| puts res.inspect;results.push(res.first["x"].to_i)} 90 | EventMachine.add_timer(0.05) { 91 | results.should == [1,2,3] 92 | #conn.connection_pool.first.close 93 | 94 | EventMachine.stop 95 | } 96 | } 97 | end 98 | 99 | 100 | it "queue up large amount of queries and execute them in order" do 101 | EventMachine.run { 102 | 103 | conn = EventMachine::Postgres.new(:database => 'test') 104 | 105 | results = [] 106 | (1..100).each do |i| 107 | conn.execute("select #{i} AS x;") {|res| results.push(res.first["x"].to_i)} 108 | 109 | end 110 | EventMachine.add_timer(1) { 111 | results.should == (1..100).to_a 112 | EventMachine.stop 113 | } 114 | } 115 | end 116 | 117 | 118 | it "should continue processing queries after hitting an error" do 119 | EventMachine.run { 120 | conn = EventMachine::Postgres.new(:database=> 'test') 121 | #errorback = Proc.new{ 122 | # true.should == true 123 | #EventMachine.stop 124 | #} 125 | q = conn.execute("select 1+ from table;") 126 | q.errback{|r| puts "hi"; true.should == true } 127 | conn.execute("select 1+1;"){ |res| 128 | res.first["?column?"].to_i.should == 2 129 | EventMachine.stop 130 | } 131 | } 132 | end 133 | 134 | it "should work with bind parameters" do 135 | EventMachine.run { 136 | conn = EventMachine::Postgres.new(:database=> 'test') 137 | conn.execute("select $1::int as bind1;",[4]){|r| 138 | r.first["bind1"].to_i.should == 4 139 | } 140 | conn.execute("select $1::text as bind1;",['four']){|r| 141 | r.first["bind1"].should == 'four' 142 | EventMachine.stop 143 | } 144 | 145 | } 146 | end 147 | 148 | it "should allow the creation of a prepare statement" do 149 | EventMachine.run { 150 | conn = EventMachine::Postgres.new(:database=> 'test') 151 | prepare_name = "random#{rand(69)}" 152 | i = rand(69) 153 | conn.prepare(prepare_name,"select #{i};") 154 | conn.execute(prepare_name){|r| 155 | r.first["?column?"].to_i.should == i 156 | EventMachine.stop 157 | } 158 | } 159 | end 160 | 161 | 162 | 163 | =begin 164 | it "should work with synchronous commands" do 165 | EventMachine.run { 166 | conn = EventMachine::Postgres #.new(:database => 'test') 167 | 168 | conn.list_dbs.class.should == Array 169 | conn.list_tables.class.should == Array 170 | conn.quote("select '1'").should == "select \\'1\\'" 171 | 172 | EventMachine.stop 173 | } 174 | end 175 | =end 176 | # it "should reconnect when disconnected" do 177 | # # to test, run: 178 | # # mysqladmin5 -u root kill `mysqladmin -u root processlist | grep "select sleep(5)" | cut -d'|' -f2` 179 | # 180 | # EventMachine.run { 181 | # conn = EventMachine::MySQL.new(:host => 'localhost') 182 | # 183 | # query = conn.query("select sleep(5)") 184 | # query.callback {|res| 185 | # res.fetch_row.first.to_i.should == 0 186 | # EventMachine.stop 187 | # } 188 | # } 189 | # end 190 | 191 | end -------------------------------------------------------------------------------- /lib/em-postgres/connection.rb: -------------------------------------------------------------------------------- 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,params, 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 | unless @postgres.error_message == "" 104 | #TODO this is wrong 105 | eb = (eblk || @opts[:on_error]) 106 | eb.call(result) if eb 107 | result.clear 108 | #reconnect 109 | @processing = false 110 | #@current = nil 111 | return next_query 112 | end 113 | # kick off next query in the background 114 | # as we process the current results 115 | @current = nil 116 | @processing = false 117 | cblk.call(result) if cblk 118 | result.clear 119 | next_query 120 | else 121 | return close 122 | end 123 | 124 | rescue Exception => e 125 | puts "error #{e}" 126 | if e.message =~ /Deadlock/ and retries < MAX_RETRIES_ON_DEADLOCKS 127 | @queue << [sql, cblk, eblk, retries + 1] 128 | @processing = false 129 | next_query 130 | 131 | elsif DisconnectErrors.include? e.message 132 | @queue << [sql,params, cblk, eblk, retries + 1] 133 | return #close 134 | 135 | elsif cb = (eblk || @opts[:on_error]) 136 | cb.call(e) 137 | @processing = false 138 | next_query 139 | 140 | else 141 | raise e 142 | end 143 | end 144 | 145 | def unbind 146 | 147 | # wait for the next tick until the current fd is removed completely from the reactor 148 | # 149 | # in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used 150 | # without next_tick in these cases, unbind will get fired on the newly attached signature as well 151 | # 152 | # do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want 153 | # reconnects to happen after all the unbinds have been processed 154 | 155 | #@connected = false 156 | EM.next_tick { reconnect } 157 | end 158 | 159 | def reconnect 160 | @processing = false 161 | @postgres = @conn.connect_socket(@opts) 162 | @fd = @postgres.socket 163 | 164 | @signature = EM.attach_fd(@postgres.socket, true) 165 | EM.set_notify_readable(@signature, true) 166 | EM.instance_variable_get('@conns')[@signature] = self 167 | @connected = true 168 | next_query 169 | 170 | rescue Exception => e 171 | EM.add_timer(1) { reconnect } 172 | end 173 | 174 | def execute(sql,params=nil, cblk = nil, eblk = nil, retries = 0) 175 | begin 176 | if not @processing or not @connected 177 | #if !@processing || !@connected 178 | @processing = true 179 | 180 | if sql =~ /\s+/ 181 | @postgres.send_query(sql,params) 182 | else 183 | @postgres.send_query_prepared(sql,params) 184 | end 185 | else 186 | @queue << [sql,params, 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,params, cblk, eblk, retries] 194 | return #close 195 | else 196 | raise e 197 | end 198 | end 199 | @current = [sql,params, cblk, eblk, retries] 200 | end 201 | 202 | # act like the pg driver 203 | def method_missing(method, *args, &blk) 204 | if @postgres.respond_to? method 205 | @postgres.send(method, *args, &blk) 206 | else 207 | super 208 | end 209 | end 210 | 211 | def close 212 | return unless @connected 213 | detach 214 | @postgres.finish 215 | @connected = false 216 | end 217 | 218 | private 219 | 220 | def next_query 221 | if @connected and !@processing and pending = @queue.shift 222 | sql, params, cblk, eblk = pending 223 | execute(sql, params, cblk, eblk) 224 | end 225 | end 226 | 227 | end 228 | end --------------------------------------------------------------------------------