em-redis 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +1 -1
- data/README.rdoc +12 -10
- data/Rakefile +11 -2
- data/lib/em-redis.rb +1 -1
- data/lib/em-redis/redis_protocol.rb +180 -88
- data/spec/live_redis_protocol_spec.rb +12 -18
- data/spec/redis_commands_spec.rb +761 -0
- data/spec/redis_protocol_spec.rb +10 -2
- data/spec/test_helper.rb +7 -0
- metadata +50 -36
- data/Manifest.txt +0 -10
- data/tasks/em-redis.rake +0 -12
data/History.txt
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
== 0.2 / 2009-12-15
|
8
8
|
* rip off stock redis gem
|
9
9
|
* sort is no longer compatible with 0.1 version
|
10
|
-
* response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true
|
10
|
+
* response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true or false, not 0 or 1 as in 0.1
|
11
11
|
* info returns hash of symbols now
|
12
12
|
* lrem has different argument order
|
13
13
|
|
data/README.rdoc
CHANGED
@@ -23,26 +23,28 @@ Like any Deferrable eventmachine-based protocol implementation, using EM-Redis i
|
|
23
23
|
require 'em-redis'
|
24
24
|
|
25
25
|
EM.run do
|
26
|
-
redis = EM::
|
27
|
-
|
28
|
-
|
26
|
+
redis = EM::Protocols::Redis.connect
|
27
|
+
redis.errback do |code|
|
28
|
+
puts "Error code: #{code}"
|
29
|
+
end
|
29
30
|
redis.set "a", "foo" do |response|
|
30
31
|
redis.get "a" do |response|
|
31
32
|
puts response
|
32
33
|
end
|
33
34
|
end
|
35
|
+
# We get pipelining for free
|
36
|
+
redis.set("b", "bar")
|
37
|
+
redis.get("a") do |response|
|
38
|
+
puts response # will be foo
|
39
|
+
end
|
34
40
|
end
|
35
41
|
|
36
|
-
To run
|
37
|
-
|
38
|
-
rake redis:live_test
|
39
|
-
|
40
|
-
To run a test of the underlying protocol implemention
|
42
|
+
To run tests on a Redis server (currently compatible with 1.3)
|
41
43
|
|
42
|
-
rake
|
44
|
+
rake
|
43
45
|
|
44
46
|
Because the EM::Protocol::Memcached code used Bacon for testing, test code is
|
45
|
-
currently in the form of bacon specs.
|
47
|
+
currently in the form of bacon specs.
|
46
48
|
|
47
49
|
== REQUIREMENTS:
|
48
50
|
|
data/Rakefile
CHANGED
@@ -11,13 +11,15 @@ end
|
|
11
11
|
ensure_in_path 'lib'
|
12
12
|
require 'em-redis'
|
13
13
|
|
14
|
-
task :default => ['redis:
|
14
|
+
task :default => ['redis:test']
|
15
15
|
|
16
16
|
Bones {
|
17
17
|
name 'em-redis'
|
18
18
|
authors ['Jonathan Broad', 'Eugene Pimenov']
|
19
19
|
email 'libc@me.com'
|
20
|
-
url ''
|
20
|
+
url 'http://github.com/libc/em-redis'
|
21
|
+
summary 'An eventmachine-based implementation of the Redis protocol'
|
22
|
+
description summary
|
21
23
|
version EMRedis::VERSION
|
22
24
|
|
23
25
|
readme_file 'README.rdoc'
|
@@ -29,4 +31,11 @@ Bones {
|
|
29
31
|
depend_on "em-spec", :development => true
|
30
32
|
}
|
31
33
|
|
34
|
+
namespace :redis do
|
35
|
+
desc "Test em-redis against a live Redis"
|
36
|
+
task :test do
|
37
|
+
sh "bacon spec/live_redis_protocol_spec.rb spec/redis_commands_spec.rb spec/redis_protocol_spec.rb"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
32
41
|
# EOF
|
data/lib/em-redis.rb
CHANGED
@@ -28,18 +28,22 @@ module EventMachine
|
|
28
28
|
"sadd" => true,
|
29
29
|
"srem" => true,
|
30
30
|
"sismember" => true,
|
31
|
-
"rpoplpush" => true,
|
32
31
|
"echo" => true,
|
33
32
|
"getset" => true,
|
34
33
|
"smove" => true,
|
35
34
|
"zadd" => true,
|
35
|
+
"zincrby" => true,
|
36
36
|
"zrem" => true,
|
37
|
-
"zscore" => true
|
37
|
+
"zscore" => true,
|
38
|
+
"hget" => true,
|
39
|
+
"hdel" => true,
|
40
|
+
"hexists" => true
|
38
41
|
}
|
39
42
|
|
40
43
|
MULTI_BULK_COMMANDS = {
|
41
44
|
"mset" => true,
|
42
45
|
"msetnx" => true,
|
46
|
+
"hset" => true,
|
43
47
|
# these aliases aren't in redis gem
|
44
48
|
"multi_get" => true
|
45
49
|
}
|
@@ -60,7 +64,16 @@ module EventMachine
|
|
60
64
|
"renamenx" => BOOLEAN_PROCESSOR,
|
61
65
|
"expire" => BOOLEAN_PROCESSOR,
|
62
66
|
"select" => BOOLEAN_PROCESSOR, # not in redis gem
|
63
|
-
"
|
67
|
+
"hset" => BOOLEAN_PROCESSOR,
|
68
|
+
"hdel" => BOOLEAN_PROCESSOR,
|
69
|
+
"hexists" => BOOLEAN_PROCESSOR,
|
70
|
+
"keys" => lambda {|r|
|
71
|
+
if r.is_a?(Array)
|
72
|
+
r
|
73
|
+
else
|
74
|
+
r.split(" ")
|
75
|
+
end
|
76
|
+
},
|
64
77
|
"info" => lambda{|r|
|
65
78
|
info = {}
|
66
79
|
r.each_line {|kv|
|
@@ -68,6 +81,9 @@ module EventMachine
|
|
68
81
|
info[k.to_sym] = v
|
69
82
|
}
|
70
83
|
info
|
84
|
+
},
|
85
|
+
"hgetall" => lambda{|r|
|
86
|
+
Hash[*r]
|
71
87
|
}
|
72
88
|
}
|
73
89
|
|
@@ -105,12 +121,14 @@ module EventMachine
|
|
105
121
|
"rename_unless_exists" => "renamenx",
|
106
122
|
"type?" => "type",
|
107
123
|
"zset_add" => "zadd",
|
108
|
-
"zset_count" =>
|
109
|
-
"zset_range_by_score" =>
|
110
|
-
"zset_reverse_range" =>
|
111
|
-
"zset_range" =>
|
112
|
-
"zset_delete" =>
|
113
|
-
"zset_score" =>
|
124
|
+
"zset_count" => "zcard",
|
125
|
+
"zset_range_by_score" => "zrangebyscore",
|
126
|
+
"zset_reverse_range" => "zrevrange",
|
127
|
+
"zset_range" => "zrange",
|
128
|
+
"zset_delete" => "zrem",
|
129
|
+
"zset_score" => "zscore",
|
130
|
+
"zset_incr_by" => "zincrby",
|
131
|
+
"zset_increment_by" => "zincrby",
|
114
132
|
# these aliases aren't in redis gem
|
115
133
|
"background_save" => 'bgsave',
|
116
134
|
"async_save" => 'bgsave',
|
@@ -160,9 +178,9 @@ module EventMachine
|
|
160
178
|
|
161
179
|
def set(key, value, expiry=nil)
|
162
180
|
call_command([:set, key, value]) do |s|
|
163
|
-
expire(key, expiry) if s == OK && expiry
|
164
181
|
yield s if block_given?
|
165
182
|
end
|
183
|
+
expire(key, expiry) if expiry
|
166
184
|
end
|
167
185
|
|
168
186
|
def sort(key, options={}, &blk)
|
@@ -184,12 +202,12 @@ module EventMachine
|
|
184
202
|
end
|
185
203
|
|
186
204
|
def select(db, &blk)
|
187
|
-
@
|
188
|
-
call_command(['select', db], &blk)
|
205
|
+
@db = db.to_i
|
206
|
+
call_command(['select', @db], &blk)
|
189
207
|
end
|
190
208
|
|
191
209
|
def auth(password, &blk)
|
192
|
-
@
|
210
|
+
@password = password
|
193
211
|
call_command(['auth', password], &blk)
|
194
212
|
end
|
195
213
|
|
@@ -216,37 +234,80 @@ module EventMachine
|
|
216
234
|
call_command(['quit'], &blk)
|
217
235
|
end
|
218
236
|
|
219
|
-
def
|
220
|
-
|
237
|
+
def exec(&blk)
|
238
|
+
call_command(['exec'], &blk)
|
239
|
+
end
|
240
|
+
|
241
|
+
# I'm not sure autocommit is a good idea.
|
242
|
+
# For example:
|
243
|
+
# r.multi { r.set('a', 'b') { raise "kaboom" } }
|
244
|
+
# will commit "a" and will stop EM
|
245
|
+
def multi
|
246
|
+
call_command(['multi'])
|
247
|
+
if block_given?
|
248
|
+
begin
|
249
|
+
yield self
|
250
|
+
exec
|
251
|
+
rescue Exception => e
|
252
|
+
discard
|
253
|
+
raise e
|
254
|
+
end
|
255
|
+
end
|
221
256
|
end
|
222
257
|
|
258
|
+
def mset(*args, &blk)
|
259
|
+
hsh = args.pop if Hash === args.last
|
260
|
+
if hsh
|
261
|
+
call_command(hsh.to_a.flatten.unshift(:mset), &blk)
|
262
|
+
else
|
263
|
+
call_command(args.unshift(:mset), &blk)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def msetnx(*args, &blk)
|
268
|
+
hsh = args.pop if Hash === args.last
|
269
|
+
if hsh
|
270
|
+
call_command(hsh.to_a.flatten.unshift(:msetnx), &blk)
|
271
|
+
else
|
272
|
+
call_command(args.unshift(:msetnx), &blk)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def errback(&blk)
|
277
|
+
@error_callback = blk
|
278
|
+
end
|
279
|
+
alias_method :on_error, :errback
|
280
|
+
|
223
281
|
def method_missing(*argv, &blk)
|
224
282
|
call_command(argv, &blk)
|
225
283
|
end
|
226
284
|
|
227
|
-
def
|
228
|
-
|
285
|
+
def maybe_lock(&blk)
|
286
|
+
if !EM.reactor_thread?
|
287
|
+
EM.schedule { maybe_lock(&blk) }
|
288
|
+
elsif @connected
|
289
|
+
yield
|
290
|
+
else
|
291
|
+
callback { yield }
|
292
|
+
end
|
229
293
|
end
|
230
294
|
|
231
|
-
def
|
295
|
+
def call_command(argv, &blk)
|
232
296
|
argv = argv.dup
|
233
297
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
command << "
|
243
|
-
command << v
|
298
|
+
argv[0] = argv[0].to_s.downcase
|
299
|
+
if MULTI_BULK_COMMANDS[argv[0]]
|
300
|
+
command = ""
|
301
|
+
command << "*#{argv.size}\r\n"
|
302
|
+
argv.each do |a|
|
303
|
+
a = a.to_s
|
304
|
+
command << "$#{get_size(a)}\r\n"
|
305
|
+
command << a
|
306
|
+
command << "\r\n"
|
244
307
|
end
|
245
|
-
command = command.map {|cmd| "#{cmd}\r\n"}.join
|
246
308
|
else
|
247
309
|
command = ""
|
248
310
|
bulk = nil
|
249
|
-
argv[0] = argv[0].to_s.downcase
|
250
311
|
argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
|
251
312
|
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
252
313
|
if BULK_COMMANDS[argv[0]] and argv.length > 1
|
@@ -257,9 +318,11 @@ module EventMachine
|
|
257
318
|
command << "#{bulk}\r\n" if bulk
|
258
319
|
end
|
259
320
|
|
260
|
-
|
261
|
-
|
262
|
-
|
321
|
+
@logger.debug { "*** sending: #{command}" } if @logger
|
322
|
+
maybe_lock do
|
323
|
+
@redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
|
324
|
+
send_data command
|
325
|
+
end
|
263
326
|
end
|
264
327
|
|
265
328
|
##
|
@@ -278,24 +341,58 @@ module EventMachine
|
|
278
341
|
# em hooks
|
279
342
|
#########################
|
280
343
|
|
281
|
-
def self.connect(
|
282
|
-
|
283
|
-
|
344
|
+
def self.connect(*args)
|
345
|
+
case args.length
|
346
|
+
when 0
|
347
|
+
options = {}
|
348
|
+
when 1
|
349
|
+
arg = args.shift
|
350
|
+
case arg
|
351
|
+
when Hash then options = arg
|
352
|
+
when String then options = {:host => arg}
|
353
|
+
else raise ArgumentError, 'first argument must be Hash or String'
|
354
|
+
end
|
355
|
+
when 2
|
356
|
+
options = {:host => args[0], :port => args[1]}
|
357
|
+
else
|
358
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
|
359
|
+
end
|
360
|
+
options[:host] ||= '127.0.0.1'
|
361
|
+
options[:port] = (options[:port] || 6379).to_i
|
362
|
+
EM.connect options[:host], options[:port], self, options
|
363
|
+
end
|
364
|
+
|
365
|
+
def initialize(options = {})
|
366
|
+
@host = options[:host]
|
367
|
+
@port = options[:port]
|
368
|
+
@db = (options[:db] || 0).to_i
|
369
|
+
@password = options[:password]
|
370
|
+
@logger = options[:logger]
|
371
|
+
@error_callback = lambda do |code|
|
372
|
+
err = RedisError.new
|
373
|
+
err.code = code
|
374
|
+
raise err, "Redis server returned error code: #{code}"
|
375
|
+
end
|
376
|
+
|
377
|
+
# These commands should be first
|
378
|
+
auth_and_select_db
|
284
379
|
end
|
285
380
|
|
286
|
-
def
|
287
|
-
|
288
|
-
|
381
|
+
def auth_and_select_db
|
382
|
+
call_command(["auth", @password]) if @password
|
383
|
+
call_command(["select", @db]) unless @db == 0
|
289
384
|
end
|
385
|
+
private :auth_and_select_db
|
290
386
|
|
291
387
|
def connection_completed
|
292
|
-
|
388
|
+
@logger.debug { "Connected to #{@host}:#{@port}" } if @logger
|
389
|
+
|
293
390
|
@redis_callbacks = []
|
294
|
-
@
|
295
|
-
@multibulk_n
|
391
|
+
@previous_multibulks = []
|
392
|
+
@multibulk_n = false
|
393
|
+
@reconnecting = false
|
394
|
+
@connected = true
|
296
395
|
|
297
|
-
@reconnecting = false
|
298
|
-
@connected = true
|
299
396
|
succeed
|
300
397
|
end
|
301
398
|
|
@@ -303,7 +400,7 @@ module EventMachine
|
|
303
400
|
# stack overflows when there is too much data.
|
304
401
|
# include EM::P::LineText2
|
305
402
|
def receive_data(data)
|
306
|
-
(@buffer||='') << data
|
403
|
+
(@buffer ||= '') << data
|
307
404
|
while index = @buffer.index(DELIM)
|
308
405
|
begin
|
309
406
|
line = @buffer.slice!(0, index+2)
|
@@ -316,7 +413,7 @@ module EventMachine
|
|
316
413
|
end
|
317
414
|
|
318
415
|
def process_cmd(line)
|
319
|
-
|
416
|
+
@logger.debug { "*** processing #{line}" } if @logger
|
320
417
|
# first character of buffer will always be the response type
|
321
418
|
reply_type = line[0, 1]
|
322
419
|
reply_args = line.slice(1..-3) # remove type character and \r\n
|
@@ -325,85 +422,80 @@ module EventMachine
|
|
325
422
|
#e.g. -MISSING
|
326
423
|
when MINUS
|
327
424
|
@redis_callbacks.shift # throw away the cb?
|
328
|
-
|
329
|
-
@err_cb.call(reply_args)
|
330
|
-
else
|
331
|
-
err = RedisError.new
|
332
|
-
err.code = reply_args
|
333
|
-
raise err, "Redis server returned error code: #{err.code}"
|
334
|
-
end
|
335
|
-
|
425
|
+
@error_callback.call(reply_args)
|
336
426
|
# e.g. +OK
|
337
427
|
when PLUS
|
338
428
|
dispatch_response(reply_args)
|
339
|
-
|
340
429
|
# e.g. $3\r\nabc\r\n
|
341
430
|
# 'bulk' is more complex because it could be part of multi-bulk
|
342
431
|
when DOLLAR
|
343
432
|
data_len = Integer(reply_args)
|
344
433
|
if data_len == -1 # expect no data; return nil
|
345
|
-
|
346
|
-
@values << nil
|
347
|
-
if @values.size == @multibulk_n # DING, we're done
|
348
|
-
dispatch_response(@values)
|
349
|
-
@values = []
|
350
|
-
@multibulk_n = 0
|
351
|
-
end
|
352
|
-
else
|
353
|
-
dispatch_response(nil)
|
354
|
-
end
|
434
|
+
dispatch_response(nil)
|
355
435
|
elsif @buffer.size >= data_len + 2 # buffer is full of expected data
|
356
|
-
|
357
|
-
@values << @buffer.slice!(0, data_len)
|
358
|
-
if @values.size == @multibulk_n # DING, we're done
|
359
|
-
dispatch_response(@values)
|
360
|
-
@values = []
|
361
|
-
@multibulk_n = 0
|
362
|
-
end
|
363
|
-
else # not multibulk
|
364
|
-
value = @buffer.slice!(0, data_len)
|
365
|
-
dispatch_response(value)
|
366
|
-
end
|
436
|
+
dispatch_response(@buffer.slice!(0, data_len))
|
367
437
|
@buffer.slice!(0,2) # tossing \r\n
|
368
438
|
else # buffer isn't full or nil
|
369
|
-
#
|
370
|
-
|
371
|
-
raise ParserError
|
439
|
+
# TODO: don't control execution with exceptions
|
440
|
+
raise ParserError
|
372
441
|
end
|
373
|
-
|
374
442
|
#e.g. :8
|
375
443
|
when COLON
|
376
444
|
dispatch_response(Integer(reply_args))
|
377
|
-
|
378
445
|
#e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
|
379
446
|
when ASTERISK
|
380
|
-
|
381
|
-
|
382
|
-
|
447
|
+
multibulk_count = Integer(reply_args)
|
448
|
+
if multibulk_count == -1 || multibulk_count == 0
|
449
|
+
dispatch_response([])
|
450
|
+
else
|
451
|
+
if @multibulk_n
|
452
|
+
@previous_multibulks << [@multibulk_n, @multibulk_values]
|
453
|
+
end
|
454
|
+
@multibulk_n = multibulk_count
|
455
|
+
@multibulk_values = []
|
456
|
+
end
|
383
457
|
# Whu?
|
384
458
|
else
|
459
|
+
# TODO: get rid of this exception
|
385
460
|
raise ProtocolError, "reply type not recognized: #{line.strip}"
|
386
461
|
end
|
387
462
|
end
|
388
463
|
|
389
464
|
def dispatch_response(value)
|
465
|
+
if @multibulk_n
|
466
|
+
@multibulk_values << value
|
467
|
+
@multibulk_n -= 1
|
468
|
+
|
469
|
+
if @multibulk_n == 0
|
470
|
+
value = @multibulk_values
|
471
|
+
@multibulk_n,@multibulk_values = @previous_multibulks.pop
|
472
|
+
if @multibulk_n
|
473
|
+
dispatch_response(value)
|
474
|
+
return
|
475
|
+
end
|
476
|
+
else
|
477
|
+
return
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
390
481
|
processor, blk = @redis_callbacks.shift
|
391
482
|
value = processor.call(value) if processor
|
392
483
|
blk.call(value) if blk
|
393
484
|
end
|
394
485
|
|
395
486
|
def unbind
|
396
|
-
|
397
|
-
if @connected
|
487
|
+
@logger.debug { "Disconnected" } if @logger
|
488
|
+
if @connected || @reconnecting
|
398
489
|
EM.add_timer(1) do
|
490
|
+
@logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger
|
399
491
|
reconnect @host, @port
|
400
|
-
|
401
|
-
select @current_database if @current_database
|
492
|
+
auth_and_select_db
|
402
493
|
end
|
403
494
|
@connected = false
|
404
495
|
@reconnecting = true
|
405
496
|
@deferred_status = nil
|
406
497
|
else
|
498
|
+
# TODO: get rid of this exception
|
407
499
|
raise 'Unable to connect to redis server'
|
408
500
|
end
|
409
501
|
end
|