tmm1-em-mysql 0.2.0

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