tmm1-em-mysql 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README +28 -0
  2. data/lib/em/mysql.rb +459 -0
  3. data/lib/sequel/async.rb +157 -0
  4. data/test.rb +59 -0
  5. metadata +65 -0
data/README ADDED
@@ -0,0 +1,28 @@
1
+ Async MySQL driver for Ruby/EventMachine
2
+ (c) 2008 Aman Gupta (tmm1)
3
+
4
+
5
+ Requires mysqlplus and the EM.attach patch.
6
+
7
+ require 'em/mysql'
8
+
9
+ # alias SQL for simpler syntax
10
+
11
+ SQL = EventedMysql
12
+ def SQL(query, &blk) SQL.select(query, &blk) end
13
+
14
+
15
+ # setup connection details and allow 4 connections to the server
16
+
17
+ SQL.settings.update :host => 'localhost',
18
+ :port => 3306,
19
+ :database => 'test',
20
+ :connections => 4
21
+
22
+
23
+ # use 4 connections to execute queries in parallel
24
+
25
+ SQL('select sleep(0.25)'){ p 'done' }
26
+ SQL('select sleep(0.25)'){ p 'done' }
27
+ SQL('select sleep(0.25)'){ p 'done' }
28
+ SQL('select sleep(0.25)'){ p 'done' }
data/lib/em/mysql.rb ADDED
@@ -0,0 +1,459 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'mysqlplus'
4
+
5
+ class EventedMysql < EM::Connection
6
+ def initialize mysql, opts
7
+ @mysql = mysql
8
+ @fd = mysql.socket
9
+ @opts = opts
10
+ @queue = []
11
+ @pending = []
12
+ @processing = false
13
+ @connected = true
14
+
15
+ log 'mysql connected'
16
+ end
17
+ attr_reader :processing, :connected, :opts
18
+ alias :settings :opts
19
+
20
+ def connection_completed
21
+ @connected = true
22
+ next_query
23
+ end
24
+
25
+ DisconnectErrors = [
26
+ 'query: not connected',
27
+ 'MySQL server has gone away',
28
+ 'Lost connection to MySQL server during query'
29
+ ] unless defined? DisconnectErrors
30
+
31
+ def notify_readable
32
+ log 'readable'
33
+ if item = @queue.shift
34
+ start, response, sql, cblk, eblk = item
35
+ log 'mysql response', Time.now-start, sql
36
+ arg = case response
37
+ when :raw
38
+ @mysql
39
+ when :select
40
+ ret = []
41
+ result = @mysql.get_result
42
+ result.each_hash{|h| ret << h }
43
+ log 'mysql result', ret
44
+ ret
45
+ when :update
46
+ result = @mysql.get_result
47
+ @mysql.affected_rows
48
+ when :insert
49
+ result = @mysql.get_result
50
+ @mysql.insert_id
51
+ else
52
+ result = @mysql.get_result
53
+ log 'got a result??', result if result
54
+ nil
55
+ end
56
+
57
+ @processing = false
58
+ # result.free if result.is_a? Mysql::Result
59
+ next_query
60
+ cblk.call(arg) if cblk
61
+ else
62
+ log 'readable, but nothing queued?! probably an ERROR state'
63
+ return close
64
+ end
65
+ rescue Mysql::Error => e
66
+ log 'mysql error', e.message
67
+ if DisconnectErrors.include? e.message
68
+ @pending << [response, sql, cblk, eblk]
69
+ return close
70
+ elsif cb = (eblk || @opts[:on_error])
71
+ cb.call(e)
72
+ @processing = false
73
+ next_query
74
+ else
75
+ raise e
76
+ end
77
+ # ensure
78
+ # res.free if res.is_a? Mysql::Result
79
+ # @processing = false
80
+ # next_query
81
+ end
82
+
83
+ def unbind
84
+ log 'mysql disconnect', $!
85
+ # cp = EventedMysql.instance_variable_get('@connection_pool') and cp.delete(self)
86
+ @connected = false
87
+
88
+ # XXX wait for the next tick until the current fd is removed completely from the reactor
89
+ #
90
+ # XXX in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
91
+ # XXX without next_tick in these cases, unbind will get fired on the newly attached signature as well
92
+ #
93
+ # XXX do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
94
+ # XXX reconnects to happen after all the unbinds have been processed
95
+ EM.add_timer(0) do
96
+ log 'mysql reconnecting'
97
+ @processing = false
98
+ @mysql = EventedMysql._connect @opts
99
+ @fd = @mysql.socket
100
+
101
+ @signature = EM.attach_fd @mysql.socket, true, false
102
+ log 'mysql connected'
103
+ EM.instance_variable_get('@conns')[@signature] = self
104
+ end
105
+ end
106
+
107
+ def execute sql, response = nil, cblk = nil, eblk = nil, &blk
108
+ cblk ||= blk
109
+
110
+ begin
111
+ unless @processing or !@connected
112
+ # begin
113
+ # log 'mysql ping', @mysql.ping
114
+ # # log 'mysql stat', @mysql.stat
115
+ # # log 'mysql errno', @mysql.errno
116
+ # rescue
117
+ # log 'mysql ping failed'
118
+ # @pending << [response, sql, blk]
119
+ # return close
120
+ # end
121
+
122
+ @processing = true
123
+
124
+ log 'mysql sending', sql
125
+ @mysql.send_query(sql)
126
+ else
127
+ @pending << [response, sql, cblk, eblk]
128
+ return
129
+ end
130
+ rescue Mysql::Error => e
131
+ log 'mysql error', e.message
132
+ if DisconnectErrors.include? e.message
133
+ @pending << [response, sql, cblk, eblk]
134
+ return close
135
+ else
136
+ raise e
137
+ end
138
+ end
139
+
140
+ log 'queuing', response, sql
141
+ @queue << [Time.now, response, sql, cblk, eblk]
142
+ end
143
+
144
+ def close
145
+ @connected = false
146
+ # @mysql.close
147
+ # IO.pipe
148
+ # EM.add_timer(0){ close_connection }
149
+ # close_connection
150
+ fd = detach
151
+ log 'detached fd', fd
152
+ end
153
+
154
+ private
155
+
156
+ def next_query
157
+ if @connected and !@processing and pending = @pending.shift
158
+ response, sql, cblk, eblk = pending
159
+ execute(sql, response, cblk, eblk)
160
+ end
161
+ end
162
+
163
+ def log *args
164
+ return unless @opts[:logging]
165
+ p [Time.now, @fd, (@signature[-4..-1] if @signature), *args]
166
+ end
167
+
168
+ public
169
+
170
+ def self.connect opts
171
+ unless EM.respond_to?(:attach) and Mysql.method_defined?(:socket)
172
+ raise RuntimeError, 'mysqlplus and EM.attach are required for EventedMysql'
173
+ end
174
+
175
+ if conn = _connect(opts)
176
+ EM.attach conn.socket, self, conn, opts
177
+ else
178
+ EM.add_timer(5){ connect opts }
179
+ end
180
+ end
181
+
182
+ self::Mysql = ::Mysql unless defined? self::Mysql
183
+
184
+ # stolen from sequel
185
+ def self._connect opts
186
+ opts = settings.merge(opts)
187
+
188
+ conn = Mysql.init
189
+
190
+ # set encoding _before_ connecting
191
+ if charset = opts[:charset] || opts[:encoding]
192
+ conn.options(Mysql::SET_CHARSET_NAME, charset)
193
+ end
194
+
195
+ conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
196
+
197
+ conn.real_connect(
198
+ opts[:host] || 'localhost',
199
+ opts[:user] || 'root',
200
+ opts[:password],
201
+ opts[:database],
202
+ opts[:port],
203
+ opts[:socket],
204
+ 0 +
205
+ # XXX multi results require multiple callbacks to parse
206
+ # Mysql::CLIENT_MULTI_RESULTS +
207
+ # Mysql::CLIENT_MULTI_STATEMENTS +
208
+
209
+ # XXX this should check for opts[:compression]
210
+ Mysql::CLIENT_COMPRESS
211
+ )
212
+
213
+ # increase timeout so mysql server doesn't disconnect us
214
+ # this is especially bad if we're disconnected while EM.attach is
215
+ # still in progress, because by the time it gets to EM, the FD is
216
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
217
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
218
+ conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
219
+
220
+ # we handle reconnecting (and reattaching the new fd to EM)
221
+ conn.reconnect = false
222
+
223
+ # By default, MySQL 'where id is null' selects the last inserted id
224
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
225
+ conn.query("set SQL_AUTO_IS_NULL=0")
226
+
227
+ # get results for queries
228
+ conn.query_with_result = true
229
+
230
+ # if encoding = opts[:encoding] || opts[:charset]
231
+ # conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
232
+ # conn.query("set names '#{encoding}'")
233
+ # conn.query("set character_set_connection = '#{encoding}'")
234
+ # conn.query("set character_set_client = '#{encoding}'")
235
+ # conn.query("set character_set_database = '#{encoding}'")
236
+ # conn.query("set character_set_server = '#{encoding}'")
237
+ # conn.query("set character_set_results = '#{encoding}'")
238
+ # end
239
+
240
+ conn
241
+ rescue Mysql::Error => e
242
+ if cb = opts[:on_error]
243
+ cb.call(e)
244
+ nil
245
+ else
246
+ raise e
247
+ end
248
+ end
249
+ end
250
+
251
+ class EventedMysql
252
+ def self.settings
253
+ @settings ||= { :connections => 4, :logging => false }
254
+ end
255
+
256
+ def self.execute query, type = nil, cblk = nil, eblk = nil, &blk
257
+ unless nil#connection = connection_pool.find{|c| not c.processing and c.connected }
258
+ @n ||= 0
259
+ connection = connection_pool[@n]
260
+ @n = 0 if (@n+=1) >= connection_pool.size
261
+ end
262
+
263
+ connection.execute(query, type, cblk, eblk, &blk)
264
+ end
265
+
266
+ %w[ select insert update raw ].each do |type| class_eval %[
267
+
268
+ def self.#{type} query, cblk = nil, eblk = nil, &blk
269
+ execute query, :#{type}, cblk, eblk, &blk
270
+ end
271
+
272
+ ] end
273
+
274
+ def self.all query, type = nil, &blk
275
+ responses = 0
276
+ connection_pool.each do |c|
277
+ c.execute(query, type) do
278
+ responses += 1
279
+ blk.call if blk and responses == @connection_pool.size
280
+ end
281
+ end
282
+ end
283
+
284
+ def self.connection_pool
285
+ @connection_pool ||= (1..settings[:connections]).map{ EventedMysql.connect(settings) }
286
+ # p ['connpool', settings[:connections], @connection_pool.size]
287
+ # (1..(settings[:connections]-@connection_pool.size)).each do
288
+ # @connection_pool << EventedMysql.connect(settings)
289
+ # end unless settings[:connections] == @connection_pool.size
290
+ # @connection_pool
291
+ end
292
+ end
293
+
294
+ if __FILE__ == $0 and require 'em/spec'
295
+
296
+ EM.describe EventedMysql, 'individual connections' do
297
+
298
+ should 'create a new connection' do
299
+ @mysql = EventedMysql.connect :host => '127.0.0.1',
300
+ :port => 3306,
301
+ :database => 'test',
302
+ :logging => false
303
+
304
+ @mysql.class.should == EventedMysql
305
+ done
306
+ end
307
+
308
+ should 'execute sql' do
309
+ start = Time.now
310
+
311
+ @mysql.execute('select sleep(0.2)'){
312
+ (Time.now-start).should.be.close 0.2, 0.1
313
+ done
314
+ }
315
+ end
316
+
317
+ should 'reconnect when disconnected' do
318
+ @mysql.close
319
+ @mysql.execute('select 1+2'){
320
+ :connected.should == :connected
321
+ done
322
+ }
323
+ end
324
+
325
+ # to test, run:
326
+ # mysqladmin5 -u root kill `mysqladmin5 -u root processlist | grep "select sleep(5)+1" | cut -d'|' -f2`
327
+ #
328
+ # should 're-run query if disconnected during query' do
329
+ # @mysql.execute('select sleep(5)+1', :select){ |res|
330
+ # res.first['sleep(5)+1'].should == '1'
331
+ # done
332
+ # }
333
+ # end
334
+
335
+ should 'run select queries and return results' do
336
+ @mysql.execute('select 1+2', :select){ |res|
337
+ res.size.should == 1
338
+ res.first['1+2'].should == '3'
339
+ done
340
+ }
341
+ end
342
+
343
+ should 'queue up queries and execute them in order' do
344
+ @mysql.execute('select 1+2', :select)
345
+ @mysql.execute('select 2+3', :select)
346
+ @mysql.execute('select 3+4', :select){ |res|
347
+ res.first['3+4'].should == '7'
348
+ done
349
+ }
350
+ end
351
+
352
+ should 'continue processing queries after hitting an error' do
353
+ @mysql.settings.update :on_error => proc{|e|}
354
+
355
+ @mysql.execute('select 1+ from table'){}
356
+ @mysql.execute('select 1+1 as num', :select){ |res|
357
+ res[0]['num'].should == '2'
358
+ done
359
+ }
360
+ end
361
+
362
+ should 'have raw mode which yields the mysql object' do
363
+ @mysql.execute('select 1+2 as num', :raw){ |mysql|
364
+ mysql.should.is_a? Mysql
365
+ mysql.get_result.all_hashes.should == [{'num' => '3'}]
366
+ done
367
+ }
368
+ end
369
+
370
+ should 'allow custom error callbacks for each query' do
371
+ @mysql.settings.update :on_error => proc{ should.flunk('default errback invoked') }
372
+
373
+ @mysql.execute('select 1+ from table', :select, proc{
374
+ should.flunk('callback invoked')
375
+ }, proc{ |e|
376
+ done
377
+ })
378
+ end
379
+
380
+ end
381
+
382
+ EM.describe EventedMysql, 'connection pools' do
383
+
384
+ EventedMysql.settings.update :connections => 3
385
+
386
+ should 'run queries in parallel' do
387
+ n = 0
388
+ EventedMysql.select('select sleep(0.25)'){ n+=1 }
389
+ EventedMysql.select('select sleep(0.25)'){ n+=1 }
390
+ EventedMysql.select('select sleep(0.25)'){ n+=1 }
391
+
392
+ EM.add_timer(0.30){
393
+ n.should == 3
394
+ done
395
+ }
396
+ end
397
+
398
+ end
399
+
400
+ SQL = EventedMysql
401
+ def SQL(query, &blk) SQL.select(query, &blk) end
402
+
403
+ # XXX this should get cleaned up automatically after reactor stops
404
+ SQL.instance_variable_set('@connection_pool', nil)
405
+
406
+
407
+ EM.describe SQL, 'sql api' do
408
+
409
+ should 'run a query on all connections' do
410
+ SQL.all('use test'){
411
+ :done.should == :done
412
+ done
413
+ }
414
+ end
415
+
416
+ should 'execute queries with no results' do
417
+ SQL.execute('drop table if exists evented_mysql_test'){
418
+ :table_dropped.should == :table_dropped
419
+ SQL.execute('create table evented_mysql_test (id int primary key auto_increment, num int not null)'){
420
+ :table_created.should == :table_created
421
+ done
422
+ }
423
+ }
424
+ end
425
+
426
+ should 'insert rows and return inserted id' do
427
+ SQL.insert('insert into evented_mysql_test (num) values (10),(11),(12)'){ |id|
428
+ id.should == 1
429
+ done
430
+ }
431
+ end
432
+
433
+ should 'select rows from the database' do
434
+ SQL.select('select * from evented_mysql_test'){ |res|
435
+ res.size.should == 3
436
+ res.first.should == { 'id' => '1', 'num' => '10' }
437
+ res.last.should == { 'id' => '3', 'num' => '12' }
438
+ done
439
+ }
440
+ end
441
+
442
+ should 'update rows and return affected rows' do
443
+ SQL.update('update evented_mysql_test set num = num + 10'){ |changed|
444
+ changed.should == 3
445
+ done
446
+ }
447
+ end
448
+
449
+ should 'fire error callback with exceptions' do
450
+ SQL.settings.update :on_error => proc{ |e|
451
+ e.class.should == Mysql::Error
452
+ done
453
+ }
454
+ SQL.select('select 1+ from table'){}
455
+ end
456
+
457
+ end
458
+
459
+ end
@@ -0,0 +1,157 @@
1
+ # async sequel extensions, for use with em-mysql
2
+ #
3
+ # require 'em/mysql'
4
+ # DB = Sequel.connect(...)
5
+ # ADB = EventedMysql.connect(..., :on_error => proc{|e| log 'error', e })
6
+ #
7
+ # def log *args
8
+ # p [Time.now, *args]
9
+ # end
10
+ #
11
+ # DB[:table].where(:id < 100).async_update do |num_updated|
12
+ # log "done updating #{num_updated} rows"
13
+ # end
14
+ #
15
+ # DB[:table].async_insert(:field => 'value') do |insert_id|
16
+ # log "inserted row #{insert_id}"
17
+ # end
18
+ #
19
+ # DB[:table].async_multi_insert([:field], [ ['one'], ['two'], ['three'] ]) do
20
+ # log "done inserting 3 rows"
21
+ # end
22
+ #
23
+ # DB[:table].limit(10).async_each do |row|
24
+ # log "got a row", row
25
+ # end; log "this will be printed before the query returns"
26
+ #
27
+ # DB[:table].async_all do |rows|
28
+ # DB[:table].async_multi_insert([:field], rows.map{|r| "new_#{r[:field]}" })
29
+ # end
30
+ #
31
+ # DB[:table].async_all do |rows|
32
+ # num = rows.size
33
+ #
34
+ # rows.each{ |r|
35
+ # DB[:table].where(:id => r[:id]).async_update(:field => rand(10000).to_s) do
36
+ # num = num-1
37
+ # if num == 0
38
+ # log "last update completed"
39
+ # end
40
+ # end
41
+ # }
42
+ # end
43
+ #
44
+ # DB[:table].async_count do |num_rows|
45
+ # log "table has #{num_rows} rows"
46
+ # end
47
+
48
+ module Sequel
49
+ class Dataset
50
+ def async_insert *args, &cb
51
+ ADB.insert insert_sql(*args), &cb
52
+ nil
53
+ end
54
+
55
+ def async_update *args, &cb
56
+ ADB.update update_sql(*args), &cb
57
+ nil
58
+ end
59
+
60
+ def async_delete &cb
61
+ ADB.execute delete_sql, &cb
62
+ nil
63
+ end
64
+
65
+ def async_multi_insert *args, &cb
66
+ ADB.execute multi_insert_sql(*args).first, &cb
67
+ nil
68
+ end
69
+
70
+ def async_multi_insert_ignore *args, &cb
71
+ ADB.execute multi_insert_sql(*args).first.sub(/insert/i, "INSERT IGNORE"), &cb
72
+ nil
73
+ end
74
+
75
+ def async_each *args
76
+ ADB.select(select_sql(*args)) do |rows|
77
+ rows.each{|r|
78
+ r = transform_load(r) if @transform
79
+ r = row_proc[r] if row_proc
80
+ yield r
81
+ }
82
+ end
83
+ nil
84
+ end
85
+
86
+ def async_all
87
+ ADB.select(sql) do |rows|
88
+ if row_proc or transform
89
+ yield(rows.map{|r|
90
+ r = transform_load(r) if @transform
91
+ r = row_proc[r] if row_proc
92
+ r
93
+ })
94
+ else
95
+ yield(rows)
96
+ end
97
+ end
98
+ nil
99
+ end
100
+
101
+ def async_count &cb
102
+ if options_overlap(COUNT_FROM_SELF_OPTS)
103
+ from_self.async_count(&cb)
104
+ else
105
+ naked.async_each(STOCK_COUNT_OPTS){|r|
106
+ yield r.values.first.to_i
107
+ }
108
+ end
109
+ nil
110
+ end
111
+ end
112
+
113
+ class Model
114
+ def async_update *args, &cb
115
+ this.async_update(*args, &cb)
116
+ set(*args)
117
+ self
118
+ end
119
+
120
+ def async_delete &cb
121
+ this.async_delete(&cb)
122
+ nil
123
+ end
124
+
125
+ class << self
126
+ [ :async_insert,
127
+ :async_multi_insert,
128
+ :async_multi_insert_ignore,
129
+ :async_each,
130
+ :async_all,
131
+ :async_update,
132
+ :async_count ].each do |method|
133
+ class_eval %[
134
+ def #{method} *args, &cb
135
+ dataset.#{method}(*args, &cb)
136
+ end
137
+ ]
138
+ end
139
+
140
+ # async version of Model#[]
141
+ def async_lookup args
142
+ unless Hash === args
143
+ args = primary_key_hash(args)
144
+ end
145
+
146
+ dataset.where(args).limit(1).async_all{ |rows|
147
+ if rows.any?
148
+ yield rows.first
149
+ else
150
+ yield nil
151
+ end
152
+ }
153
+ nil
154
+ end
155
+ end
156
+ end
157
+ end
data/test.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'lib/em/mysql'
2
+
3
+ # EM.kqueue
4
+ # EM.epoll
5
+ EM.run{
6
+ EM.start_server '127.0.0.1', 12345 do |c|
7
+ def c.receive_data data
8
+ p 'sending http response'
9
+ send_data "hello"
10
+ close_connection_after_writing
11
+ end
12
+ end
13
+
14
+ SQL = EventedMysql
15
+ def SQL(query, &blk) SQL.select(query, &blk) end
16
+
17
+ if true
18
+
19
+ SQL.settings.update :logging => true,
20
+ :database => 'test',
21
+ :connections => 1
22
+
23
+ SQL.execute('select 1+2')
24
+
25
+ EM.add_timer(1){
26
+ 3.times do SQL.select('select sleep(0.5)+1'){|r| p(r) } end
27
+ }
28
+
29
+ elsif false
30
+
31
+ SQL.settings.update :logging => true,
32
+ :database => 'test',
33
+ :connections => 10
34
+
35
+ EM.add_timer(2.5){ SQL.all('use test') }
36
+
37
+ else
38
+
39
+ SQL.settings.update :logging => true,
40
+ :database => 'test',
41
+ :connections => 10,
42
+ :timeout => 1
43
+
44
+ n = 0
45
+
46
+ SQL.execute('drop table if exists testingabc'){
47
+ SQL.execute('create table testingabc (a int, b int, c int)'){
48
+ EM.add_periodic_timer(0.2) do
49
+ cur_num = n+=1
50
+ SQL.execute("insert into testingabc values (1,2,#{cur_num})"){
51
+ SQL("select * from testingabc where c = #{cur_num} limit 1"){ |res| puts;puts }
52
+ }
53
+ end
54
+ }
55
+ }
56
+
57
+ end
58
+
59
+ }
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tmm1-em-mysql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Aman Gupta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-18 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.4
24
+ version:
25
+ description: Async MySQL client API for Ruby/EventMachine
26
+ email: em-mysql@tmm1.net
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - lib/em/mysql.rb
36
+ - lib/sequel/async.rb
37
+ - test.rb
38
+ has_rdoc: false
39
+ homepage: http://github.com/tmm1/em-mysql
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.2.0
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: Async MySQL client API for Ruby/EventMachine
64
+ test_files: []
65
+