├── .gitignore ├── History.txt ├── README.rdoc ├── Rakefile ├── lib ├── em-redis.rb └── em-redis │ └── redis_protocol.rb └── spec ├── live_redis_protocol_spec.rb ├── redis_commands_spec.rb ├── redis_protocol_spec.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 0.2.2 / 2009-12-29 2 | * reselect database after reconnecting 3 | 4 | == 0.2.1 / 2009-12-15 5 | * updated gem dependencies 6 | 7 | == 0.2 / 2009-12-15 8 | * rip off stock redis gem 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 or false, not 0 or 1 as in 0.1 11 | * info returns hash of symbols now 12 | * lrem has different argument order 13 | 14 | == 0.1.1 / 2009-05-01 15 | 16 | * added a number of aliases to redis-based method names 17 | * refactored process_cmd method for greater clarity 18 | 19 | == 0.1.0 / 2009-04-28 20 | 21 | * initial release 22 | * compatible with Redis 0.093 23 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == EM-REDIS 2 | 3 | == DESCRIPTION: 4 | 5 | An EventMachine[http://rubyeventmachine.com/] based library for interacting with the very cool Redis[http://code.google.com/p/redis/] data store by Salvatore 'antirez' Sanfilippo. 6 | Modeled after eventmachine's implementation of the memcached protocol, and influenced by Ezra Zygmuntowicz's {redis-rb}[http://github.com/ezmobius/redis-rb/tree/master] library (distributed as part of Redis). 7 | 8 | This library is only useful when used as part of an application that relies on 9 | Event Machine's event loop. It implements an EM-based client protocol, which 10 | leverages the non-blocking nature of the EM interface to achieve significant 11 | parallelization without threads. 12 | 13 | 14 | == FEATURES/PROBLEMS: 15 | 16 | Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable 17 | exception of MONITOR. 18 | 19 | == SYNOPSIS: 20 | 21 | Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when the call returns. 22 | 23 | require 'em-redis' 24 | 25 | EM.run do 26 | redis = EM::Protocols::Redis.connect 27 | redis.errback do |code| 28 | puts "Error code: #{code}" 29 | end 30 | redis.set "a", "foo" do |response| 31 | redis.get "a" do |response| 32 | puts response 33 | end 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 40 | end 41 | 42 | To run tests on a Redis server (currently compatible with 1.3) 43 | 44 | rake 45 | 46 | Because the EM::Protocol::Memcached code used Bacon for testing, test code is 47 | currently in the form of bacon specs. 48 | 49 | == REQUIREMENTS: 50 | 51 | * Redis (download[http://code.google.com/p/redis/downloads/list]) 52 | 53 | == INSTALL: 54 | 55 | sudo gem install em-redis 56 | 57 | == LICENSE: 58 | 59 | (The MIT License) 60 | 61 | Copyright (c) 2008, 2009 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining 64 | a copy of this software and associated documentation files (the 65 | 'Software'), to deal in the Software without restriction, including 66 | without limitation the rights to use, copy, modify, merge, publish, 67 | distribute, sublicense, and/or sell copies of the Software, and to 68 | permit persons to whom the Software is furnished to do so, subject to 69 | the following conditions: 70 | 71 | The above copyright notice and this permission notice shall be 72 | included in all copies or substantial portions of the Software. 73 | 74 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 75 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 76 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 77 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 78 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 79 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 80 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 81 | 82 | == CREDIT 83 | 84 | by Jonathan Broad (http://www.relativepath.org) 85 | 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Look in the tasks/setup.rb file for the various options that can be 2 | # configured in this Rakefile. The .rake files in the tasks directory 3 | # are where the options are used. 4 | 5 | begin 6 | require 'bones' 7 | rescue LoadError 8 | abort '### Please install the "bones" gem ###' 9 | end 10 | 11 | ensure_in_path 'lib' 12 | require 'em-redis' 13 | 14 | task :default => ['redis:test'] 15 | 16 | Bones { 17 | name 'em-redis' 18 | authors ['Jonathan Broad', 'Eugene Pimenov'] 19 | email 'libc@me.com' 20 | url 'http://github.com/libc/em-redis' 21 | summary 'An eventmachine-based implementation of the Redis protocol' 22 | description summary 23 | version EMRedis::VERSION 24 | 25 | readme_file 'README.rdoc' 26 | ignore_file '.gitignore' 27 | 28 | depend_on 'eventmachine', '>=0.12.10' 29 | 30 | depend_on "bacon", :development => true 31 | depend_on "em-spec", :development => true 32 | } 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 | 41 | # EOF 42 | -------------------------------------------------------------------------------- /lib/em-redis.rb: -------------------------------------------------------------------------------- 1 | 2 | module EMRedis 3 | 4 | # :stopdoc: 5 | VERSION = '0.2.2' 6 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR 7 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 8 | # :startdoc: 9 | 10 | # Returns the version string for the library. 11 | # 12 | def self.version 13 | VERSION 14 | end 15 | 16 | # Returns the library path for the module. If any arguments are given, 17 | # they will be joined to the end of the libray path using 18 | # File.join. 19 | # 20 | def self.libpath( *args ) 21 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten) 22 | end 23 | 24 | # Returns the lpath for the module. If any arguments are given, 25 | # they will be joined to the end of the path using 26 | # File.join. 27 | # 28 | def self.path( *args ) 29 | args.empty? ? PATH : ::File.join(PATH, args.flatten) 30 | end 31 | 32 | # Utility method used to require all files ending in .rb that lie in the 33 | # directory below this file that has the same name as the filename passed 34 | # in. Optionally, a specific _directory_ name can be passed in such that 35 | # the _filename_ does not have to be equivalent to the directory. 36 | # 37 | def self.require_all_libs_relative_to( fname, dir = nil ) 38 | dir ||= ::File.basename(fname, '.*') 39 | search_me = ::File.expand_path( 40 | ::File.join(::File.dirname(fname), dir, '**', '*.rb')) 41 | 42 | Dir.glob(search_me).sort.each {|rb| require rb} 43 | end 44 | 45 | end # module EMRedis 46 | 47 | EMRedis.require_all_libs_relative_to(__FILE__) 48 | -------------------------------------------------------------------------------- /lib/em-redis/redis_protocol.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'eventmachine' 3 | 4 | module EventMachine 5 | module Protocols 6 | module Redis 7 | include EM::Deferrable 8 | 9 | ## 10 | # constants 11 | ######################### 12 | 13 | OK = "OK".freeze 14 | MINUS = "-".freeze 15 | PLUS = "+".freeze 16 | COLON = ":".freeze 17 | DOLLAR = "$".freeze 18 | ASTERISK = "*".freeze 19 | DELIM = "\r\n".freeze 20 | 21 | BULK_COMMANDS = { 22 | "set" => true, 23 | "setnx" => true, 24 | "rpush" => true, 25 | "lpush" => true, 26 | "lset" => true, 27 | "lrem" => true, 28 | "sadd" => true, 29 | "srem" => true, 30 | "sismember" => true, 31 | "echo" => true, 32 | "getset" => true, 33 | "smove" => true, 34 | "zadd" => true, 35 | "zincrby" => true, 36 | "zrem" => true, 37 | "zscore" => true 38 | } 39 | 40 | MULTI_BULK_COMMANDS = { 41 | "mset" => true, 42 | "msetnx" => true, 43 | # these aliases aren't in redis gem 44 | "multi_get" => true 45 | } 46 | 47 | BOOLEAN_PROCESSOR = lambda{|r| %w(1 OK).include? r.to_s } 48 | 49 | REPLY_PROCESSOR = { 50 | "exists" => BOOLEAN_PROCESSOR, 51 | "sismember" => BOOLEAN_PROCESSOR, 52 | "sadd" => BOOLEAN_PROCESSOR, 53 | "srem" => BOOLEAN_PROCESSOR, 54 | "smove" => BOOLEAN_PROCESSOR, 55 | "zadd" => BOOLEAN_PROCESSOR, 56 | "zrem" => BOOLEAN_PROCESSOR, 57 | "move" => BOOLEAN_PROCESSOR, 58 | "setnx" => BOOLEAN_PROCESSOR, 59 | "del" => BOOLEAN_PROCESSOR, 60 | "renamenx" => BOOLEAN_PROCESSOR, 61 | "expire" => BOOLEAN_PROCESSOR, 62 | "select" => BOOLEAN_PROCESSOR, # not in redis gem 63 | "keys" => lambda{|r| r.split(" ")}, 64 | "info" => lambda{|r| 65 | info = {} 66 | r.each_line {|kv| 67 | k,v = kv.split(":",2).map{|x| x.chomp} 68 | info[k.to_sym] = v 69 | } 70 | info 71 | } 72 | } 73 | 74 | ALIASES = { 75 | "flush_db" => "flushdb", 76 | "flush_all" => "flushall", 77 | "last_save" => "lastsave", 78 | "key?" => "exists", 79 | "delete" => "del", 80 | "randkey" => "randomkey", 81 | "list_length" => "llen", 82 | "push_tail" => "rpush", 83 | "push_head" => "lpush", 84 | "pop_tail" => "rpop", 85 | "pop_head" => "lpop", 86 | "list_set" => "lset", 87 | "list_range" => "lrange", 88 | "list_trim" => "ltrim", 89 | "list_index" => "lindex", 90 | "list_rm" => "lrem", 91 | "set_add" => "sadd", 92 | "set_delete" => "srem", 93 | "set_count" => "scard", 94 | "set_member?" => "sismember", 95 | "set_members" => "smembers", 96 | "set_intersect" => "sinter", 97 | "set_intersect_store" => "sinterstore", 98 | "set_inter_store" => "sinterstore", 99 | "set_union" => "sunion", 100 | "set_union_store" => "sunionstore", 101 | "set_diff" => "sdiff", 102 | "set_diff_store" => "sdiffstore", 103 | "set_move" => "smove", 104 | "set_unless_exists" => "setnx", 105 | "rename_unless_exists" => "renamenx", 106 | "type?" => "type", 107 | "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", 114 | "zset_incr_by" => "zincrby", 115 | "zset_increment_by" => "zincrby", 116 | # these aliases aren't in redis gem 117 | "background_save" => 'bgsave', 118 | "async_save" => 'bgsave', 119 | "members" => 'smembers', 120 | "decrement_by" => "decrby", 121 | "decrement" => "decr", 122 | "increment_by" => "incrby", 123 | "increment" => "incr", 124 | "set_if_nil" => "setnx", 125 | "multi_get" => "mget", 126 | "random_key" => "randomkey", 127 | "random" => "randomkey", 128 | "rename_if_nil" => "renamenx", 129 | "tail_pop" => "rpop", 130 | "pop" => "rpop", 131 | "head_pop" => "lpop", 132 | "shift" => "lpop", 133 | "list_remove" => "lrem", 134 | "index" => "lindex", 135 | "trim" => "ltrim", 136 | "list_range" => "lrange", 137 | "range" => "lrange", 138 | "list_len" => "llen", 139 | "len" => "llen", 140 | "head_push" => "lpush", 141 | "unshift" => "lpush", 142 | "tail_push" => "rpush", 143 | "push" => "rpush", 144 | "add" => "sadd", 145 | "set_remove" => "srem", 146 | "set_size" => "scard", 147 | "member?" => "sismember", 148 | "intersect" => "sinter", 149 | "intersect_and_store" => "sinterstore", 150 | "members" => "smembers", 151 | "exists?" => "exists" 152 | } 153 | 154 | DISABLED_COMMANDS = { 155 | "monitor" => true, 156 | "sync" => true 157 | } 158 | 159 | def []=(key,value) 160 | set(key,value) 161 | end 162 | 163 | def set(key, value, expiry=nil) 164 | call_command([:set, key, value]) do |s| 165 | yield s if block_given? 166 | end 167 | expire(key, expiry) if expiry 168 | end 169 | 170 | def sort(key, options={}, &blk) 171 | cmd = ["SORT"] 172 | cmd << key 173 | cmd << "BY #{options[:by]}" if options[:by] 174 | cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get] 175 | cmd << "#{options[:order]}" if options[:order] 176 | cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit] 177 | call_command(cmd, &blk) 178 | end 179 | 180 | def incr(key, increment = nil, &blk) 181 | call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk) 182 | end 183 | 184 | def decr(key, decrement = nil, &blk) 185 | call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk) 186 | end 187 | 188 | def select(db, &blk) 189 | @db = db.to_i 190 | call_command(['select', @db], &blk) 191 | end 192 | 193 | def auth(password, &blk) 194 | @password = password 195 | call_command(['auth', password], &blk) 196 | end 197 | 198 | # Similar to memcache.rb's #get_multi, returns a hash mapping 199 | # keys to values. 200 | def mapped_mget(*keys) 201 | mget(*keys) do |response| 202 | result = {} 203 | response.each do |value| 204 | key = keys.shift 205 | result.merge!(key => value) unless value.nil? 206 | end 207 | yield result if block_given? 208 | end 209 | end 210 | 211 | # Ruby defines a now deprecated type method so we need to override it here 212 | # since it will never hit method_missing 213 | def type(key, &blk) 214 | call_command(['type', key], &blk) 215 | end 216 | 217 | def quit(&blk) 218 | call_command(['quit'], &blk) 219 | end 220 | 221 | def errback(&blk) 222 | @error_callback = blk 223 | end 224 | alias_method :on_error, :errback 225 | 226 | def method_missing(*argv, &blk) 227 | call_command(argv, &blk) 228 | end 229 | 230 | def call_command(argv, &blk) 231 | callback { raw_call_command(argv, &blk) } 232 | end 233 | 234 | def raw_call_command(argv, &blk) 235 | argv[0] = argv[0].to_s unless argv[0].kind_of? String 236 | argv[0] = argv[0].downcase 237 | send_command(argv) 238 | @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk] 239 | end 240 | 241 | def call_commands(argvs, &blk) 242 | callback { raw_call_commands(argvs, &blk) } 243 | end 244 | 245 | def raw_call_commands(argvs, &blk) 246 | if argvs.empty? # Shortcut 247 | blk.call [] 248 | return 249 | end 250 | 251 | argvs.each do |argv| 252 | argv[0] = argv[0].to_s unless argv[0].kind_of? String 253 | send_command argv 254 | end 255 | # FIXME: argvs may contain heterogenous commands, storing all 256 | # REPLY_PROCESSORs may turn out expensive and has been omitted 257 | # for now. 258 | @redis_callbacks << [nil, argvs.length, blk] 259 | end 260 | 261 | def send_command(argv) 262 | argv = argv.dup 263 | 264 | if MULTI_BULK_COMMANDS[argv.flatten[0].to_s] 265 | # TODO improve this code 266 | argvp = argv.flatten 267 | values = argvp.pop.to_a.flatten 268 | argvp = values.unshift(argvp[0]) 269 | command = ["*#{argvp.size}"] 270 | argvp.each do |v| 271 | v = v.to_s 272 | command << "$#{get_size(v)}" 273 | command << v 274 | end 275 | command = command.map {|cmd| "#{cmd}\r\n"}.join 276 | else 277 | command = "" 278 | bulk = nil 279 | argv[0] = argv[0].to_s.downcase 280 | argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]] 281 | raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]] 282 | if BULK_COMMANDS[argv[0]] and argv.length > 1 283 | bulk = argv[-1].to_s 284 | argv[-1] = get_size(bulk) 285 | end 286 | command << "#{argv.join(' ')}\r\n" 287 | command << "#{bulk}\r\n" if bulk 288 | end 289 | 290 | @logger.debug { "*** sending: #{command}" } if @logger 291 | send_data command 292 | end 293 | 294 | ## 295 | # errors 296 | ######################### 297 | 298 | class ParserError < StandardError; end 299 | class ProtocolError < StandardError; end 300 | 301 | class RedisError < StandardError 302 | attr_accessor :code 303 | end 304 | 305 | 306 | ## 307 | # em hooks 308 | ######################### 309 | 310 | def self.connect(*args) 311 | case args.length 312 | when 0 313 | options = {} 314 | when 1 315 | arg = args.shift 316 | case arg 317 | when Hash then options = arg 318 | when String then options = {:host => arg} 319 | else raise ArgumentError, 'first argument must be Hash or String' 320 | end 321 | when 2 322 | options = {:host => args[1], :port => args[2]} 323 | else 324 | raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" 325 | end 326 | options[:host] ||= '127.0.0.1' 327 | options[:port] = (options[:port] || 6379).to_i 328 | EM.connect options[:host], options[:port], self, options 329 | end 330 | 331 | def initialize(options = {}) 332 | @host = options[:host] 333 | @port = options[:port] 334 | @db = (options[:db] || 0).to_i 335 | @password = options[:password] 336 | @logger = options[:logger] 337 | @error_callback = lambda do |code| 338 | err = RedisError.new 339 | err.code = code 340 | raise err, "Redis server returned error code: #{code}" 341 | end 342 | @values = [] 343 | 344 | # These commands should be first 345 | auth_and_select_db 346 | end 347 | 348 | def auth_and_select_db 349 | call_command(["auth", @password]) if @password 350 | call_command(["select", @db]) unless @db == 0 351 | end 352 | private :auth_and_select_db 353 | 354 | def connection_completed 355 | @logger.debug { "Connected to #{@host}:#{@port}" } if @logger 356 | 357 | @redis_callbacks = [] 358 | @multibulk_n = false 359 | @reconnecting = false 360 | @connected = true 361 | 362 | succeed 363 | end 364 | 365 | # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause 366 | # stack overflows when there is too much data. 367 | # include EM::P::LineText2 368 | def receive_data(data) 369 | (@buffer ||= '') << data 370 | while index = @buffer.index(DELIM) 371 | begin 372 | line = @buffer.slice!(0, index+2) 373 | process_cmd line 374 | rescue ParserError 375 | @buffer[0...0] = line 376 | break 377 | end 378 | end 379 | end 380 | 381 | def process_cmd(line) 382 | @logger.debug { "*** processing #{line}" } if @logger 383 | # first character of buffer will always be the response type 384 | reply_type = line[0, 1] 385 | reply_args = line.slice(1..-3) # remove type character and \r\n 386 | case reply_type 387 | 388 | #e.g. -MISSING 389 | when MINUS 390 | # Missing, dispatch empty response 391 | dispatch_response(nil) 392 | # e.g. +OK 393 | when PLUS 394 | dispatch_response(reply_args) 395 | # e.g. $3\r\nabc\r\n 396 | # 'bulk' is more complex because it could be part of multi-bulk 397 | when DOLLAR 398 | data_len = Integer(reply_args) 399 | if data_len == -1 # expect no data; return nil 400 | dispatch_response(nil) 401 | elsif @buffer.size >= data_len + 2 # buffer is full of expected data 402 | dispatch_response(@buffer.slice!(0, data_len)) 403 | @buffer.slice!(0,2) # tossing \r\n 404 | else # buffer isn't full or nil 405 | # TODO: don't control execution with exceptions 406 | raise ParserError 407 | end 408 | #e.g. :8 409 | when COLON 410 | dispatch_response(Integer(reply_args)) 411 | #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n 412 | when ASTERISK 413 | multibulk_count = Integer(reply_args) 414 | if multibulk_count == -1 415 | dispatch_response([]) 416 | else 417 | start_multibulk(multibulk_count) 418 | end 419 | # Whu? 420 | else 421 | # TODO: get rid of this exception 422 | raise ProtocolError, "reply type not recognized: #{line.strip}" 423 | end 424 | end 425 | 426 | def dispatch_response(value) 427 | if @multibulk_n 428 | @multibulk_values << value 429 | @multibulk_n -= 1 430 | 431 | if @multibulk_n == 0 432 | value = @multibulk_values 433 | @multibulk_n = false 434 | else 435 | return 436 | end 437 | end 438 | 439 | callback = @redis_callbacks.shift 440 | if callback.kind_of?(Array) && callback.length == 2 441 | processor, blk = callback 442 | value = processor.call(value) if processor 443 | blk.call(value) if blk 444 | elsif callback.kind_of?(Array) && callback.length == 3 445 | processor, pipeline_count, blk = callback 446 | value = processor.call(value) if processor 447 | @values << value 448 | if pipeline_count > 1 449 | @redis_callbacks.unshift [processor, pipeline_count - 1, blk] 450 | else 451 | blk.call(@values) if blk 452 | @values = [] 453 | end 454 | end 455 | end 456 | 457 | def start_multibulk(multibulk_count) 458 | @multibulk_n = multibulk_count 459 | @multibulk_values = [] 460 | end 461 | 462 | def unbind 463 | @logger.debug { "Disconnected" } if @logger 464 | if @connected || @reconnecting 465 | EM.add_timer(1) do 466 | @logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger 467 | reconnect @host, @port 468 | auth_and_select_db 469 | end 470 | @connected = false 471 | @reconnecting = true 472 | @deferred_status = nil 473 | else 474 | # TODO: get rid of this exception 475 | raise 'Unable to connect to redis server' 476 | end 477 | end 478 | 479 | private 480 | def get_size(string) 481 | string.respond_to?(:bytesize) ? string.bytesize : string.size 482 | end 483 | 484 | end 485 | end 486 | end 487 | -------------------------------------------------------------------------------- /spec/live_redis_protocol_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") 2 | 3 | EM.describe EM::Protocols::Redis, "connected to an empty db" do 4 | 5 | before do 6 | @c = EM::Protocols::Redis.connect :db => 14 7 | @c.flushdb 8 | end 9 | 10 | should "be able to set a string value" do 11 | @c.set("foo", "bar") do |r| 12 | r.should == "OK" 13 | done 14 | end 15 | end 16 | 17 | should "be able to increment the value of a string" do 18 | @c.incr "foo" do |r| 19 | r.should == 1 20 | @c.incr "foo" do |r| 21 | r.should == 2 22 | done 23 | end 24 | end 25 | end 26 | 27 | should "be able to increment the value of a string by an amount" do 28 | @c.incrby "foo", 10 do |r| 29 | r.should == 10 30 | done 31 | end 32 | end 33 | 34 | should "be able to decrement the value of a string" do 35 | @c.incr "foo" do |r| 36 | r.should == 1 37 | @c.decr "foo" do |r| 38 | r.should == 0 39 | done 40 | end 41 | end 42 | end 43 | 44 | should "be able to decrement the value of a string by an amount" do 45 | @c.incrby "foo", 20 do |r| 46 | r.should == 20 47 | @c.decrby "foo", 10 do |r| 48 | r.should == 10 49 | done 50 | end 51 | end 52 | end 53 | 54 | should "be able to 'lpush' to a nonexistent list" do 55 | @c.lpush("foo", "bar") do |r| 56 | r.should == "OK" 57 | done 58 | end 59 | end 60 | 61 | should "be able to 'rpush' to a nonexistent list" do 62 | @c.rpush("foo", "bar") do |r| 63 | r.should == "OK" 64 | done 65 | end 66 | end 67 | 68 | 69 | should "be able to get the size of the database" do 70 | @c.dbsize do |r| 71 | r.should == 0 72 | done 73 | end 74 | end 75 | 76 | should "be able to add a member to a nonexistent set" do 77 | @c.sadd("set_foo", "bar") do |r| 78 | r.should == true 79 | done 80 | end 81 | end 82 | 83 | should "be able to get info about the db as a hash" do 84 | @c.info do |r| 85 | r.should.key? :redis_version 86 | done 87 | end 88 | end 89 | 90 | should "be able to save db" do 91 | @c.save do |r| 92 | r.should == "OK" 93 | done 94 | end 95 | end 96 | 97 | should "be able to save db in the background" do 98 | @c.bgsave do |r| 99 | r.should == "Background saving started" 100 | done 101 | end 102 | end 103 | 104 | end 105 | 106 | EM.describe EM::Protocols::Redis, "connected to a db containing some simple string-valued keys" do 107 | 108 | before do 109 | @c = EM::Protocols::Redis.connect :db => 14 110 | @c.flushdb 111 | @c.set "a", "b" 112 | @c.set "x", "y" 113 | end 114 | 115 | should "be able to fetch the values of multiple keys" do 116 | @c.mget "a", "x" do |r| 117 | r.should == ["b", "y"] 118 | done 119 | end 120 | end 121 | 122 | should "be able to fetch the values of multiple keys in a hash" do 123 | @c.mapped_mget "a", "x" do |r| 124 | r.should == {"a" => "b", "x" => "y"} 125 | done 126 | end 127 | end 128 | 129 | should "be able to fetch all the keys" do 130 | @c.keys "*" do |r| 131 | r.sort.should == ["a", "x"] 132 | done 133 | end 134 | end 135 | 136 | should "be able to set a value if a key doesn't exist" do 137 | @c.setnx "a", "foo" do |r| 138 | r.should == false 139 | @c.setnx "zzz", "foo" do |r| 140 | r.should == true 141 | done 142 | end 143 | end 144 | end 145 | 146 | should "be able to test for the existence of a key" do 147 | @c.exists "a" do |r| 148 | r.should == true 149 | @c.exists "zzz" do |r| 150 | r.should == false 151 | done 152 | end 153 | end 154 | end 155 | 156 | should "be able to delete a key" do 157 | @c.del "a" do |r| 158 | r.should == true 159 | @c.exists "a" do |r| 160 | r.should == false 161 | @c.del "a" do |r| 162 | r.should == false 163 | done 164 | end 165 | end 166 | end 167 | end 168 | 169 | should "be able to detect the type of a key, existing or not" do 170 | @c.type "a" do |r| 171 | r.should == "string" 172 | @c.type "zzz" do |r| 173 | r.should == "none" 174 | done 175 | end 176 | end 177 | end 178 | 179 | should "be able to rename a key" do 180 | @c.rename "a", "x" do |r| 181 | @c.get "x" do |r| 182 | r.should == "b" 183 | done 184 | end 185 | end 186 | end 187 | 188 | should "be able to rename a key unless it exists" do 189 | @c.renamenx "a", "x" do |r| 190 | r.should == false 191 | @c.renamenx "a", "zzz" do |r| 192 | r.should == true 193 | @c.get "zzz" do |r| 194 | r.should == "b" 195 | done 196 | end 197 | end 198 | end 199 | end 200 | 201 | 202 | end 203 | 204 | EM.describe EM::Protocols::Redis, "connected to a db containing a list" do 205 | 206 | before do 207 | @c = EM::Protocols::Redis.connect :db => 14 208 | @c.flushdb 209 | @c.lpush "foo", "c" 210 | @c.lpush "foo", "b" 211 | @c.lpush "foo", "a" 212 | end 213 | 214 | should "be able to 'lset' a list member and 'lindex' to retrieve it" do 215 | @c.lset("foo", 1, "bar") do |r| 216 | @c.lindex("foo", 1) do |r| 217 | r.should == "bar" 218 | done 219 | end 220 | end 221 | end 222 | 223 | should "be able to 'rpush' onto the tail of the list" do 224 | @c.rpush "foo", "d" do |r| 225 | r.should == "OK" 226 | @c.rpop "foo" do |r| 227 | r.should == "d" 228 | done 229 | end 230 | end 231 | end 232 | 233 | should "be able to 'lpush' onto the head of the list" do 234 | @c.lpush "foo", "d" do |r| 235 | r.should == "OK" 236 | @c.lpop "foo" do |r| 237 | r.should == "d" 238 | done 239 | end 240 | end 241 | end 242 | 243 | should "be able to 'rpop' off the tail of the list" do 244 | @c.rpop("foo") do |r| 245 | r.should == "c" 246 | done 247 | end 248 | end 249 | 250 | should "be able to 'lpop' off the tail of the list" do 251 | @c.lpop("foo") do |r| 252 | r.should == "a" 253 | done 254 | end 255 | end 256 | 257 | should "be able to get a range of values from a list" do 258 | @c.lrange("foo", 0, 1) do |r| 259 | r.should == ["a", "b"] 260 | done 261 | end 262 | end 263 | 264 | should "be able to 'ltrim' a list" do 265 | @c.ltrim("foo", 0, 1) do |r| 266 | r.should == "OK" 267 | @c.llen("foo") do |r| 268 | r.should == 2 269 | done 270 | end 271 | end 272 | end 273 | 274 | should "be able to 'rem' a list element" do 275 | @c.lrem("foo", 0, "a") do |r| 276 | r.should == 1 277 | @c.llen("foo") do |r| 278 | r.should == 2 279 | done 280 | end 281 | end 282 | end 283 | 284 | should "be able to detect the type of a list" do 285 | @c.type "foo" do |r| 286 | r.should == "list" 287 | done 288 | end 289 | end 290 | 291 | end 292 | 293 | EM.describe EM::Protocols::Redis, "connected to a db containing two sets" do 294 | before do 295 | @c = EM::Protocols::Redis.connect :db => 14 296 | @c.flushdb 297 | @c.sadd "foo", "a" 298 | @c.sadd "foo", "b" 299 | @c.sadd "foo", "c" 300 | @c.sadd "bar", "c" 301 | @c.sadd "bar", "d" 302 | @c.sadd "bar", "e" 303 | end 304 | 305 | should "be able to find a set's cardinality" do 306 | @c.scard("foo") do |r| 307 | r.should == 3 308 | done 309 | end 310 | end 311 | 312 | should "be able to add a new member to a set unless it is a duplicate" do 313 | @c.sadd("foo", "d") do |r| 314 | r.should == true # success 315 | @c.sadd("foo", "a") do |r| 316 | r.should == false # failure 317 | @c.scard("foo") do |r| 318 | r.should == 4 319 | done 320 | end 321 | end 322 | end 323 | end 324 | 325 | should "be able to remove a set member if it exists" do 326 | @c.srem("foo", "a") do |r| 327 | r.should == true 328 | @c.srem("foo", "z") do |r| 329 | r.should == false 330 | @c.scard("foo") do |r| 331 | r.should == 2 332 | done 333 | end 334 | end 335 | end 336 | end 337 | 338 | should "be able to retrieve a set's members" do 339 | @c.smembers("foo") do |r| 340 | r.sort.should == ["a", "b", "c"] 341 | done 342 | end 343 | end 344 | 345 | should "be able to detect set membership" do 346 | @c.sismember("foo", "a") do |r| 347 | r.should == true 348 | @c.sismember("foo", "z") do |r| 349 | r.should == false 350 | done 351 | end 352 | end 353 | end 354 | 355 | should "be able to find the sets' intersection" do 356 | @c.sinter("foo", "bar") do |r| 357 | r.should == ["c"] 358 | done 359 | end 360 | end 361 | 362 | should "be able to find and store the sets' intersection" do 363 | @c.sinterstore("baz", "foo", "bar") do |r| 364 | r.should == 1 365 | @c.smembers("baz") do |r| 366 | r.should == ["c"] 367 | done 368 | end 369 | end 370 | end 371 | 372 | should "be able to find the sets' union" do 373 | @c.sunion("foo", "bar") do |r| 374 | r.sort.should == ["a","b","c","d","e"] 375 | done 376 | end 377 | end 378 | 379 | should "be able to find and store the sets' union" do 380 | @c.sunionstore("baz", "foo", "bar") do |r| 381 | r.should == 5 382 | @c.smembers("baz") do |r| 383 | r.sort.should == ["a","b","c","d","e"] 384 | done 385 | end 386 | end 387 | end 388 | 389 | should "be able to detect the type of a set" do 390 | @c.type "foo" do |r| 391 | r.should == "set" 392 | done 393 | end 394 | end 395 | 396 | end 397 | 398 | 399 | EM.describe EM::Protocols::Redis, "connected to a db containing three linked lists" do 400 | before do 401 | @c = EM::Protocols::Redis.connect :db => 14 402 | @c.flushdb 403 | @c.rpush "foo", "a" 404 | @c.rpush "foo", "b" 405 | @c.set "a_sort", "2" 406 | @c.set "b_sort", "1" 407 | @c.set "a_data", "foo" 408 | @c.set "b_data", "bar" 409 | end 410 | 411 | should "be able to collate a sorted set of data" do 412 | @c.sort("foo", :by => "*_sort", :get => "*_data") do |r| 413 | r.should == ["bar", "foo"] 414 | done 415 | end 416 | end 417 | 418 | should "be able to get keys selectively" do 419 | @c.keys "a_*" do |r| 420 | r.should == ["a_sort", "a_data"] 421 | done 422 | end 423 | end 424 | end 425 | 426 | EM.describe EM::Protocols::Redis, "when reconnecting" do 427 | before do 428 | @c = EM::Protocols::Redis.connect :db => 14 429 | @c.flushdb 430 | end 431 | 432 | should "select previously selected datase" do 433 | #simulate disconnect 434 | @c.set('foo', 'a') { @c.close_connection_after_writing } 435 | 436 | EM.add_timer(2) do 437 | @c.get('foo') do |r| 438 | r.should == 'a' 439 | @c.get('non_existing') do |r| 440 | r.should == nil 441 | done 442 | end 443 | end 444 | end 445 | end 446 | end 447 | -------------------------------------------------------------------------------- /spec/redis_commands_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") 2 | require 'logger' 3 | 4 | EM.describe EM::Protocols::Redis do 5 | default_timeout 1 6 | 7 | before do 8 | @r = EM::Protocols::Redis.connect :db => 14 9 | @r.flushdb 10 | @r['foo'] = 'bar' 11 | end 12 | 13 | 14 | should "be able to provide a logger" do 15 | log = StringIO.new 16 | r = EM::Protocols::Redis.connect :db => 14, :logger => Logger.new(log) 17 | r.ping do 18 | log.string.should.include "ping" 19 | done 20 | end 21 | end 22 | 23 | it "should be able to PING" do 24 | @r.ping { |r| r.should == 'PONG'; done } 25 | end 26 | 27 | it "should be able to GET a key" do 28 | @r.get('foo') { |r| r.should == 'bar'; done } 29 | end 30 | 31 | it "should be able to SET a key" do 32 | @r['foo'] = 'nik' 33 | @r.get('foo') { |r| r.should == 'nik'; done } 34 | end 35 | 36 | it "should properly handle trailing newline characters" do 37 | @r['foo'] = "bar\n" 38 | @r.get('foo') { |r| r.should == "bar\n"; done } 39 | end 40 | 41 | it "should store and retrieve all possible characters at the beginning and the end of a string" do 42 | (0..255).each do |char_idx| 43 | string = "#{char_idx.chr}---#{char_idx.chr}" 44 | @r['foo'] = string 45 | @r.get('foo') { |r| r.should == string } 46 | end 47 | @r.ping { done } 48 | end 49 | 50 | it "should be able to SET a key with an expiry" do 51 | timeout(3) 52 | 53 | @r.set('foo', 'bar', 1) 54 | @r.get('foo') { |r| r.should == 'bar' } 55 | EM.add_timer(2) do 56 | @r.get('foo') { |r| r.should == nil } 57 | @r.ping { done } 58 | end 59 | end 60 | 61 | it "should be able to return a TTL for a key" do 62 | @r.set('foo', 'bar', 1) 63 | @r.ttl('foo') { |r| r.should == 1; done } 64 | end 65 | 66 | it "should be able to SETNX" do 67 | @r['foo'] = 'nik' 68 | @r.get('foo') { |r| r.should == 'nik' } 69 | @r.setnx 'foo', 'bar' 70 | @r.get('foo') { |r| r.should == 'nik' } 71 | 72 | @r.ping { done } 73 | end 74 | # 75 | it "should be able to GETSET" do 76 | @r.getset('foo', 'baz') { |r| r.should == 'bar' } 77 | @r.get('foo') { |r| r.should == 'baz'; done } 78 | end 79 | # 80 | it "should be able to INCR a key" do 81 | @r.del('counter') 82 | @r.incr('counter') { |r| r.should == 1 } 83 | @r.incr('counter') { |r| r.should == 2 } 84 | @r.incr('counter') { |r| r.should == 3 } 85 | 86 | @r.ping { done } 87 | end 88 | # 89 | it "should be able to INCRBY a key" do 90 | @r.del('counter') 91 | @r.incrby('counter', 1) { |r| r.should == 1 } 92 | @r.incrby('counter', 2) { |r| r.should == 3 } 93 | @r.incrby('counter', 3) { |r| r.should == 6 } 94 | 95 | @r.ping { done } 96 | end 97 | # 98 | it "should be able to DECR a key" do 99 | @r.del('counter') 100 | @r.incr('counter') { |r| r.should == 1 } 101 | @r.incr('counter') { |r| r.should == 2 } 102 | @r.incr('counter') { |r| r.should == 3 } 103 | @r.decr('counter') { |r| r.should == 2 } 104 | @r.decr('counter', 2) { |r| r.should == 0; done } 105 | end 106 | # 107 | it "should be able to RANDKEY" do 108 | @r.randkey { |r| r.should.not == nil; done } 109 | end 110 | # 111 | it "should be able to RENAME a key" do 112 | @r.del 'foo' 113 | @r.del 'bar' 114 | @r['foo'] = 'hi' 115 | @r.rename 'foo', 'bar' 116 | @r.get('bar') { |r| r.should == 'hi' ; done } 117 | end 118 | # 119 | it "should be able to RENAMENX a key" do 120 | @r.del 'foo' 121 | @r.del 'bar' 122 | @r['foo'] = 'hi' 123 | @r['bar'] = 'ohai' 124 | @r.renamenx 'foo', 'bar' 125 | @r.get('bar') { |r| r.should == 'ohai' ; done } 126 | end 127 | # 128 | it "should be able to get DBSIZE of the database" do 129 | dbsize_without_foo, dbsize_with_foo = nil 130 | @r.delete 'foo' 131 | @r.dbsize { |r| dbsize_without_foo = r } 132 | @r['foo'] = 0 133 | @r.dbsize { |r| dbsize_with_foo = r } 134 | 135 | @r.ping do 136 | dbsize_with_foo.should == dbsize_without_foo + 1 137 | done 138 | end 139 | end 140 | # 141 | it "should be able to EXPIRE a key" do 142 | timeout(3) 143 | 144 | @r['foo'] = 'bar' 145 | @r.expire 'foo', 1 146 | @r.get('foo') { |r| r.should == "bar" } 147 | EM.add_timer(2) do 148 | @r.get('foo') { |r| r.should == nil } 149 | @r.ping { done } 150 | end 151 | end 152 | # 153 | it "should be able to EXISTS" do 154 | @r['foo'] = 'nik' 155 | @r.exists('foo') { |r| r.should == true } 156 | @r.del 'foo' 157 | @r.exists('foo') { |r| r.should == false ; done } 158 | end 159 | # 160 | it "should be able to KEYS" do 161 | @r.keys("f*") { |keys| keys.each { |key| @r.del key } } 162 | @r['f'] = 'nik' 163 | @r['fo'] = 'nak' 164 | @r['foo'] = 'qux' 165 | @r.keys("f*") { |r| r.sort.should == ['f', 'fo', 'foo'].sort } 166 | 167 | @r.ping { done } 168 | end 169 | # 170 | it "should be able to return a random key (RANDOMKEY)" do 171 | 3.times do |i| 172 | @r.randomkey do |r| 173 | @r.exists(r) do |e| 174 | e.should == true 175 | done if i == 2 176 | end 177 | end 178 | end 179 | end 180 | # 181 | it "should be able to check the TYPE of a key" do 182 | @r['foo'] = 'nik' 183 | @r.type('foo') { |r| r.should == "string" } 184 | @r.del 'foo' 185 | @r.type('foo') { |r| r.should == "none" ; done } 186 | end 187 | # 188 | it "should be able to push to the head of a list (LPUSH)" do 189 | @r.lpush "list", 'hello' 190 | @r.lpush "list", 42 191 | @r.type('list') { |r| r.should == "list" } 192 | @r.llen('list') { |r| r.should == 2 } 193 | @r.lpop('list') { |r| r.should == '42'; done } 194 | end 195 | # 196 | it "should be able to push to the tail of a list (RPUSH)" do 197 | @r.rpush "list", 'hello' 198 | @r.type('list') { |r| r.should == "list" } 199 | @r.llen('list') { |r| r.should == 1 ; done } 200 | end 201 | # 202 | it "should be able to pop the tail of a list (RPOP)" do 203 | @r.rpush "list", 'hello' 204 | @r.rpush"list", 'goodbye' 205 | @r.type('list') { |r| r.should == "list" } 206 | @r.llen('list') { |r| r.should == 2 } 207 | @r.rpop('list') { |r| r.should == 'goodbye'; done } 208 | end 209 | # 210 | it "should be able to pop the head of a list (LPOP)" do 211 | @r.rpush "list", 'hello' 212 | @r.rpush "list", 'goodbye' 213 | @r.type('list') { |r| r.should == "list" } 214 | @r.llen('list') { |r| r.should == 2 } 215 | @r.lpop('list') { |r| r.should == 'hello'; done } 216 | end 217 | # 218 | it "should be able to get the length of a list (LLEN)" do 219 | @r.rpush "list", 'hello' 220 | @r.rpush "list", 'goodbye' 221 | @r.type('list') { |r| r.should == "list" } 222 | @r.llen('list') { |r| r.should == 2 ; done } 223 | end 224 | # 225 | it "should be able to get a range of values from a list (LRANGE)" do 226 | @r.rpush "list", 'hello' 227 | @r.rpush "list", 'goodbye' 228 | @r.rpush "list", '1' 229 | @r.rpush "list", '2' 230 | @r.rpush "list", '3' 231 | @r.type('list') { |r| r.should == "list" } 232 | @r.llen('list') { |r| r.should == 5 } 233 | @r.lrange('list', 2, -1) { |r| r.should == ['1', '2', '3']; done } 234 | end 235 | # 236 | it "should be able to trim a list (LTRIM)" do 237 | @r.rpush "list", 'hello' 238 | @r.rpush "list", 'goodbye' 239 | @r.rpush "list", '1' 240 | @r.rpush "list", '2' 241 | @r.rpush "list", '3' 242 | @r.type('list') { |r| r.should == "list" } 243 | @r.llen('list') { |r| r.should == 5 } 244 | @r.ltrim 'list', 0, 1 245 | @r.llen('list') { |r| r.should == 2 } 246 | @r.lrange('list', 0, -1) { |r| r.should == ['hello', 'goodbye']; done } 247 | end 248 | # 249 | it "should be able to get a value by indexing into a list (LINDEX)" do 250 | @r.rpush "list", 'hello' 251 | @r.rpush "list", 'goodbye' 252 | @r.type('list') { |r| r.should == "list" } 253 | @r.llen('list') { |r| r.should == 2 } 254 | @r.lindex('list', 1) { |r| r.should == 'goodbye'; done } 255 | end 256 | # 257 | it "should be able to set a value by indexing into a list (LSET)" do 258 | @r.rpush "list", 'hello' 259 | @r.rpush "list", 'hello' 260 | @r.type('list') { |r| r.should == "list" } 261 | @r.llen('list') { |r| r.should == 2 } 262 | @r.lset('list', 1, 'goodbye') { |r| r.should == 'OK' } 263 | @r.lindex('list', 1) { |r| r.should == 'goodbye'; done } 264 | end 265 | # 266 | it "should be able to remove values from a list (LREM)" do 267 | @r.rpush "list", 'hello' 268 | @r.rpush "list", 'goodbye' 269 | @r.type('list') { |r| r.should == "list" } 270 | @r.llen('list') { |r| r.should == 2 } 271 | @r.lrem('list', 1, 'hello') { |r| r.should == 1 } 272 | @r.lrange('list', 0, -1) { |r| r.should == ['goodbye']; done } 273 | end 274 | 275 | it "should be able to pop values from a list and push them onto a temp list(RPOPLPUSH)" do 276 | @r.rpush "list", 'one' 277 | @r.rpush "list", 'two' 278 | @r.rpush "list", 'three' 279 | @r.type('list') { |r| r.should == "list" } 280 | @r.llen('list') { |r| r.should == 3 } 281 | @r.lrange('list', 0, -1) { |r| r.should == ['one', 'two', 'three'] } 282 | @r.lrange('tmp', 0, -1) { |r| r.should == [] } 283 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'three' } 284 | @r.lrange('tmp', 0, -1) { |r| r.should == ['three'] } 285 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'two' } 286 | @r.lrange('tmp', 0, -1) { |r| r.should == ['two', 'three'] } 287 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'one' } 288 | @r.lrange('tmp', 0, -1) { |r| r.should == ['one', 'two', 'three']; done } 289 | end 290 | # 291 | it "should be able add members to a set (SADD)" do 292 | @r.sadd "set", 'key1' 293 | @r.sadd "set", 'key2' 294 | @r.type('set') { |r| r.should == "set" } 295 | @r.scard('set') { |r| r.should == 2 } 296 | @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort; done } 297 | end 298 | # 299 | it "should be able delete members to a set (SREM)" do 300 | @r.sadd "set", 'key1' 301 | @r.sadd "set", 'key2' 302 | @r.type('set') { |r| r.should == "set" } 303 | @r.scard('set') { |r| r.should == 2 } 304 | @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort } 305 | @r.srem('set', 'key1') 306 | @r.scard('set') { |r| r.should == 1 } 307 | @r.smembers('set') { |r| r.should == ['key2']; done } 308 | end 309 | # 310 | it "should be able to return and remove random key from set (SPOP)" do 311 | @r.sadd "set_pop", "key1" 312 | @r.sadd "set_pop", "key2" 313 | @r.spop("set_pop") { |r| r.should.not == nil } 314 | @r.scard("set_pop") { |r| r.should == 1; done } 315 | end 316 | # 317 | it "should be able to return random key without delete the key from a set (SRANDMEMBER)" do 318 | @r.sadd "set_srandmember", "key1" 319 | @r.sadd "set_srandmember", "key2" 320 | @r.srandmember("set_srandmember") { |r| r.should.not == nil } 321 | @r.scard("set_srandmember") { |r| r.should == 2; done } 322 | end 323 | # 324 | it "should be able count the members of a set (SCARD)" do 325 | @r.sadd "set", 'key1' 326 | @r.sadd "set", 'key2' 327 | @r.type('set') { |r| r.should == "set" } 328 | @r.scard('set') { |r| r.should == 2; done } 329 | end 330 | # 331 | it "should be able test for set membership (SISMEMBER)" do 332 | @r.sadd "set", 'key1' 333 | @r.sadd "set", 'key2' 334 | @r.type('set') { |r| r.should == "set" } 335 | @r.scard('set') { |r| r.should == 2 } 336 | @r.sismember('set', 'key1') { |r| r.should == true } 337 | @r.sismember('set', 'key2') { |r| r.should == true } 338 | @r.sismember('set', 'notthere') { |r| r.should == false; done } 339 | end 340 | # 341 | it "should be able to do set intersection (SINTER)" do 342 | @r.sadd "set", 'key1' 343 | @r.sadd "set", 'key2' 344 | @r.sadd "set2", 'key2' 345 | @r.sinter('set', 'set2') { |r| r.should == ['key2']; done } 346 | end 347 | # 348 | it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do 349 | @r.sadd "set", 'key1' 350 | @r.sadd "set", 'key2' 351 | @r.sadd "set2", 'key2' 352 | @r.sinterstore('newone', 'set', 'set2') { |r| r.should == 1 } 353 | @r.smembers('newone') { |r| r.should == ['key2']; done } 354 | end 355 | # 356 | it "should be able to do set union (SUNION)" do 357 | @r.sadd "set", 'key1' 358 | @r.sadd "set", 'key2' 359 | @r.sadd "set2", 'key2' 360 | @r.sadd "set2", 'key3' 361 | @r.sunion('set', 'set2') { |r| r.sort.should == ['key1','key2','key3'].sort; done } 362 | end 363 | # 364 | it "should be able to do set union and store the results in a key (SUNIONSTORE)" do 365 | @r.sadd "set", 'key1' 366 | @r.sadd "set", 'key2' 367 | @r.sadd "set2", 'key2' 368 | @r.sadd "set2", 'key3' 369 | @r.sunionstore('newone', 'set', 'set2') { |r| r.should == 3 } 370 | @r.smembers('newone') { |r| r.sort.should == ['key1','key2','key3'].sort; done } 371 | end 372 | # 373 | it "should be able to do set difference (SDIFF)" do 374 | @r.sadd "set", 'a' 375 | @r.sadd "set", 'b' 376 | @r.sadd "set2", 'b' 377 | @r.sadd "set2", 'c' 378 | @r.sdiff('set', 'set2') { |r| r.should == ['a']; done } 379 | end 380 | # 381 | it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do 382 | @r.sadd "set", 'a' 383 | @r.sadd "set", 'b' 384 | @r.sadd "set2", 'b' 385 | @r.sadd "set2", 'c' 386 | @r.sdiffstore('newone', 'set', 'set2') 387 | @r.smembers('newone') { |r| r.should == ['a']; done } 388 | end 389 | # 390 | it "should be able move elements from one set to another (SMOVE)" do 391 | @r.sadd 'set1', 'a' 392 | @r.sadd 'set1', 'b' 393 | @r.sadd 'set2', 'x' 394 | @r.smove('set1', 'set2', 'a') { |r| r.should == true } 395 | @r.sismember('set2', 'a') { |r| r.should == true } 396 | @r.delete('set1') { done } 397 | end 398 | # 399 | it "should be able to do crazy SORT queries" do 400 | # The 'Dogs' is capitialized on purpose 401 | @r['dog_1'] = 'louie' 402 | @r.rpush 'Dogs', 1 403 | @r['dog_2'] = 'lucy' 404 | @r.rpush 'Dogs', 2 405 | @r['dog_3'] = 'max' 406 | @r.rpush 'Dogs', 3 407 | @r['dog_4'] = 'taj' 408 | @r.rpush 'Dogs', 4 409 | @r.sort('Dogs', :get => 'dog_*', :limit => [0,1]) { |r| r.should == ['louie'] } 410 | @r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj'] } 411 | @r.ping { done } 412 | end 413 | 414 | it "should be able to handle array of :get using SORT" do 415 | @r['dog:1:name'] = 'louie' 416 | @r['dog:1:breed'] = 'mutt' 417 | @r.rpush 'dogs', 1 418 | @r['dog:2:name'] = 'lucy' 419 | @r['dog:2:breed'] = 'poodle' 420 | @r.rpush 'dogs', 2 421 | @r['dog:3:name'] = 'max' 422 | @r['dog:3:breed'] = 'hound' 423 | @r.rpush 'dogs', 3 424 | @r['dog:4:name'] = 'taj' 425 | @r['dog:4:breed'] = 'terrier' 426 | @r.rpush 'dogs', 4 427 | @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]) { |r| r.should == ['louie', 'mutt'] } 428 | @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj', 'terrier'] } 429 | @r.ping { done } 430 | end 431 | # 432 | it "should be able count the members of a zset" do 433 | @r.set_add "set", 'key1' 434 | @r.set_add "set", 'key2' 435 | @r.zset_add 'zset', 1, 'set' 436 | @r.zset_count('zset') { |r| r.should == 1 } 437 | @r.delete('set') 438 | @r.delete('zset') { done } 439 | end 440 | # 441 | it "should be able add members to a zset" do 442 | @r.set_add "set", 'key1' 443 | @r.set_add "set", 'key2' 444 | @r.zset_add 'zset', 1, 'set' 445 | @r.zset_range('zset', 0, 1) { |r| r.should == ['set'] } 446 | @r.zset_count('zset') { |r| r.should == 1 } 447 | @r.delete('set') 448 | @r.delete('zset') { done } 449 | end 450 | # 451 | it "should be able delete members to a zset" do 452 | @r.set_add "set", 'key1' 453 | @r.set_add "set", 'key2' 454 | @r.type?('set') { |r| r.should == "set" } 455 | @r.set_add "set2", 'key3' 456 | @r.set_add "set2", 'key4' 457 | @r.type?('set2') { |r| r.should == "set" } 458 | @r.zset_add 'zset', 1, 'set' 459 | @r.zset_count('zset') { |r| r.should == 1 } 460 | @r.zset_add 'zset', 2, 'set2' 461 | @r.zset_count('zset') { |r| r.should == 2 } 462 | @r.zset_delete 'zset', 'set' 463 | @r.zset_count('zset') { |r| r.should == 1 } 464 | @r.delete('set') 465 | @r.delete('set2') 466 | @r.delete('zset') { done } 467 | end 468 | # 469 | it "should be able to get a range of values from a zset" do 470 | @r.set_add "set", 'key1' 471 | @r.set_add "set", 'key2' 472 | @r.set_add "set2", 'key3' 473 | @r.set_add "set2", 'key4' 474 | @r.set_add "set3", 'key1' 475 | @r.type?('set') { |r| r.should == 'set' } 476 | @r.type?('set2') { |r| r.should == 'set' } 477 | @r.type?('set3') { |r| r.should == 'set' } 478 | @r.zset_add 'zset', 1, 'set' 479 | @r.zset_add 'zset', 2, 'set2' 480 | @r.zset_add 'zset', 3, 'set3' 481 | @r.zset_count('zset') { |r| r.should == 3 } 482 | @r.zset_range('zset', 0, 3) { |r| r.should == ['set', 'set2', 'set3'] } 483 | @r.delete('set') 484 | @r.delete('set2') 485 | @r.delete('set3') 486 | @r.delete('zset') { done } 487 | end 488 | # 489 | it "should be able to get a reverse range of values from a zset" do 490 | @r.set_add "set", 'key1' 491 | @r.set_add "set", 'key2' 492 | @r.set_add "set2", 'key3' 493 | @r.set_add "set2", 'key4' 494 | @r.set_add "set3", 'key1' 495 | @r.type?('set') { |r| r.should == 'set' } 496 | @r.type?('set2') { |r| r.should == 'set' } 497 | @r.type?('set3') { |r| r.should == 'set' } 498 | @r.zset_add 'zset', 1, 'set' 499 | @r.zset_add 'zset', 2, 'set2' 500 | @r.zset_add 'zset', 3, 'set3' 501 | @r.zset_count('zset') { |r| r.should == 3 } 502 | @r.zset_reverse_range('zset', 0, 3) { |r| r.should == ['set3', 'set2', 'set'] } 503 | @r.delete('set') 504 | @r.delete('set2') 505 | @r.delete('set3') 506 | @r.delete('zset') { done } 507 | end 508 | # 509 | it "should be able to get a range by score of values from a zset" do 510 | @r.set_add "set", 'key1' 511 | @r.set_add "set", 'key2' 512 | @r.set_add "set2", 'key3' 513 | @r.set_add "set2", 'key4' 514 | @r.set_add "set3", 'key1' 515 | @r.set_add "set4", 'key4' 516 | @r.zset_add 'zset', 1, 'set' 517 | @r.zset_add 'zset', 2, 'set2' 518 | @r.zset_add 'zset', 3, 'set3' 519 | @r.zset_add 'zset', 4, 'set4' 520 | @r.zset_count('zset') { |r| r.should == 4 } 521 | @r.zset_range_by_score('zset', 2, 3) { |r| r.should == ['set2', 'set3'] } 522 | @r.delete('set') 523 | @r.delete('set2') 524 | @r.delete('set3') 525 | @r.delete('set4') 526 | @r.delete('zset') { done } 527 | end 528 | # 529 | it "should be able to get a score for a specific value in a zset (ZSCORE)" do 530 | @r.zset_add "zset", 23, "value" 531 | @r.zset_score("zset", "value") { |r| r.should == "23" } 532 | 533 | @r.zset_score("zset", "value2") { |r| r.should == nil } 534 | @r.zset_score("unknown_zset", "value") { |r| r.should == nil } 535 | 536 | @r.delete("zset") { done } 537 | end 538 | # 539 | it "should be able to increment a range score of a zset (ZINCRBY)" do 540 | # create a new zset 541 | @r.zset_increment_by "hackers", 1965, "Yukihiro Matsumoto" 542 | @r.zset_score("hackers", "Yukihiro Matsumoto") { |r| r.should == "1965" } 543 | 544 | # add a new element 545 | @r.zset_increment_by "hackers", 1912, "Alan Turing" 546 | @r.zset_score("hackers", "Alan Turing") { |r| r.should == "1912" } 547 | 548 | # update the score 549 | @r.zset_increment_by "hackers", 100, "Alan Turing" # yeah, we are making Turing a bit younger 550 | @r.zset_score("hackers", "Alan Turing") { |r| r.should == "2012" } 551 | 552 | # attempt to update a key that's not a zset 553 | @r["i_am_not_a_zet"] = "value" 554 | # shouldn't raise error anymore 555 | @r.zset_incr_by("i_am_not_a_zet", 23, "element") { |r| r.should == nil } 556 | 557 | @r.delete("hackers") 558 | @r.delete("i_am_not_a_zet") { done } 559 | end 560 | # 561 | it "should provide info (INFO)" do 562 | @r.info do |r| 563 | [:last_save_time, :redis_version, :total_connections_received, :connected_clients, :total_commands_processed, :connected_slaves, :uptime_in_seconds, :used_memory, :uptime_in_days, :changes_since_last_save].each do |x| 564 | r.keys.include?(x).should == true 565 | end 566 | done 567 | end 568 | end 569 | # 570 | it "should be able to flush the database (FLUSHDB)" do 571 | @r['key1'] = 'keyone' 572 | @r['key2'] = 'keytwo' 573 | @r.keys('*') { |r| r.sort.should == ['foo', 'key1', 'key2'].sort } #foo from before 574 | @r.flushdb 575 | @r.keys('*') { |r| r.should == []; done } 576 | end 577 | # 578 | it "should be able to SELECT database" do 579 | @r.select(15) 580 | @r.get('foo') { |r| r.should == nil; done } 581 | end 582 | # 583 | it "should be able to provide the last save time (LASTSAVE)" do 584 | @r.lastsave do |savetime| 585 | Time.at(savetime).class.should == Time 586 | Time.at(savetime).should <= Time.now 587 | done 588 | end 589 | end 590 | 591 | it "should be able to MGET keys" do 592 | @r['foo'] = 1000 593 | @r['bar'] = 2000 594 | @r.mget('foo', 'bar') { |r| r.should == ['1000', '2000'] } 595 | @r.mget('foo', 'bar', 'baz') { |r| r.should == ['1000', '2000', nil] } 596 | @r.ping { done } 597 | end 598 | 599 | it "should be able to mapped MGET keys" do 600 | @r['foo'] = 1000 601 | @r['bar'] = 2000 602 | @r.mapped_mget('foo', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} } 603 | @r.mapped_mget('foo', 'baz', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} } 604 | @r.ping { done } 605 | end 606 | 607 | it "should be able to MSET values" do 608 | @r.mset :key1 => "value1", :key2 => "value2" 609 | @r.get('key1') { |r| r.should == "value1" } 610 | @r.get('key2') { |r| r.should == "value2"; done } 611 | end 612 | 613 | it "should be able to MSETNX values" do 614 | @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2" 615 | @r.mget('keynx1', 'keynx2') { |r| r.should == ["valuenx1", "valuenx2"] } 616 | 617 | @r["keynx1"] = "value1" 618 | @r["keynx2"] = "value2" 619 | @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2" 620 | @r.mget('keynx1', 'keynx2') { |r| r.should == ["value1", "value2"]; done } 621 | end 622 | 623 | it "should bgsave" do 624 | @r.bgsave do |r| 625 | ['OK', 'Background saving started'].include?(r).should == true 626 | done 627 | end 628 | end 629 | 630 | it "should be able to ECHO" do 631 | @r.echo("message in a bottle\n") { |r| r.should == "message in a bottle\n"; done } 632 | end 633 | 634 | # Tests are disabled due uncatchable exceptions. We should use on_error callback, 635 | # intead of raising exceptions in random places. 636 | # 637 | # it "should raise error when invoke MONITOR" do 638 | # # lambda { @r.monitor }.should.raise 639 | # done 640 | # end 641 | # 642 | # it "should raise error when invoke SYNC" do 643 | # # lambda { @r.sync }.should.raise 644 | # done 645 | # end 646 | 647 | it "should work with 10 commands" do 648 | @r.call_commands((1..10).map { |i| 649 | ['get', "foo"] 650 | }) do |rs| 651 | rs.length.should == 10 652 | rs.each { |r| r.should == "bar" } 653 | done 654 | end 655 | end 656 | it "should work with 1 command" do 657 | @r.call_commands([['get', "foo"]]) do |rs| 658 | rs.length.should == 1 659 | rs[0].should == "bar" 660 | done 661 | end 662 | end 663 | it "should work with zero commands" do 664 | @r.call_commands([]) do |rs| 665 | rs.should == [] 666 | done 667 | end 668 | end 669 | end 670 | -------------------------------------------------------------------------------- /spec/redis_protocol_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") 2 | 3 | EM.describe EM::Protocols::Redis do 4 | default_timeout 1 5 | 6 | before do 7 | @c = TestConnection.new 8 | end 9 | 10 | # Inline request protocol 11 | should 'send inline commands correctly' do 12 | @c.call_command(["GET", 'a']) 13 | @c.sent_data.should == "get a\r\n" 14 | done 15 | end 16 | 17 | should "space-separate multiple inline arguments" do 18 | @c.call_command(["GET", 'a', 'b', 'c']) 19 | @c.sent_data.should == "get a b c\r\n" 20 | done 21 | end 22 | 23 | # Multiline request protocol 24 | should "send multiline commands correctly" do 25 | @c.call_command(["SET", "foo", "abc"]) 26 | @c.sent_data.should == "set foo 3\r\nabc\r\n" 27 | done 28 | end 29 | 30 | should "send integers in multiline commands correctly" do 31 | @c.call_command(["SET", "foo", 1_000_000]) 32 | @c.sent_data.should == "set foo 7\r\n1000000\r\n" 33 | done 34 | end 35 | 36 | # Specific calls 37 | # 38 | # SORT 39 | should "send sort command" do 40 | @c.sort "foo" 41 | @c.sent_data.should == "sort foo\r\n" 42 | done 43 | end 44 | 45 | should "send sort command with all optional parameters" do 46 | @c.sort "foo", :by => "foo_sort_*", :limit => [0, 10], :get => "data_*", :order => "DESC ALPHA" 47 | @c.sent_data.should == "sort foo BY foo_sort_* GET data_* DESC ALPHA LIMIT 0 10\r\n" 48 | done 49 | end 50 | 51 | should "parse keys response into an array" do 52 | @c.keys("*") do |resp| 53 | resp.should == ["a","b","c"] 54 | done 55 | end 56 | @c.receive_data "$5\r\na b c\r\n" 57 | end 58 | 59 | 60 | # Inline response 61 | should "parse an inline response" do 62 | @c.call_command(["PING"]) do |resp| 63 | resp.should == "OK" 64 | done 65 | end 66 | @c.receive_data "+OK\r\n" 67 | end 68 | 69 | should "parse an inline integer response" do 70 | @c.call_command(["integer"]) do |resp| 71 | resp.should == 0 72 | done 73 | end 74 | @c.receive_data ":0\r\n" 75 | end 76 | 77 | should "call processor if any" do 78 | @c.call_command(["EXISTS"]) do |resp| 79 | resp.should == false 80 | done 81 | end 82 | @c.receive_data ":0\r\n" 83 | end 84 | 85 | should "parse an inline error response" do 86 | @c.call_command(["blarg"]) do |resp| 87 | resp.should == nil 88 | done 89 | end 90 | @c.receive_data "-FAIL\r\n" 91 | end 92 | 93 | should "trigger a given error callback (specified with on_error) for inline error response instead of raising an error" do 94 | lambda do 95 | @c.call_command(["blarg"]) 96 | @c.on_error {|code| code.should == "FAIL"; done } 97 | @c.receive_data "-FAIL\r\n" 98 | end.should.not.raise(EM::P::Redis::RedisError) 99 | end 100 | 101 | should "trigger a given error callback for inline error response instead of raising an error" do 102 | lambda do 103 | @c.call_command(["blarg"]) 104 | @c.errback { |code| code.should == "FAIL"; done } 105 | @c.receive_data "-FAIL\r\n" 106 | end.should.not.raise(EM::P::Redis::RedisError) 107 | end 108 | 109 | # Bulk response 110 | should "parse a bulk response" do 111 | @c.call_command(["GET", "foo"]) do |resp| 112 | resp.should == "bar" 113 | done 114 | end 115 | @c.receive_data "$3\r\n" 116 | @c.receive_data "bar\r\n" 117 | end 118 | 119 | should "distinguish nil in a bulk response" do 120 | @c.call_command(["GET", "bar"]) do |resp| 121 | resp.should == nil 122 | done 123 | end 124 | @c.receive_data "$-1\r\n" 125 | end 126 | 127 | # Multi-bulk response 128 | should "parse a multi-bulk response" do 129 | @c.call_command(["RANGE", 0, 10]) do |resp| 130 | resp.should == ["a", "b", "foo"] 131 | done 132 | end 133 | @c.receive_data "*3\r\n" 134 | @c.receive_data "$1\r\na\r\n" 135 | @c.receive_data "$1\r\nb\r\n" 136 | @c.receive_data "$3\r\nfoo\r\n" 137 | end 138 | 139 | should "distinguish nil in a multi-bulk response" do 140 | @c.call_command(["RANGE", 0, 10]) do |resp| 141 | resp.should == ["a", nil, "foo"] 142 | done 143 | end 144 | @c.receive_data "*3\r\n" 145 | @c.receive_data "$1\r\na\r\n" 146 | @c.receive_data "$-1\r\n" 147 | @c.receive_data "$3\r\nfoo\r\n" 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/test_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../lib/em-redis") 2 | require 'bacon' 3 | require 'em-spec/bacon' 4 | 5 | EM.spec_backend = EventMachine::Spec::Bacon 6 | 7 | class TestConnection 8 | include EM::P::Redis 9 | 10 | def send_data data 11 | sent_data << data 12 | end 13 | 14 | def sent_data 15 | @sent_data ||= '' 16 | end 17 | 18 | def initialize 19 | super 20 | connection_completed 21 | end 22 | end 23 | --------------------------------------------------------------------------------