jrmey-mysqlplus 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/mysqlplus.rb ADDED
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/mysql' # load our version of mysql--note
2
+ # if someone does a require 'mysql' after a require 'mysqlplus' then their screen will be littered with warnings
3
+ # and the "old" mysql will override the "new" mysqlplus, so be careful.
4
+
5
+ #
6
+ # The mysqlplus library is a [slightly updated] fork of the Mysql class, with asynchronous capability added
7
+ # See http://www.kitebird.com/articles/ruby-mysql.html for details, as well as the test directory within the gem
8
+ #
9
+ class Mysql
10
+
11
+ def ruby_async_query(sql, timeout = nil) # known to deadlock TODO
12
+ send_query(sql)
13
+ select [ (@sockets ||= {})[socket] ||= IO.new(socket) ], nil, nil, nil
14
+ get_result
15
+ end
16
+
17
+ begin
18
+ alias_method :async_query, :c_async_query
19
+ rescue NameError => e
20
+ raise LoadError.new("error loading mysqlplus--this may mean you ran a require 'mysql' before a require 'mysqplus', which must come first -- possibly also run gem uninstall mysql")
21
+ end
22
+
23
+ end
data/mysqlplus.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "jrmey-mysqlplus"
3
+ s.version = "0.1.3"
4
+ s.date = "2010-07-04"
5
+ s.summary = "Enhanced Ruby MySQL driver with Ruby 1.9 support"
6
+ s.email = "jeremysuriel@gmail.com"
7
+ s.homepage = "http://github.com/jrmey/mysqlplus"
8
+ s.description = "Enhanced Ruby MySQL driver"
9
+ s.has_rdoc = true
10
+ s.authors = ["Muhammad A. Ali", "Jeremy Suriel"]
11
+ s.platform = Gem::Platform::RUBY
12
+ s.files = %w[
13
+ README
14
+ Rakefile
15
+ TODO_LIST
16
+ ext/error_const.h
17
+ ext/extconf.rb
18
+ ext/mysql.c
19
+ lib/mysqlplus.rb
20
+ mysqlplus.gemspec
21
+ ] + Dir.glob('test/*')
22
+ s.rdoc_options = ["--main", "README"]
23
+ s.extra_rdoc_files = ["README"]
24
+ s.extensions << "ext/extconf.rb"
25
+
26
+ if s.respond_to? :specification_version then
27
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
28
+ s.specification_version = 3
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # I suppose if all the tests don't blow up, that probably means pass
2
+ require 'mysqlplus'
3
+ for file in Dir.glob('*_test.rb') do
4
+ puts 'testing ' + file
5
+ # fork so we don't run out of connections to the mysql db, as few tests ever clean up their old processes
6
+ pid = Process.fork { load file }
7
+ Process.wait(pid)
8
+ end
9
+ puts 'successful'
@@ -0,0 +1,37 @@
1
+ require 'create_test_db'
2
+
3
+ use_the_all_hashes_method = true
4
+
5
+ $count = 5
6
+
7
+ $start = Time.now
8
+
9
+ $connections = []
10
+ $count.times do
11
+ $connections << Mysql.real_connect('localhost','root', '', 'local_test_db')
12
+ end
13
+
14
+
15
+ $threads = []
16
+ $count.times do |i|
17
+ $threads << Thread.new do
18
+
19
+ query = "select * from test_table"
20
+ puts "sending query on connection #{i}"
21
+ conn = $connections[i]
22
+ result = conn.async_query(query)
23
+ if use_the_all_hashes_method
24
+ saved = result.all_hashes
25
+ else
26
+ saved = []
27
+ result.each_hash {|h| saved << h }
28
+ end
29
+ result.free
30
+
31
+ end
32
+ end
33
+
34
+ puts 'waiting on threads'
35
+ $threads.each{|t| t.join }
36
+
37
+ puts Time.now - $start
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ m = Mysql.real_connect('localhost','root','','mysql')
4
+
5
+ m.c_async_query( 'SELECT * FROM user' ) do |result|
6
+ puts result.inspect
7
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ ThreadedMysqlTest.new( 10, "Threaded, C, very small overhead" ) do |test|
4
+ test.setup{ Mysql.real_connect('localhost','root') }
5
+ test.per_query_overhead = 0.005
6
+ test.query_with = :c_async_query
7
+ test.run!
8
+ end
9
+
10
+ ThreadedMysqlTest.new( 10, "Threaded, C, small overhead" ) do |test|
11
+ test.setup{ Mysql.real_connect('localhost','root') }
12
+ test.per_query_overhead = 0.1
13
+ test.query_with = :c_async_query
14
+ test.run!
15
+ end
16
+
17
+ ThreadedMysqlTest.new( 10, "Threaded, C, medium overhead" ) do |test|
18
+ test.setup{ Mysql.real_connect('localhost','root') }
19
+ test.per_query_overhead = 1
20
+ test.query_with = :c_async_query
21
+ test.run!
22
+ end
23
+
24
+ ThreadedMysqlTest.new( 10, "Threaded, C, large overhead" ) do |test|
25
+ test.setup{ Mysql.real_connect('localhost','root') }
26
+ test.per_query_overhead = 3
27
+ test.query_with = :c_async_query
28
+ test.run!
29
+ end
30
+
31
+ ThreadedMysqlTest.new( 10, "Threaded, C, random overhead" ) do |test|
32
+ test.setup{ Mysql.real_connect('localhost','root') }
33
+ test.per_query_overhead = :random
34
+ test.query_with = :c_async_query
35
+ test.run!
36
+ end
@@ -0,0 +1,22 @@
1
+ # If this script returns without the word pass
2
+ # you may have compiled mysqlplus using ruby and
3
+ # run it using a different version of ruby
4
+
5
+ if RUBY_VERSION >= "1.9.1"
6
+ require 'mysqlplus'
7
+ require 'socket'
8
+ require 'timeout'
9
+ TCPServer.new '0.0.0.0', 8002
10
+ Thread.new {
11
+ sleep 2
12
+ print "pass"
13
+ system("kill -9 #{Process.pid}")
14
+ }
15
+ Timeout::timeout(1) {
16
+ # uncomment this line to do the 'real' test
17
+ # which hangs otherwise (blows up if code is bad, otherwise hangs)
18
+ Mysql.real_connect '127.0.0.1', 'root', 'pass', 'db', 8002
19
+ }
20
+ raise 'should never get here'
21
+ end
22
+
@@ -0,0 +1,17 @@
1
+ require 'mysqlplus'
2
+ begin
3
+ Mysql.real_connect('fakehost','root', '', 'local_leadgen_dev')
4
+ rescue Mysql::Error
5
+ end
6
+ begin
7
+ Mysql.real_connect('localhost','root', '', 'faketable')
8
+ rescue Mysql::Error
9
+ end
10
+ begin
11
+ Mysql.real_connect('localhost', 'root', 'pass', 'db', 3307)# bad port
12
+ rescue Mysql::Error
13
+ end
14
+
15
+ print "pass"
16
+
17
+
@@ -0,0 +1,22 @@
1
+ # To run first execute:
2
+ =begin
3
+ create database local_test_db;
4
+ use local_test_db;
5
+ CREATE TABLE test_table (
6
+ c1 INT,
7
+ c2 VARCHAR(20)
8
+ );
9
+ =end
10
+ # This script shows the effect of using .all_hashes instead of looping on each hash
11
+ # run it by substiting in a 'long' [many row] query for the query variable and toggling use_all_hashes here at the top
12
+ # note that we load all the rows first, then run .all_hashes on the result [to see more easily the effect of all hashes]
13
+ # on my machine and a 200_000 row table, it took 3.38s versus 3.65s for the old .each_hash way [note also that .each_hash is
14
+ # almost as fast, now, as .all_hashes--they've both been optimized]
15
+ require 'mysqlplus'
16
+
17
+ puts 'initing db'
18
+ # init the DB
19
+ conn = Mysql.real_connect('localhost', 'root', '', 'local_test_db')
20
+ conn.query("delete from test_table")
21
+ 200_000.times {conn.query(" insert into test_table (c1, c2) values (3, 'ABCDEFG')")}
22
+ puts 'connection pool ready'
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ EventedMysqlTest.new( 10, "Evented, very small overhead" ) do |test|
4
+ test.setup{ Mysql.real_connect('localhost','root') }
5
+ test.per_query_overhead = 0.005
6
+ test.run!
7
+ end
8
+
9
+ EventedMysqlTest.new( 10, "Evented, small overhead" ) do |test|
10
+ test.setup{ Mysql.real_connect('localhost','root') }
11
+ test.per_query_overhead = 0.1
12
+ test.run!
13
+ end
14
+
15
+ EventedMysqlTest.new( 10, "Evented, medium overhead" ) do |test|
16
+ test.setup{ Mysql.real_connect('localhost','root') }
17
+ test.per_query_overhead = 1
18
+ test.run!
19
+ end
20
+
21
+ EventedMysqlTest.new( 10, "Evented, large overhead" ) do |test|
22
+ test.setup{ Mysql.real_connect('localhost','root') }
23
+ test.per_query_overhead = 3
24
+ test.run!
25
+ end
26
+
27
+ EventedMysqlTest.new( 10, "Evented, random overhead" ) do |test|
28
+ test.setup{ Mysql.real_connect('localhost','root') }
29
+ test.per_query_overhead = :random
30
+ test.run!
31
+ end
@@ -0,0 +1,40 @@
1
+ require 'mysqlplus'
2
+ require 'benchmark'
3
+
4
+ with_gc = Mysql.real_connect('localhost','root','','mysql')
5
+ without_gc = Mysql.real_connect('localhost','root','','mysql')
6
+ without_gc.disable_gc = true
7
+
8
+ $gc_stats = []
9
+
10
+ def countable_gc?
11
+ GC.respond_to? :count
12
+ end
13
+
14
+ def gc_counts( label, scope )
15
+ $gc_stats << "Objects #{scope} ( #{label} ) #{GC.count}"
16
+ end
17
+
18
+ def with_gc_counts( label )
19
+ gc_counts( label, 'before' ) if countable_gc?
20
+ yield
21
+ gc_counts( label, 'after' ) if countable_gc?
22
+ end
23
+
24
+ n = 1000
25
+
26
+ Benchmark.bmbm do |x|
27
+ x.report( 'With GC' ) do
28
+ with_gc_counts( 'With GC' ) do
29
+ n.times{ with_gc.c_async_query( 'SELECT * FROM user' ) }
30
+ end
31
+ end
32
+ GC.start
33
+ x.report( 'Without GC' ) do
34
+ with_gc_counts( 'Without GC' ) do
35
+ n.times{ without_gc.c_async_query( 'SELECT * FROM user' ) }
36
+ end
37
+ end
38
+ end
39
+
40
+ puts $gc_stats.join( ' | ' )
@@ -0,0 +1,6 @@
1
+ require 'mysqlplus'
2
+ a = Mysql.real_connect('localhost','root')
3
+ 100.times { a.query("select sleep(0)") }
4
+ print "pass"
5
+
6
+
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ ThreadedMysqlTest.new( 10, "Threaded, native Ruby, very small overhead" ) do |test|
4
+ test.setup{ Mysql.real_connect('localhost','root') }
5
+ test.per_query_overhead = 0.005
6
+ test.query_with = :async_query
7
+ test.run!
8
+ end
9
+
10
+ ThreadedMysqlTest.new( 10, "Threaded, native Ruby, small overhead" ) do |test|
11
+ test.setup{ Mysql.real_connect('localhost','root') }
12
+ test.per_query_overhead = 0.1
13
+ test.query_with = :async_query
14
+ test.run!
15
+ end
16
+
17
+ ThreadedMysqlTest.new( 10, "Threaded, native Ruby, medium overhead" ) do |test|
18
+ test.setup{ Mysql.real_connect('localhost','root') }
19
+ test.per_query_overhead = 1
20
+ test.query_with = :async_query
21
+ test.run!
22
+ end
23
+
24
+ ThreadedMysqlTest.new( 10, "Threaded, native Ruby, large overhead" ) do |test|
25
+ test.setup{ Mysql.real_connect('localhost','root') }
26
+ test.per_query_overhead = 3
27
+ test.query_with = :async_query
28
+ test.run!
29
+ end
30
+
31
+ ThreadedMysqlTest.new( 10, "Threaded, native Ruby, random overhead" ) do |test|
32
+ test.setup{ Mysql.real_connect('localhost','root') }
33
+ test.per_query_overhead = :random
34
+ test.query_with = :async_query
35
+ test.run!
36
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ m = Mysql.real_connect('localhost','root')
4
+ m.reconnect = true
5
+ $count = 0
6
+ class << m
7
+ def safe_query( query )
8
+ begin
9
+ send_query( query )
10
+ rescue => e
11
+ $count += 1
12
+ puts e.message
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ m.safe_query( 'select sleep(1)' )
19
+ m.safe_query( 'select sleep(1)' )#raises
20
+ m.simulate_disconnect #fires mysql_library_end
21
+ m.safe_query( 'select sleep(1)' )
22
+ m.safe_query( 'select sleep(1)' )#raises
23
+ m.close
24
+ m.connect('localhost','root')
25
+ m.safe_query( 'select sleep(1)' )
26
+ m.safe_query( 'select sleep(1)' )#raises
27
+ m.simulate_disconnect
28
+ raise unless $count == 3
29
+ m.safe_query( 'BEGIN' )
30
+ m.safe_query( 'select sleep(1)' ) # raises
31
+ m.get_result()
32
+ m.safe_query( 'COMMIT' )
33
+ m.get_result
34
+ raise unless $count == 4
@@ -0,0 +1,47 @@
1
+ # This is an example of using Mysql::ResultSet#use_result [see docs for what that does]
2
+ # this function is useful for those who have large query results and want to be able to parse them
3
+ # as they come in, instead of having to wait for the query to finish before doing parsing
4
+ # for me, running this on a query with 200_000 lines decreases total time to create an array of results
5
+ # from .82s to .62s
6
+ # you can experiment with it by changing the query here to be a long one, and toggling the do_the_use_query_optimization variable
7
+ # this also has the interesting property of 'freeing' Ruby to do thread changes mid-query.
8
+ require 'create_test_db'
9
+
10
+ do_the_use_query_optimization = true
11
+
12
+ $count = 5
13
+
14
+ $start = Time.now
15
+
16
+ $connections = []
17
+ $count.times do
18
+ $connections << Mysql.real_connect('localhost','root', '', 'local_test_db')
19
+ end
20
+
21
+ puts 'connection pool ready'
22
+
23
+ $threads = []
24
+ $count.times do |i|
25
+ $threads << Thread.new do
26
+
27
+ puts "sending query on connection #{i}"
28
+ conn = $connections[i]
29
+ saved = []
30
+ query = "select * from test_table"
31
+ if do_the_use_query_optimization
32
+ conn.query_with_result=false
33
+ result = conn.async_query(query)
34
+ res = result.use_result
35
+ res.each_hash { |h| saved << h }
36
+ res.free
37
+ else
38
+ conn.async_query(query).each_hash {|h| saved << h }
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ puts 'waiting on threads'
45
+ $threads.each{|t| t.join }
46
+
47
+ puts Time.now - $start
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ $m = Mysql.real_connect('localhost','root')
4
+ #$m.reconnect = true
5
+
6
+ def assert_reconnected
7
+ puts $m.reconnected?().inspect
8
+ sleep 1
9
+ yield
10
+ puts $m.reconnected?().inspect
11
+ end
12
+
13
+ assert_reconnected do
14
+ $m.simulate_disconnect
15
+ end
16
+ assert_reconnected do
17
+ $m.close
18
+ end
@@ -0,0 +1,198 @@
1
+ require 'mysqlplus'
2
+
3
+ class MysqlTest
4
+
5
+ class NotImplemented < StandardError
6
+ end
7
+
8
+ attr_accessor :queries,
9
+ :context,
10
+ :connections,
11
+ :connection_signature,
12
+ :start,
13
+ :done,
14
+ :query_with,
15
+ :per_query_overhead,
16
+ :timeout
17
+
18
+ def initialize( queries, context = '' )
19
+ @queries = queries
20
+ @context = context
21
+ @done = []
22
+ @query_with = :async_query
23
+ @per_query_overhead = 3
24
+ @timeout = 20
25
+ yield self if block_given?
26
+ end
27
+
28
+ def setup( &block )
29
+ @start = Time.now
30
+ @connection_signature = block
31
+ end
32
+
33
+ def run!
34
+ c_or_native_ruby_async_query do
35
+ present_context if context?
36
+ prepare
37
+ yield
38
+ end
39
+ end
40
+
41
+ def per_query_overhead=( overhead )
42
+ @per_query_overhead = ( overhead == :random ) ? rand() : overhead
43
+ end
44
+
45
+ protected
46
+
47
+ def prepare
48
+ raise NotImplemented
49
+ end
50
+
51
+ def teardown
52
+ raise NotImplemented
53
+ end
54
+
55
+ def log( message, prefix = '' )
56
+ puts "[#{timestamp}] #{prefix} #{message}"
57
+ end
58
+
59
+ def with_logging( message )
60
+ log( message, 'Start' )
61
+ yield
62
+ log( message, 'End' )
63
+ end
64
+
65
+ def timestamp
66
+ Time.now - @start
67
+ end
68
+
69
+ def context?
70
+ @context != ''
71
+ end
72
+
73
+ def present_context
74
+ log "#############################################"
75
+ log "# #{@context}"
76
+ log "#############################################"
77
+ end
78
+
79
+ def c_or_native_ruby_async_query
80
+ if @query_with == :c_async_query
81
+ log "** using C based async_query"
82
+ else
83
+ log "** using native Ruby async_query"
84
+ end
85
+ yield
86
+ end
87
+
88
+ def dispatch_query( connection, sql, timeout = nil )
89
+ connection.send( @query_with, sql, timeout )
90
+ end
91
+
92
+ end
93
+
94
+ class EventedMysqlTest < MysqlTest
95
+
96
+ attr_accessor :sockets
97
+
98
+ def initialize( queries, context = '' )
99
+ @sockets = []
100
+ @connections = {}
101
+ super( queries, context )
102
+ end
103
+
104
+ def setup( &block )
105
+ super( &block )
106
+ with_logging 'Setup connection pool' do
107
+ @queries.times do
108
+ connection = @connection_signature.call
109
+ @connections[ IO.new(connection.socket) ] = connection
110
+ @sockets = @connections.keys
111
+ end
112
+ end
113
+ end
114
+
115
+ def run!
116
+ super do
117
+ catch :END_EVENT_LOOP do
118
+ loop do
119
+ result = select( @sockets,nil,nil,nil )
120
+ if result
121
+ result.first.each do |conn|
122
+ @connections[conn].get_result.each{|res| log( "Result for socket #{conn.fileno} : #{res}" ) }
123
+ @done << nil
124
+ if done?
125
+ teardown
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ protected
135
+
136
+ def prepare
137
+ @connections.each_value do |conn|
138
+ conn.send_query( "select sleep(#{@per_query_overhead})" )
139
+ end
140
+ end
141
+
142
+ def teardown
143
+ log "done"
144
+ throw :END_EVENT_LOOP
145
+ end
146
+
147
+ def done?
148
+ @done.size == @queries
149
+ end
150
+
151
+ end
152
+
153
+ class ThreadedMysqlTest < MysqlTest
154
+
155
+ attr_accessor :threads
156
+
157
+ def initialize( queries, context = '' )
158
+ @connections = []
159
+ @threads = []
160
+ super( queries, context )
161
+ end
162
+
163
+ def setup( &block )
164
+ super( &block )
165
+ with_logging "Setup connection pool" do
166
+ @queries.times do
167
+ @connections << @connection_signature.call
168
+ end
169
+ end
170
+ end
171
+
172
+ def run!
173
+ super do
174
+ with_logging "waiting on threads" do
175
+ @threads.each{|t| t.join }
176
+ end
177
+ end
178
+ end
179
+
180
+ protected
181
+
182
+ def prepare
183
+ with_logging "prepare" do
184
+ @queries.times do |conn|
185
+ @threads << Thread.new do
186
+
187
+ log "sending query on connection #{conn}"
188
+
189
+ dispatch_query( @connections[conn], "select sleep(#{@per_query_overhead})", @timeout ).each do |result|
190
+ log "connection #{conn} done"
191
+ end
192
+
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ end
@@ -0,0 +1,24 @@
1
+ require 'mysqlplus'
2
+ require 'rubygems'
3
+ require 'sequel'
4
+
5
+ class Mysql
6
+ unless method_defined? :sync_query
7
+ alias :sync_query :query
8
+ alias :query :async_query
9
+ end
10
+ end
11
+
12
+ DB = Sequel.connect('mysql://root@localhost', :max_connections => 20)
13
+
14
+ start = Time.now
15
+
16
+ (0..10).map do
17
+ Thread.new do
18
+
19
+ p DB['select sleep(2)'].all
20
+
21
+ end
22
+ end.map{|t| t.join }
23
+
24
+ p (Time.now - start)