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 +43 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/lib/em-mysqlplus.rb +7 -0
- data/lib/em-mysqlplus/connection.rb +140 -0
- data/lib/em-mysqlplus/mysql.rb +107 -0
- data/spec/helper.rb +5 -0
- data/spec/mysql_spec.rb +119 -0
- metadata +84 -0
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
|
data/lib/em-mysqlplus.rb
ADDED
@@ -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
data/spec/mysql_spec.rb
ADDED
@@ -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
|