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