em-mysql 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +40 -0
- data/em-mysql.gemspec +21 -0
- data/lib/em/mysql.rb +473 -0
- data/lib/sequel/async.rb +184 -0
- data/test.rb +59 -0
- metadata +68 -0
data/README
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Async MySQL driver for Ruby/EventMachine
|
2
|
+
(c) 2008 Aman Gupta (tmm1)
|
3
|
+
|
4
|
+
|
5
|
+
Requires mysqlplus.
|
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' }
|
29
|
+
|
30
|
+
Also includes a sequel async wrapper
|
31
|
+
|
32
|
+
require 'sequel'
|
33
|
+
require 'sequel/async'
|
34
|
+
|
35
|
+
DB = Sequel.connect(:adapter => 'mysql', :user => 'root', :database => 'test', ...)
|
36
|
+
EventedMysql.settings.update(..., :on_error => proc{|e| log 'error', e })
|
37
|
+
|
38
|
+
DB[:table].where(:field => 'value').async_update(:field => 'new value')
|
39
|
+
|
40
|
+
For more info, see the comments in lib/sequel/async.rb
|
data/em-mysql.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = 'em-mysql'
|
3
|
+
s.version = '0.4.0'
|
4
|
+
s.date = '2009-06-23'
|
5
|
+
s.summary = 'Async MySQL client API for Ruby/EventMachine'
|
6
|
+
s.email = "em-mysql@tmm1.net"
|
7
|
+
s.homepage = "http://github.com/tmm1/em-mysql"
|
8
|
+
s.description = 'Async MySQL client API for Ruby/EventMachine'
|
9
|
+
s.has_rdoc = false
|
10
|
+
s.authors = ["Aman Gupta"]
|
11
|
+
s.add_dependency('eventmachine', '>= 0.12.9')
|
12
|
+
|
13
|
+
# git ls-files
|
14
|
+
s.files = %w[
|
15
|
+
README
|
16
|
+
em-mysql.gemspec
|
17
|
+
lib/em/mysql.rb
|
18
|
+
lib/sequel/async.rb
|
19
|
+
test.rb
|
20
|
+
]
|
21
|
+
end
|
data/lib/em/mysql.rb
ADDED
@@ -0,0 +1,473 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'mysqlplus'
|
4
|
+
require 'fcntl'
|
5
|
+
|
6
|
+
class Mysql
|
7
|
+
def result
|
8
|
+
@cur_result
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class EventedMysql < EM::Connection
|
13
|
+
def initialize mysql, opts
|
14
|
+
@mysql = mysql
|
15
|
+
@fd = mysql.socket
|
16
|
+
@opts = opts
|
17
|
+
@current = nil
|
18
|
+
@@queue ||= []
|
19
|
+
@processing = false
|
20
|
+
@connected = true
|
21
|
+
|
22
|
+
log 'mysql connected'
|
23
|
+
|
24
|
+
self.notify_readable = true
|
25
|
+
EM.add_timer(0){ next_query }
|
26
|
+
end
|
27
|
+
attr_reader :processing, :connected, :opts
|
28
|
+
alias :settings :opts
|
29
|
+
|
30
|
+
DisconnectErrors = [
|
31
|
+
'query: not connected',
|
32
|
+
'MySQL server has gone away',
|
33
|
+
'Lost connection to MySQL server during query'
|
34
|
+
] unless defined? DisconnectErrors
|
35
|
+
|
36
|
+
def notify_readable
|
37
|
+
log 'readable'
|
38
|
+
if item = @current
|
39
|
+
@current = nil
|
40
|
+
start, response, sql, cblk, eblk = item
|
41
|
+
log 'mysql response', Time.now-start, sql
|
42
|
+
arg = case response
|
43
|
+
when :raw
|
44
|
+
result = @mysql.get_result
|
45
|
+
@mysql.instance_variable_set('@cur_result', result)
|
46
|
+
@mysql
|
47
|
+
when :select
|
48
|
+
ret = []
|
49
|
+
result = @mysql.get_result
|
50
|
+
result.each_hash{|h| ret << h }
|
51
|
+
log 'mysql result', ret
|
52
|
+
ret
|
53
|
+
when :update
|
54
|
+
result = @mysql.get_result
|
55
|
+
@mysql.affected_rows
|
56
|
+
when :insert
|
57
|
+
result = @mysql.get_result
|
58
|
+
@mysql.insert_id
|
59
|
+
else
|
60
|
+
result = @mysql.get_result
|
61
|
+
log 'got a result??', result if result
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
@processing = false
|
66
|
+
# result.free if result.is_a? Mysql::Result
|
67
|
+
next_query
|
68
|
+
cblk.call(arg) if cblk
|
69
|
+
else
|
70
|
+
log 'readable, but nothing queued?! probably an ERROR state'
|
71
|
+
return close
|
72
|
+
end
|
73
|
+
rescue Mysql::Error => e
|
74
|
+
log 'mysql error', e.message
|
75
|
+
if e.message =~ /Deadlock/
|
76
|
+
@@queue << [response, sql, cblk, eblk]
|
77
|
+
@processing = false
|
78
|
+
next_query
|
79
|
+
elsif DisconnectErrors.include? e.message
|
80
|
+
@@queue << [response, sql, cblk, eblk]
|
81
|
+
return close
|
82
|
+
elsif cb = (eblk || @opts[:on_error])
|
83
|
+
cb.call(e)
|
84
|
+
@processing = false
|
85
|
+
next_query
|
86
|
+
else
|
87
|
+
raise e
|
88
|
+
end
|
89
|
+
# ensure
|
90
|
+
# res.free if res.is_a? Mysql::Result
|
91
|
+
# @processing = false
|
92
|
+
# next_query
|
93
|
+
end
|
94
|
+
|
95
|
+
def unbind
|
96
|
+
log 'mysql disconnect', $!
|
97
|
+
# cp = EventedMysql.instance_variable_get('@connection_pool') and cp.delete(self)
|
98
|
+
@connected = false
|
99
|
+
|
100
|
+
# XXX wait for the next tick until the current fd is removed completely from the reactor
|
101
|
+
#
|
102
|
+
# XXX in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
|
103
|
+
# XXX without next_tick in these cases, unbind will get fired on the newly attached signature as well
|
104
|
+
#
|
105
|
+
# XXX do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
|
106
|
+
# XXX reconnects to happen after all the unbinds have been processed
|
107
|
+
EM.add_timer(0) do
|
108
|
+
log 'mysql reconnecting'
|
109
|
+
@processing = false
|
110
|
+
@mysql = EventedMysql._connect @opts
|
111
|
+
@fd = @mysql.socket
|
112
|
+
|
113
|
+
@signature = EM.attach_fd @mysql.socket, true
|
114
|
+
EM.set_notify_readable @signature, true
|
115
|
+
log 'mysql connected'
|
116
|
+
EM.instance_variable_get('@conns')[@signature] = self
|
117
|
+
@connected = true
|
118
|
+
make_socket_blocking
|
119
|
+
next_query
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def execute sql, response = nil, cblk = nil, eblk = nil, &blk
|
124
|
+
cblk ||= blk
|
125
|
+
|
126
|
+
begin
|
127
|
+
unless @processing or !@connected
|
128
|
+
# begin
|
129
|
+
# log 'mysql ping', @mysql.ping
|
130
|
+
# # log 'mysql stat', @mysql.stat
|
131
|
+
# # log 'mysql errno', @mysql.errno
|
132
|
+
# rescue
|
133
|
+
# log 'mysql ping failed'
|
134
|
+
# @@queue << [response, sql, blk]
|
135
|
+
# return close
|
136
|
+
# end
|
137
|
+
|
138
|
+
@processing = true
|
139
|
+
|
140
|
+
log 'mysql sending', sql
|
141
|
+
@mysql.send_query(sql)
|
142
|
+
else
|
143
|
+
@@queue << [response, sql, cblk, eblk]
|
144
|
+
return
|
145
|
+
end
|
146
|
+
rescue Mysql::Error => e
|
147
|
+
log 'mysql error', e.message
|
148
|
+
if DisconnectErrors.include? e.message
|
149
|
+
@@queue << [response, sql, cblk, eblk]
|
150
|
+
return close
|
151
|
+
else
|
152
|
+
raise e
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
log 'queuing', response, sql
|
157
|
+
@current = [Time.now, response, sql, cblk, eblk]
|
158
|
+
end
|
159
|
+
|
160
|
+
def close
|
161
|
+
@connected = false
|
162
|
+
fd = detach
|
163
|
+
log 'detached fd', fd
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def next_query
|
169
|
+
if @connected and !@processing and pending = @@queue.shift
|
170
|
+
response, sql, cblk, eblk = pending
|
171
|
+
execute(sql, response, cblk, eblk)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def log *args
|
176
|
+
return unless @opts[:logging]
|
177
|
+
p [Time.now, @fd, (@signature[-4..-1] if @signature), *args]
|
178
|
+
end
|
179
|
+
|
180
|
+
public
|
181
|
+
|
182
|
+
def self.connect opts
|
183
|
+
unless EM.respond_to?(:watch) and Mysql.method_defined?(:socket)
|
184
|
+
raise RuntimeError, 'mysqlplus and EM.watch are required for EventedMysql'
|
185
|
+
end
|
186
|
+
|
187
|
+
if conn = _connect(opts)
|
188
|
+
EM.watch conn.socket, self, conn, opts
|
189
|
+
else
|
190
|
+
EM.add_timer(5){ connect opts }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
self::Mysql = ::Mysql unless defined? self::Mysql
|
195
|
+
|
196
|
+
# stolen from sequel
|
197
|
+
def self._connect opts
|
198
|
+
opts = settings.merge(opts)
|
199
|
+
|
200
|
+
conn = Mysql.init
|
201
|
+
|
202
|
+
# set encoding _before_ connecting
|
203
|
+
if charset = opts[:charset] || opts[:encoding]
|
204
|
+
conn.options(Mysql::SET_CHARSET_NAME, charset)
|
205
|
+
end
|
206
|
+
|
207
|
+
conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
|
208
|
+
|
209
|
+
conn.real_connect(
|
210
|
+
opts[:host] || 'localhost',
|
211
|
+
opts[:user] || 'root',
|
212
|
+
opts[:password],
|
213
|
+
opts[:database],
|
214
|
+
opts[:port],
|
215
|
+
opts[:socket],
|
216
|
+
0 +
|
217
|
+
# XXX multi results require multiple callbacks to parse
|
218
|
+
# Mysql::CLIENT_MULTI_RESULTS +
|
219
|
+
# Mysql::CLIENT_MULTI_STATEMENTS +
|
220
|
+
(opts[:compress] == false ? 0 : Mysql::CLIENT_COMPRESS)
|
221
|
+
)
|
222
|
+
|
223
|
+
# increase timeout so mysql server doesn't disconnect us
|
224
|
+
# this is especially bad if we're disconnected while EM.attach is
|
225
|
+
# still in progress, because by the time it gets to EM, the FD is
|
226
|
+
# no longer valid, and it throws a c++ 'bad file descriptor' error
|
227
|
+
# (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
|
228
|
+
conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
|
229
|
+
|
230
|
+
# we handle reconnecting (and reattaching the new fd to EM)
|
231
|
+
conn.reconnect = false
|
232
|
+
|
233
|
+
# By default, MySQL 'where id is null' selects the last inserted id
|
234
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
235
|
+
conn.query("set SQL_AUTO_IS_NULL=0")
|
236
|
+
|
237
|
+
# get results for queries
|
238
|
+
conn.query_with_result = true
|
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.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 'allow access to insert_id in raw mode' do
|
450
|
+
SQL.raw('insert into evented_mysql_test (num) values (20), (21), (22)'){ |mysql|
|
451
|
+
mysql.insert_id.should == 4
|
452
|
+
done
|
453
|
+
}
|
454
|
+
end
|
455
|
+
|
456
|
+
should 'allow access to affected_rows in raw mode' do
|
457
|
+
SQL.raw('update evented_mysql_test set num = num + 10'){ |mysql|
|
458
|
+
mysql.affected_rows.should == 6
|
459
|
+
done
|
460
|
+
}
|
461
|
+
end
|
462
|
+
|
463
|
+
should 'fire error callback with exceptions' do
|
464
|
+
SQL.settings.update :on_error => proc{ |e|
|
465
|
+
e.class.should == Mysql::Error
|
466
|
+
done
|
467
|
+
}
|
468
|
+
SQL.select('select 1+ from table'){}
|
469
|
+
end
|
470
|
+
|
471
|
+
end
|
472
|
+
|
473
|
+
end
|
data/lib/sequel/async.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
# async sequel extensions, for use with em-mysql
|
2
|
+
#
|
3
|
+
# require 'em/mysql'
|
4
|
+
# DB = Sequel.connect(:adapter => 'mysql', :user => 'root', :database => 'test', ...)
|
5
|
+
# EventedMysql.settings.update(..., :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(:field => 'new value') 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
|
+
require 'sequel'
|
49
|
+
raise 'need Sequel >= 3.2.0' unless Sequel::MAJOR == 3 and Sequel::MINOR >= 2
|
50
|
+
|
51
|
+
module Sequel
|
52
|
+
class Dataset
|
53
|
+
def async_insert *args, &cb
|
54
|
+
EventedMysql.insert insert_sql(*args), &cb
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def async_insert_ignore *args, &cb
|
59
|
+
EventedMysql.insert insert_ignore.insert_sql(*args), &cb
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def async_update *args, &cb
|
64
|
+
EventedMysql.update update_sql(*args), &cb
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def async_delete &cb
|
69
|
+
EventedMysql.execute delete_sql, &cb
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def async_multi_insert *args, &cb
|
74
|
+
EventedMysql.execute multi_insert_sql(*args).first, &cb
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def async_multi_insert_ignore *args, &cb
|
79
|
+
EventedMysql.execute insert_ignore.multi_insert_sql(*args).first, &cb
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def async_fetch_rows sql, iter = :each
|
84
|
+
EventedMysql.raw(sql) do |m|
|
85
|
+
r = m.result
|
86
|
+
|
87
|
+
i = -1
|
88
|
+
cols = r.fetch_fields.map{|f| [output_identifier(f.name), Sequel::MySQL::MYSQL_TYPES[f.type], i+=1]}
|
89
|
+
@columns = cols.map{|c| c.first}
|
90
|
+
rows = []
|
91
|
+
while row = r.fetch_row
|
92
|
+
h = {}
|
93
|
+
cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v}
|
94
|
+
if iter == :each
|
95
|
+
yield h
|
96
|
+
else
|
97
|
+
rows << h
|
98
|
+
end
|
99
|
+
end
|
100
|
+
yield rows if iter == :all
|
101
|
+
end
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def async_each
|
106
|
+
async_fetch_rows(select_sql, :each) do |r|
|
107
|
+
if row_proc = @row_proc
|
108
|
+
yield row_proc.call(r)
|
109
|
+
else
|
110
|
+
yield r
|
111
|
+
end
|
112
|
+
end
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
def async_all
|
117
|
+
async_fetch_rows(sql, :all) do |rows|
|
118
|
+
if row_proc = @row_proc
|
119
|
+
yield(rows.map{|r| row_proc.call(r) })
|
120
|
+
else
|
121
|
+
yield(rows)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def async_count &cb
|
128
|
+
if options_overlap(COUNT_FROM_SELF_OPTS)
|
129
|
+
from_self.async_count(&cb)
|
130
|
+
else
|
131
|
+
clone(STOCK_COUNT_OPTS).async_each{|r|
|
132
|
+
yield r.is_a?(Hash) ? r.values.first.to_i : r.values.values.first.to_i
|
133
|
+
}
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class Model
|
140
|
+
def async_update *args, &cb
|
141
|
+
this.async_update(*args, &cb)
|
142
|
+
set(*args)
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
def async_delete &cb
|
147
|
+
this.async_delete(&cb)
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
151
|
+
class << self
|
152
|
+
[ :async_insert,
|
153
|
+
:async_insert_ignore,
|
154
|
+
:async_multi_insert,
|
155
|
+
:async_multi_insert_ignore,
|
156
|
+
:async_each,
|
157
|
+
:async_all,
|
158
|
+
:async_update,
|
159
|
+
:async_count ].each do |method|
|
160
|
+
class_eval %[
|
161
|
+
def #{method} *args, &cb
|
162
|
+
dataset.#{method}(*args, &cb)
|
163
|
+
end
|
164
|
+
]
|
165
|
+
end
|
166
|
+
|
167
|
+
# async version of Model#[]
|
168
|
+
def async_lookup args
|
169
|
+
unless Hash === args
|
170
|
+
args = primary_key_hash(args)
|
171
|
+
end
|
172
|
+
|
173
|
+
dataset.where(args).limit(1).async_all{ |rows|
|
174
|
+
if rows.any?
|
175
|
+
yield rows.first
|
176
|
+
else
|
177
|
+
yield nil
|
178
|
+
end
|
179
|
+
}
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
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,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-mysql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aman Gupta
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-23 00:00:00 +04: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.9
|
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
|
+
- em-mysql.gemspec
|
36
|
+
- lib/em/mysql.rb
|
37
|
+
- lib/sequel/async.rb
|
38
|
+
- test.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/tmm1/em-mysql
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Async MySQL client API for Ruby/EventMachine
|
67
|
+
test_files: []
|
68
|
+
|