em-mysqlplus 0.1.2

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.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