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.
@@ -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