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.
@@ -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 of false, not 0/1 as in 0.1
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
 
@@ -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::Protocol::Redis.connect
27
- error_callback = lambda {|code| puts "Error code: #{code}" }
28
- redis.on_error error_callback
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 live tests on a Redis server (currently compatible with 1.3)
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 redis:offline_test
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. I'll port them to RSpec at some point.
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:live_test', 'redis:offline_test']
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
@@ -2,7 +2,7 @@
2
2
  module EMRedis
3
3
 
4
4
  # :stopdoc:
5
- VERSION = '0.2.2'
5
+ VERSION = '0.2.3'
6
6
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
7
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
8
  # :startdoc:
@@ -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
- "keys" => lambda{|r| r.split(" ")},
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" => 'zcard',
109
- "zset_range_by_score" => 'zrangebyscore',
110
- "zset_reverse_range" => 'zrevrange',
111
- "zset_range" => 'zrange',
112
- "zset_delete" => 'zrem',
113
- "zset_score" => 'zscore',
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
- @current_database = db
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
- @current_password = password
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 on_error(&blk)
220
- @err_cb = blk
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 call_command(argv, &blk)
228
- callback { raw_call_command(argv, &blk) }
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 raw_call_command(argv, &blk)
295
+ def call_command(argv, &blk)
232
296
  argv = argv.dup
233
297
 
234
- if MULTI_BULK_COMMANDS[argv.flatten[0].to_s]
235
- # TODO improve this code
236
- argvp = argv.flatten
237
- values = argvp.pop.to_a.flatten
238
- argvp = values.unshift(argvp[0])
239
- command = ["*#{argvp.size}"]
240
- argvp.each do |v|
241
- v = v.to_s
242
- command << "$#{get_size(v)}"
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
- puts "*** sending: #{command}" if $debug
261
- @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
262
- send_data command
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(host = 'localhost', port = 6379 )
282
- puts "*** connecting" if $debug
283
- EM.connect host, port, self, host, port
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 initialize(host, port = 6379 )
287
- puts "*** initializing" if $debug
288
- @host, @port = host, port
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
- puts "*** connection_complete!" if $debug
388
+ @logger.debug { "Connected to #{@host}:#{@port}" } if @logger
389
+
293
390
  @redis_callbacks = []
294
- @values = []
295
- @multibulk_n = 0
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
- puts "*** processing #{line}" if $debug
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
- if @err_cb
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
- if @multibulk_n > 0 # we're in the middle of a multibulk reply
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
- if @multibulk_n > 0 # we're in the middle of a multibulk reply
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
- # FYI, ParseError puts command back on head of buffer, waits for
370
- # more data complete buffer
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
- @multibulk_n = Integer(reply_args)
381
- dispatch_response(nil) if @multibulk_n == -1
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
- puts "*** unbinding" if $debug
397
- if @connected or @reconnecting
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
- auth @current_password if @current_password
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