diff --git a/client-libraries/ruby/Rakefile b/client-libraries/ruby/Rakefile index b6217054..e659d18a 100644 --- a/client-libraries/ruby/Rakefile +++ b/client-libraries/ruby/Rakefile @@ -9,7 +9,7 @@ require 'tasks/redis.tasks' GEM = 'redis' GEM_NAME = 'redis' GEM_VERSION = '0.1' -AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark'] +AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi'] EMAIL = "ez@engineyard.com" HOMEPAGE = "http://github.com/ezmobius/redis-rb" SUMMARY = "Ruby client library for redis key value storage server" @@ -28,7 +28,7 @@ spec = Gem::Specification.new do |s| s.add_dependency "rspec" s.require_path = 'lib' s.autorequire = GEM - s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,spec}/**/*") + s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*") end task :default => :spec diff --git a/client-libraries/ruby/examples/test_server.rb b/client-libraries/ruby/examples/test_server.rb deleted file mode 100644 index fa388d15..00000000 --- a/client-libraries/ruby/examples/test_server.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'socket' -require 'pp' -require File.join(File.dirname(__FILE__), '../lib/redis') - -#require File.join(File.dirname(__FILE__), '../lib/server') - - -#r = Redis.new -#loop do - -# puts "--------------------------------------" -# sleep 12 -#end \ No newline at end of file diff --git a/client-libraries/ruby/lib/dist_redis.rb b/client-libraries/ruby/lib/dist_redis.rb index 830f8b6d..d92c956e 100644 --- a/client-libraries/ruby/lib/dist_redis.rb +++ b/client-libraries/ruby/lib/dist_redis.rb @@ -4,32 +4,30 @@ class DistRedis attr_reader :ring def initialize(opts={}) hosts = [] - + db = opts[:db] || nil - timeout = opts[:timeout] || nil + timeout = opts[:timeout] || nil raise Error, "No hosts given" unless opts[:hosts] opts[:hosts].each do |h| host, port = h.split(':') - hosts << Redis.new(:host => host, :port => port, :db => db, :timeout => timeout, :db => db) + hosts << Redis.new(:host => host, :port => port, :db => db, :timeout => timeout) end - @ring = HashRing.new hosts + @ring = HashRing.new hosts end - + def node_for_key(key) - if key =~ /\{(.*)?\}/ - key = $1 - end + key = $1 if key =~ /\{(.*)?\}/ @ring.get_node(key) end - + def add_server(server) server, port = server.split(':') @ring.add_node Redis.new(:host => server, :port => port) end - + def method_missing(sym, *args, &blk) if redis = node_for_key(args.first.to_s) redis.send sym, *args, &blk @@ -37,41 +35,49 @@ class DistRedis super end end - + def keys(glob) - keyz = [] - @ring.nodes.each do |red| - keyz.concat red.keys(glob) + @ring.nodes.map do |red| + red.keys(glob) end - keyz end - + def save - @ring.nodes.each do |red| - red.save - end + on_each_node :save end - + def bgsave - @ring.nodes.each do |red| - red.bgsave - end + on_each_node :bgsave end - + def quit - @ring.nodes.each do |red| - red.quit - end + on_each_node :quit end - + + def flush_all + on_each_node :flush_all + end + alias_method :flushall, :flush_all + + def flush_db + on_each_node :flush_db + end + alias_method :flushdb, :flush_db + def delete_cloud! @ring.nodes.each do |red| red.keys("*").each do |key| red.delete key - end + end end end - + + def on_each_node(command, *args) + @ring.nodes.each do |red| + red.send(command, *args) + end + end + end @@ -94,21 +100,21 @@ r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localho p r['urdad2'] p r['urmom3'] p r['urdad3'] - + r.push_tail 'listor', 'foo1' r.push_tail 'listor', 'foo2' r.push_tail 'listor', 'foo3' r.push_tail 'listor', 'foo4' r.push_tail 'listor', 'foo5' - + p r.pop_tail('listor') p r.pop_tail('listor') p r.pop_tail('listor') p r.pop_tail('listor') p r.pop_tail('listor') - + puts "key distribution:" - + r.ring.nodes.each do |red| p [red.port, red.keys("*")] end diff --git a/client-libraries/ruby/lib/hash_ring.rb b/client-libraries/ruby/lib/hash_ring.rb index bed86601..ec488636 100644 --- a/client-libraries/ruby/lib/hash_ring.rb +++ b/client-libraries/ruby/lib/hash_ring.rb @@ -31,8 +31,9 @@ class HashRing end def remove_node(node) + @nodes.reject!{|n| n.to_s == node.to_s} @replicas.times do |i| - key = Zlib.crc32("#{node}:#{count}") + key = Zlib.crc32("#{node}:#{i}") @ring.delete(key) @sorted_keys.reject! {|k| k == key} end diff --git a/client-libraries/ruby/lib/redis.rb b/client-libraries/ruby/lib/redis.rb index 4138e9c5..bbe5343e 100644 --- a/client-libraries/ruby/lib/redis.rb +++ b/client-libraries/ruby/lib/redis.rb @@ -36,7 +36,7 @@ class Redis "smove" => true } - BOOLEAN_PROCESSOR = lambda{|r| r == 0 ? false : r} + BOOLEAN_PROCESSOR = lambda{|r| r == 1 } REPLY_PROCESSOR = { "exists" => BOOLEAN_PROCESSOR, @@ -95,21 +95,34 @@ class Redis "type?" => "type" } + DISABLED_COMMANDS = { + "monitor" => true, + "sync" => true + } + def initialize(options = {}) @host = options[:host] || '127.0.0.1' @port = (options[:port] || 6379).to_i @db = (options[:db] || 0).to_i @timeout = (options[:timeout] || 5).to_i - $debug = options[:debug] + @password = options[:password] + @logger = options[:logger] + + @logger.info { self.to_s } if @logger connect_to_server end def to_s - "Redis Client connected to #{@host}:#{@port} against DB #{@db}" + "Redis Client connected to #{server} against DB #{@db}" + end + + def server + "#{@host}:#{@port}" end def connect_to_server @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout) + call_command(["auth",@password]) if @password call_command(["select",@db]) unless @db == 0 end @@ -147,15 +160,18 @@ class Redis end def call_command(argv) - puts argv.inspect if $debug + @logger.debug { argv.inspect } if @logger + # this wrapper to raw_call_command handle reconnection on socket # error. We try to reconnect just one time, otherwise let the error # araise. connect_to_server if !@sock + begin raw_call_command(argv.dup) rescue Errno::ECONNRESET, Errno::EPIPE @sock.close + @sock = nil connect_to_server raw_call_command(argv.dup) end @@ -176,12 +192,13 @@ class Redis bulk = nil argv[0] = argv[0].to_s.downcase argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]] + raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]] if BULK_COMMANDS[argv[0]] and argv.length > 1 bulk = argv[-1].to_s - argv[-1] = bulk.length + argv[-1] = bulk.respond_to?(:bytesize) ? bulk.bytesize : bulk.size end - command << argv.join(' ') + "\r\n" - command << bulk + "\r\n" if bulk + command << "#{argv.join(' ')}\r\n" + command << "#{bulk}\r\n" if bulk end @sock.write(command) @@ -199,7 +216,7 @@ class Redis end def [](key) - get(key) + self.get(key) end def []=(key,value) @@ -213,8 +230,8 @@ class Redis end def sort(key, options = {}) - cmd = [] - cmd << "SORT #{key}" + cmd = ["SORT"] + cmd << key cmd << "BY #{options[:by]}" if options[:by] cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get] cmd << "#{options[:order]}" if options[:order] @@ -230,6 +247,15 @@ class Redis call_command(decrement ? ["decrby",key,decrement] : ["decr",key]) end + # Similar to memcache.rb's #get_multi, returns a hash mapping + # keys to values. + def mapped_mget(*keys) + mget(*keys).inject({}) do |hash, value| + key = keys.shift + value.nil? ? hash : hash.merge(key => value) + end + end + # Ruby defines a now deprecated type method so we need to override it here # since it will never hit method_missing def type(key) diff --git a/client-libraries/ruby/redis-rb.gemspec b/client-libraries/ruby/redis-rb.gemspec index 2cd25211..04de472c 100644 --- a/client-libraries/ruby/redis-rb.gemspec +++ b/client-libraries/ruby/redis-rb.gemspec @@ -2,18 +2,18 @@ Gem::Specification.new do |s| s.name = %q{redis} - s.version = "0.0.5" + s.version = "0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark"] - #s.autorequire = %q{redis} - s.date = %q{2009-03-31} + s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark", "Brian McKinney", "Salvatore Sanfilippo", "Luca Guidi"] + # s.autorequire = %q{redis-rb} + s.date = %q{2009-06-23} s.description = %q{Ruby client library for redis key value storage server} s.email = %q{ez@engineyard.com} s.extra_rdoc_files = ["LICENSE"] - s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/redis.rb", "lib/dist_redis.rb", "lib/hash_ring.rb", "lib/pipeline.rb", "lib/server.rb", "spec/redis_spec.rb", "spec/spec_helper.rb"] + s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/dist_redis.rb", "lib/hash_ring.rb", "lib/pipeline.rb", "lib/redis.rb", "spec/redis_spec.rb", "spec/spec_helper.rb"] s.has_rdoc = true - s.homepage = %q{http://github.com/winescout/redis-rb} + s.homepage = %q{http://github.com/ezmobius/redis-rb} s.require_paths = ["lib"] s.rubygems_version = %q{1.3.1} s.summary = %q{Ruby client library for redis key value storage server} diff --git a/client-libraries/ruby/spec/redis_spec.rb b/client-libraries/ruby/spec/redis_spec.rb index 4ffa6b66..0da249e1 100644 --- a/client-libraries/ruby/spec/redis_spec.rb +++ b/client-libraries/ruby/spec/redis_spec.rb @@ -1,15 +1,16 @@ require File.dirname(__FILE__) + '/spec_helper' +require 'logger' class Foo attr_accessor :bar def initialize(bar) @bar = bar end - + def ==(other) @bar == other.bar end -end +end describe "redis" do before(:all) do @@ -23,34 +24,41 @@ describe "redis" do after(:each) do @r.keys('*').each {|k| @r.del k} - end + end after(:all) do @r.quit - end + end it "should be able connect without a timeout" do lambda { Redis.new :timeout => 0 }.should_not raise_error end + it "should be able to provide a logger" do + log = StringIO.new + r = Redis.new :db => 15, :logger => Logger.new(log) + r.ping + log.string.should include("ping") + end + it "should be able to PING" do - @r.ping.should == 'PONG' + @r.ping.should == 'PONG' end it "should be able to GET a key" do @r['foo'].should == 'bar' end - + it "should be able to SET a key" do @r['foo'] = 'nik' @r['foo'].should == 'nik' end - + it "should properly handle trailing newline characters" do @r['foo'] = "bar\n" @r['foo'].should == "bar\n" end - + it "should store and retrieve all possible characters at the beginning and the end of a string" do (0..255).each do |char_idx| string = "#{char_idx.chr}---#{char_idx.chr}" @@ -58,7 +66,7 @@ describe "redis" do @r['foo'].should == string end end - + it "should be able to SET a key with an expiry" do @r.set('foo', 'bar', 1) @r['foo'].should == 'bar' @@ -82,7 +90,7 @@ describe "redis" do @r.getset('foo', 'baz').should == 'bar' @r['foo'].should == 'baz' end - # + # it "should be able to INCR a key" do @r.del('counter') @r.incr('counter').should == 1 @@ -105,11 +113,11 @@ describe "redis" do @r.decr('counter').should == 2 @r.decr('counter', 2).should == 0 end - # + # it "should be able to RANDKEY" do @r.randkey.should_not be_nil end - # + # it "should be able to RENAME a key" do @r.del 'foo' @r.del'bar' @@ -117,7 +125,7 @@ describe "redis" do @r.rename 'foo', 'bar' @r['bar'].should == 'hi' end - # + # it "should be able to RENAMENX a key" do @r.del 'foo' @r.del 'bar' @@ -150,7 +158,7 @@ describe "redis" do @r.del 'foo' @r.exists('foo').should be_false end - # + # it "should be able to KEYS" do @r.keys("f*").each { |key| @r.del key } @r['f'] = 'nik' @@ -162,14 +170,14 @@ describe "redis" do it "should be able to return a random key (RANDOMKEY)" do 3.times { @r.exists(@r.randomkey).should be_true } end - #BTM - TODO + # it "should be able to check the TYPE of a key" do @r['foo'] = 'nik' @r.type('foo').should == "string" @r.del 'foo' @r.type('foo').should == "none" end - # + # it "should be able to push to the head of a list (LPUSH)" do @r.lpush "list", 'hello' @r.lpush "list", 42 @@ -177,13 +185,13 @@ describe "redis" do @r.llen('list').should == 2 @r.lpop('list').should == '42' end - # + # it "should be able to push to the tail of a list (RPUSH)" do @r.rpush "list", 'hello' @r.type('list').should == "list" @r.llen('list').should == 1 end - # + # it "should be able to pop the tail of a list (RPOP)" do @r.rpush "list", 'hello' @r.rpush"list", 'goodbye' @@ -191,7 +199,7 @@ describe "redis" do @r.llen('list').should == 2 @r.rpop('list').should == 'goodbye' end - # + # it "should be able to pop the head of a list (LPOP)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @@ -199,14 +207,14 @@ describe "redis" do @r.llen('list').should == 2 @r.lpop('list').should == 'hello' end - # + # it "should be able to get the length of a list (LLEN)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.type('list').should == "list" @r.llen('list').should == 2 end - # + # it "should be able to get a range of values from a list (LRANGE)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @@ -217,7 +225,7 @@ describe "redis" do @r.llen('list').should == 5 @r.lrange('list', 2, -1).should == ['1', '2', '3'] end - # + # it "should be able to trim a list (LTRIM)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @@ -230,7 +238,7 @@ describe "redis" do @r.llen('list').should == 2 @r.lrange('list', 0, -1).should == ['hello', 'goodbye'] end - # + # it "should be able to get a value by indexing into a list (LINDEX)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @@ -238,7 +246,7 @@ describe "redis" do @r.llen('list').should == 2 @r.lindex('list', 1).should == 'goodbye' end - # + # it "should be able to set a value by indexing into a list (LSET)" do @r.rpush "list", 'hello' @r.rpush "list", 'hello' @@ -247,7 +255,7 @@ describe "redis" do @r.lset('list', 1, 'goodbye').should == 'OK' @r.lindex('list', 1).should == 'goodbye' end - # + # it "should be able to remove values from a list (LREM)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @@ -256,7 +264,7 @@ describe "redis" do @r.lrem('list', 1, 'hello').should == 1 @r.lrange('list', 0, -1).should == ['goodbye'] end - # + # it "should be able add members to a set (SADD)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @@ -264,7 +272,7 @@ describe "redis" do @r.scard('set').should == 2 @r.smembers('set').sort.should == ['key1', 'key2'].sort end - # + # it "should be able delete members to a set (SREM)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @@ -275,14 +283,14 @@ describe "redis" do @r.scard('set').should == 1 @r.smembers('set').should == ['key2'] end - # + # it "should be able count the members of a set (SCARD)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.type('set').should == "set" @r.scard('set').should == 2 end - # + # it "should be able test for set membership (SISMEMBER)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @@ -292,14 +300,14 @@ describe "redis" do @r.sismember('set', 'key2').should be_true @r.sismember('set', 'notthere').should be_false end - # + # it "should be able to do set intersection (SINTER)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.sadd "set2", 'key2' @r.sinter('set', 'set2').should == ['key2'] end - # + # it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @@ -315,7 +323,7 @@ describe "redis" do @r.sadd "set2", 'key3' @r.sunion('set', 'set2').sort.should == ['key1','key2','key3'].sort end - # + # it "should be able to do set union and store the results in a key (SUNIONSTORE)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @@ -324,7 +332,7 @@ describe "redis" do @r.sunionstore('newone', 'set', 'set2').should == 3 @r.smembers('newone').sort.should == ['key1','key2','key3'].sort end - # + # it "should be able to do set difference (SDIFF)" do @r.sadd "set", 'a' @r.sadd "set", 'b' @@ -332,7 +340,7 @@ describe "redis" do @r.sadd "set2", 'c' @r.sdiff('set', 'set2').should == ['a'] end - # + # it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do @r.sadd "set", 'a' @r.sadd "set", 'b' @@ -341,27 +349,28 @@ describe "redis" do @r.sdiffstore('newone', 'set', 'set2') @r.smembers('newone').should == ['a'] end - # + # it "should be able move elements from one set to another (SMOVE)" do @r.sadd 'set1', 'a' @r.sadd 'set1', 'b' @r.sadd 'set2', 'x' - @r.smove('set1', 'set2', 'a').should be_true + @r.smove('set1', 'set2', 'a').should be_true @r.sismember('set2', 'a').should be_true @r.delete('set1') end # it "should be able to do crazy SORT queries" do + # The 'Dogs' is capitialized on purpose @r['dog_1'] = 'louie' - @r.rpush 'dogs', 1 + @r.rpush 'Dogs', 1 @r['dog_2'] = 'lucy' - @r.rpush 'dogs', 2 + @r.rpush 'Dogs', 2 @r['dog_3'] = 'max' - @r.rpush 'dogs', 3 + @r.rpush 'Dogs', 3 @r['dog_4'] = 'taj' - @r.rpush 'dogs', 4 - @r.sort('dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie'] - @r.sort('dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj'] + @r.rpush 'Dogs', 4 + @r.sort('Dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie'] + @r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj'] end it "should be able to handle array of :get using SORT" do @@ -380,13 +389,13 @@ describe "redis" do @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]).should == ['louie', 'mutt'] @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha').should == ['taj', 'terrier'] end - # + # it "should provide info (INFO)" do [: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| @r.info.keys.should include(x) end end - # + # it "should be able to flush the database (FLUSHDB)" do @r['key1'] = 'keyone' @r['key2'] = 'keytwo' @@ -404,22 +413,37 @@ describe "redis" do Time.at(savetime).class.should == Time Time.at(savetime).should <= Time.now end - + it "should be able to MGET keys" do @r['foo'] = 1000 @r['bar'] = 2000 @r.mget('foo', 'bar').should == ['1000', '2000'] @r.mget('foo', 'bar', 'baz').should == ['1000', '2000', nil] end - + + it "should be able to mapped MGET keys" do + @r['foo'] = 1000 + @r['bar'] = 2000 + @r.mapped_mget('foo', 'bar').should == { 'foo' => '1000', 'bar' => '2000'} + @r.mapped_mget('foo', 'baz', 'bar').should == { 'foo' => '1000', 'bar' => '2000'} + end + it "should bgsave" do @r.bgsave.should == 'OK' end - - it "should should be able to ECHO" do + + it "should be able to ECHO" do @r.echo("message in a bottle\n").should == "message in a bottle\n" end + it "should raise error when invoke MONITOR" do + lambda { @r.monitor }.should raise_error + end + + it "should raise error when invoke SYNC" do + lambda { @r.sync }.should raise_error + end + it "should handle multiple servers" do require 'dist_redis' @r = DistRedis.new(:hosts=> ['localhost:6379', '127.0.0.1:6379'], :db => 15) @@ -438,10 +462,17 @@ describe "redis" do pipeline.lpush 'list', "hello" pipeline.lpush 'list', 42 end - + @r.type('list').should == "list" @r.llen('list').should == 2 @r.lpop('list').should == '42' end - + + it "should AUTH when connecting with a password" do + r = Redis.new(:password => 'secret') + r.stub!(:connect_to) + r.should_receive(:call_command).with(['auth', 'secret']) + r.connect_to_server + end + end diff --git a/client-libraries/ruby/spec/spec_helper.rb b/client-libraries/ruby/spec/spec_helper.rb index 55c7855c..da70fe70 100644 --- a/client-libraries/ruby/spec/spec_helper.rb +++ b/client-libraries/ruby/spec/spec_helper.rb @@ -1,4 +1,4 @@ require 'rubygems' $TESTING=true -$:.push File.join(File.dirname(__FILE__), '..', 'lib') +$:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'redis' diff --git a/client-libraries/ruby/tasks/redis.tasks.rb b/client-libraries/ruby/tasks/redis.tasks.rb index 580af215..ed317d38 100644 --- a/client-libraries/ruby/tasks/redis.tasks.rb +++ b/client-libraries/ruby/tasks/redis.tasks.rb @@ -1,5 +1,6 @@ # Inspired by rabbitmq.rake the Redbox project at http://github.com/rick/redbox/tree/master require 'fileutils' +require 'open-uri' class RedisRunner @@ -106,10 +107,8 @@ namespace :dtach do unless File.exists?('/tmp/dtach-0.8.tar.gz') require 'net/http' - Net::HTTP.start('superb-west.dl.sourceforge.net') do |http| - resp = http.get('/sourceforge/dtach/dtach-0.8.tar.gz') - open('/tmp/dtach-0.8.tar.gz', 'wb') do |file| file.write(resp.body) end - end + url = 'http://downloads.sourceforge.net/project/dtach/dtach/0.8/dtach-0.8.tar.gz' + open('/tmp/dtach-0.8.tar.gz', 'wb') do |file| file.write(open(url).read) end end unless File.directory?('/tmp/dtach-0.8') @@ -123,4 +122,4 @@ namespace :dtach do puts 'Dtach successfully installed to /usr/bin.' end end - \ No newline at end of file +