em-mysqlplus 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # EM-MySQL (Plus)
2
+
3
+ EventMachine wrapper for the C-based MySQL / MySQLPlus Ruby gems, which provides
4
+ callbacks, errbacks and all other niceties of EventMachine while keeping the API
5
+ of the original C-based MySQL gem.
6
+
7
+ Features:
8
+
9
+ * Maintains C-based mysql gem API
10
+ * Deferrables for every query with callback & errback
11
+ * Connection query queue - pile 'em up!
12
+ * Auto-reconnect on disconnects
13
+ * Auto-retry on deadlocks
14
+
15
+ ## Example usage:
16
+ > gem install em-mysqlplus
17
+ > irb -r em-mysqlplus
18
+
19
+ EventMachine.run {
20
+ conn = EventMachine::MySQL.new(:host => 'localhost')
21
+ query = conn.query("select 1+1")
22
+ query.callback { |res| p res.all_hashes }
23
+ query.errback { |res| p res.all_hashes }
24
+ }
25
+
26
+ ## Query queueing:
27
+
28
+ EventMachine.run {
29
+ conn = EventMachine::MySQL.new(:host => 'localhost')
30
+
31
+ results = []
32
+ conn.query("select 1") {|res| results.push res.fetch_row.first.to_i}
33
+ conn.query("select 2") {|res| results.push res.fetch_row.first.to_i}
34
+ conn.query("select 3") {|res| results.push res.fetch_row.first.to_i}
35
+
36
+ EventMachine.add_timer(0.05) {
37
+ p results # => [1,2,3]
38
+ }
39
+ }
40
+
41
+ # Credits
42
+
43
+ Original Async MySQL driver for Ruby/EventMachine - (c) 2008 Aman Gupta (tmm1)
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "em-mysqlplus"
7
+ gemspec.summary = "Async MySQL driver for Ruby/Eventmachine"
8
+ gemspec.description = gemspec.summary
9
+ gemspec.email = "ilya@igvita.com"
10
+ gemspec.homepage = "http://github.com/igrigorik/em-mysql"
11
+ gemspec.authors = ["Ilya Grigorik", "Aman Gupta"]
12
+ gemspec.add_dependency('eventmachine', '>= 0.12.9')
13
+ gemspec.rubyforge_project = "em-mysqlplus"
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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.2
@@ -0,0 +1,7 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require "eventmachine"
4
+
5
+ %w[ mysql connection ].each do |file|
6
+ require "em-mysqlplus/#{file}"
7
+ end
@@ -0,0 +1,140 @@
1
+
2
+ class Mysql
3
+ def result
4
+ @cur_result
5
+ end
6
+ end
7
+
8
+ module EventMachine
9
+ class MySQLConnection < 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
+ 'MySQL server has gone away',
19
+ 'Lost connection to MySQL server during query'
20
+ ] unless defined? DisconnectErrors
21
+
22
+ def initialize(mysql, opts, conn)
23
+ @conn = conn
24
+ @mysql = mysql
25
+ @fd = mysql.socket
26
+ @opts = opts
27
+ @current = nil
28
+ @queue = []
29
+ @processing = false
30
+ @connected = true
31
+
32
+ self.notify_readable = true
33
+ EM.add_timer(0){ next_query }
34
+ end
35
+
36
+ def notify_readable
37
+ if item = @current
38
+ sql, cblk, eblk, retries = item
39
+ result = @mysql.get_result
40
+
41
+ # kick off next query in the background
42
+ # as we process the current results
43
+ @current = nil
44
+ @processing = false
45
+ next_query
46
+
47
+ cblk.call(result)
48
+ else
49
+ return close
50
+ end
51
+
52
+ rescue Mysql::Error => e
53
+
54
+ if e.message =~ /Deadlock/ and retries < MAX_RETRIES_ON_DEADLOCKS
55
+ @queue << [sql, cblk, eblk, retries + 1]
56
+ @processing = false
57
+ next_query
58
+
59
+ elsif DisconnectErrors.include? e.message
60
+ @queue << [sql, cblk, eblk, retries + 1]
61
+ return close
62
+
63
+ elsif cb = (eblk || @opts[:on_error])
64
+ cb.call(e)
65
+ @processing = false
66
+ next_query
67
+
68
+ else
69
+ raise e
70
+ end
71
+ end
72
+
73
+ def unbind
74
+ # wait for the next tick until the current fd is removed completely from the reactor
75
+ #
76
+ # in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
77
+ # without next_tick in these cases, unbind will get fired on the newly attached signature as well
78
+ #
79
+ # do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
80
+ # reconnects to happen after all the unbinds have been processed
81
+
82
+ @connected = false
83
+
84
+ EM.add_timer(0) do
85
+ @processing = false
86
+ @mysql = @conn.connect_socket(@opts)
87
+ @fd = @mysql.socket
88
+
89
+ @signature = EM.attach_fd(@mysql.socket, true)
90
+ EM.set_notify_readable @signature, true
91
+ EM.instance_variable_get('@conns')[@signature] = self
92
+ @connected = true
93
+ next_query
94
+ end
95
+ end
96
+
97
+ def execute(sql, cblk = nil, eblk = nil, retries = 0)
98
+ begin
99
+ if not @processing or not @connected
100
+ @processing = true
101
+ @mysql.send_query(sql)
102
+ else
103
+ @queue << [sql, cblk, eblk, retries]
104
+ return
105
+ end
106
+
107
+ rescue Mysql::Error => e
108
+ if DisconnectErrors.include? e.message
109
+ @queue << [sql, cblk, eblk, retries]
110
+ return close
111
+ else
112
+ raise e
113
+ end
114
+ end
115
+
116
+ @current = [sql, cblk, eblk, retries]
117
+ end
118
+
119
+ # mysql gem has syncronous methods such as list_dbs
120
+ # and others which require that we execute without callbacks
121
+ def method_missing(method, *args, &blk)
122
+ @mysql.send(method, *args, &blk) if @mysql.respond_to? method
123
+ end
124
+
125
+ def close
126
+ @connected = false
127
+ detach
128
+ end
129
+
130
+ private
131
+
132
+ def next_query
133
+ if @connected and !@processing and pending = @queue.shift
134
+ sql, cblk, eblk = pending
135
+ execute(sql, cblk, eblk)
136
+ end
137
+ end
138
+
139
+ end
140
+ end
@@ -0,0 +1,107 @@
1
+ require "eventmachine"
2
+ require "mysqlplus"
3
+ require "fcntl"
4
+
5
+ module EventMachine
6
+ class MySQL
7
+
8
+ self::Mysql = ::Mysql unless defined? self::Mysql
9
+
10
+ attr_reader :connection
11
+
12
+ def initialize(opts)
13
+ unless EM.respond_to?(:watch) and Mysql.method_defined?(:socket)
14
+ raise RuntimeError, 'mysqlplus and EM.watch are required for EventedMysql'
15
+ end
16
+
17
+ @settings = { :debug => false }.merge!(opts)
18
+ @connection = connect(@settings)
19
+ end
20
+
21
+ def close
22
+ @connection.close
23
+ end
24
+
25
+ def query(sql, &blk)
26
+ df = EventMachine::DefaultDeferrable.new
27
+ cb = blk || Proc.new { |r| df.succeed(r) }
28
+ eb = Proc.new { |r| df.fail(r) }
29
+
30
+ @connection.execute(sql, cb, eb)
31
+
32
+ df
33
+ end
34
+ alias :real_query :query
35
+
36
+ # behave as a normal mysql connection
37
+ def method_missing(method, *args, &blk)
38
+ @connection.send(method, *args)
39
+ end
40
+
41
+ def connect(opts)
42
+ if conn = connect_socket(opts)
43
+ debug [:connect, conn.socket, opts]
44
+ EM.watch(conn.socket, EventMachine::MySQLConnection, conn, opts, self)
45
+ else
46
+ # invokes :errback callback in opts before firing again
47
+ debug [:reconnect]
48
+ EM.add_timer(5) { connect opts }
49
+ end
50
+ end
51
+
52
+ # stolen from sequel
53
+ def connect_socket(opts)
54
+ conn = Mysql.init
55
+
56
+ # set encoding _before_ connecting
57
+ if charset = opts[:charset] || opts[:encoding]
58
+ conn.options(Mysql::SET_CHARSET_NAME, charset)
59
+ end
60
+
61
+ conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
62
+ conn.real_connect(
63
+ opts[:host] || 'localhost',
64
+ opts[:user] || 'root',
65
+ opts[:password],
66
+ opts[:database],
67
+ opts[:port],
68
+ opts[:socket],
69
+ 0 +
70
+ # XXX multi results require multiple callbacks to parse
71
+ # Mysql::CLIENT_MULTI_RESULTS +
72
+ # Mysql::CLIENT_MULTI_STATEMENTS +
73
+ (opts[:compress] == false ? 0 : Mysql::CLIENT_COMPRESS)
74
+ )
75
+
76
+ # increase timeout so mysql server doesn't disconnect us
77
+ # this is especially bad if we're disconnected while EM.attach is
78
+ # still in progress, because by the time it gets to EM, the FD is
79
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
80
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
81
+ conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
82
+
83
+ # we handle reconnecting (and reattaching the new fd to EM)
84
+ conn.reconnect = false
85
+
86
+ # By default, MySQL 'where id is null' selects the last inserted id
87
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
88
+ conn.query("set SQL_AUTO_IS_NULL=0")
89
+
90
+ # get results for queries
91
+ conn.query_with_result = true
92
+
93
+ conn
94
+ rescue Mysql::Error => e
95
+ if cb = opts[:errback]
96
+ cb.call(e)
97
+ nil
98
+ else
99
+ raise e
100
+ end
101
+ end
102
+
103
+ def debug(data)
104
+ p data if @settings[:debug]
105
+ end
106
+ end
107
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+ require "spec"
4
+
5
+ require "lib/em-mysqlplus"
@@ -0,0 +1,119 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::MySQL do
4
+ it "should create a new connection" do
5
+ EventMachine.run {
6
+ lambda {
7
+ conn = EventMachine::MySQL.new(:host => 'localhost')
8
+ conn.connection.connected.should be_true
9
+
10
+ conn.close
11
+ conn.connection.connected.should be_false
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::MySQL.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
+ it "should execute sql" do
33
+ EventMachine.run {
34
+ conn = EventMachine::MySQL.new(:host => 'localhost')
35
+ query = conn.query("select 1")
36
+ query.callback { |res|
37
+ res.fetch_row.first.should == "1"
38
+ EventMachine.stop
39
+ }
40
+ }
41
+ end
42
+
43
+ it "should accept block as query callback" do
44
+ EventMachine.run {
45
+ conn = EventMachine::MySQL.new(:host => 'localhost')
46
+ conn.query("select 1") { |res|
47
+ res.fetch_row.first.should == "1"
48
+ EventMachine.stop
49
+ }
50
+ }
51
+ end
52
+
53
+ it "allow custom error callbacks for each query" do
54
+ EventMachine.run {
55
+ conn = EventMachine::MySQL.new(:host => 'localhost')
56
+ query = conn.query("select 1 from")
57
+ query.errback { |res|
58
+ res.class.should == Mysql::Error
59
+ EventMachine.stop
60
+ }
61
+ }
62
+ end
63
+
64
+ it "queue up queries and execute them in order" do
65
+ EventMachine.run {
66
+ conn = EventMachine::MySQL.new(:host => 'localhost')
67
+
68
+ results = []
69
+ conn.query("select 1") {|res| results.push res.fetch_row.first.to_i}
70
+ conn.query("select 2") {|res| results.push res.fetch_row.first.to_i}
71
+ conn.query("select 3") {|res| results.push res.fetch_row.first.to_i}
72
+
73
+ EventMachine.add_timer(0.05) {
74
+ results.should == [1,2,3]
75
+ EventMachine.stop
76
+ }
77
+ }
78
+ end
79
+
80
+ it "should continue processing queries after hitting an error" do
81
+ EventMachine.run {
82
+ conn = EventMachine::MySQL.new(:host => 'localhost')
83
+
84
+ conn.query("select 1+ from table")
85
+ conn.query("select 1+1") { |res|
86
+ res.fetch_row.first.to_i.should == 2
87
+ EventMachine.stop
88
+ }
89
+ }
90
+ end
91
+
92
+ it "should work with synchronous commands" do
93
+ EventMachine.run {
94
+ conn = EventMachine::MySQL.new(:host => 'localhost', :database => 'test')
95
+
96
+ conn.list_dbs.class.should == Array
97
+ conn.list_tables.class.should == Array
98
+ conn.quote("select '1'").should == "select \\'1\\'"
99
+
100
+ EventMachine.stop
101
+ }
102
+ end
103
+
104
+ # it "should reconnect when disconnected" do
105
+ # # to test, run:
106
+ # # mysqladmin5 -u root kill `mysqladmin -u root processlist | grep "select sleep(5)" | cut -d'|' -f2`
107
+ #
108
+ # EventMachine.run {
109
+ # conn = EventMachine::MySQL.new(:host => 'localhost')
110
+ #
111
+ # query = conn.query("select sleep(5)")
112
+ # query.callback {|res|
113
+ # res.fetch_row.first.to_i.should == 0
114
+ # EventMachine.stop
115
+ # }
116
+ # }
117
+ # end
118
+
119
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-mysqlplus
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 2
9
+ version: 0.1.2
10
+ platform: ruby
11
+ authors:
12
+ - Ilya Grigorik
13
+ - Aman Gupta
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-12 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
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 MySQL driver for Ruby/Eventmachine
36
+ email: ilya@igvita.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.md
43
+ files:
44
+ - README.md
45
+ - Rakefile
46
+ - VERSION
47
+ - lib/em-mysqlplus.rb
48
+ - lib/em-mysqlplus/connection.rb
49
+ - lib/em-mysqlplus/mysql.rb
50
+ - spec/helper.rb
51
+ - spec/mysql_spec.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/igrigorik/em-mysql
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project: em-mysqlplus
78
+ rubygems_version: 1.3.6
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Async MySQL driver for Ruby/Eventmachine
82
+ test_files:
83
+ - spec/helper.rb
84
+ - spec/mysql_spec.rb