jrmey-mysqlplus 0.1.3

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/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)