em-redis 0.2.2 → 0.2.3
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/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
|