From d7fc9edb182d361ac5c31b21fcdd25345c972898 Mon Sep 17 00:00:00 2001 From: antirez Date: Tue, 26 May 2009 18:10:50 +0200 Subject: [PATCH] client libraries synched in git --- Changelog | 3 + client-libraries/cpp/TODO | 21 +- client-libraries/cpp/redisclient.cpp | 24 +- client-libraries/cpp/redisclient.h | 2 +- client-libraries/lua/redis.lua | 2 +- client-libraries/ruby/.gitignore | 3 +- client-libraries/ruby/README.markdown | 7 +- client-libraries/ruby/Rakefile | 11 +- client-libraries/ruby/benchmarking/suite.rb | 24 ++ client-libraries/ruby/benchmarking/worker.rb | 71 ++++ client-libraries/ruby/lib/pipeline.rb | 9 +- client-libraries/ruby/lib/redis.rb | 334 ++++++++----------- client-libraries/ruby/lib/server.rb | 67 ++-- client-libraries/ruby/redis-rb.gemspec | 2 +- client-libraries/ruby/spec/redis_spec.rb | 83 +++-- client-libraries/ruby/spec/server_spec.rb | 22 ++ client-libraries/ruby/tasks/redis.tasks.rb | 2 +- redis.c | 2 +- 18 files changed, 384 insertions(+), 305 deletions(-) create mode 100644 client-libraries/ruby/benchmarking/suite.rb create mode 100644 client-libraries/ruby/benchmarking/worker.rb create mode 100644 client-libraries/ruby/spec/server_spec.rb diff --git a/Changelog b/Changelog index 4a26c5a2..94d43a3d 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,6 @@ +2009-05-26 ignore gcc warning about write() return code not checked. It is esplicitily this way since the "max number of clients reached" is a best-effort error +2009-05-26 max bytes of a received command enlarged from 1k to 16k +2009-05-26 RubyRedis: set TCP_NODELAY TCP socket option to to disable the neagle algorithm. Makes a huge difference under some OS, notably Linux 2009-05-25 maxclients implemented, see redis.conf for details 2009-05-25 INFO command now reports replication info 2009-05-25 minor fix to RubyRedis about bulk commands sent without arguments diff --git a/client-libraries/cpp/TODO b/client-libraries/cpp/TODO index 55967cf4..3e84754d 100644 --- a/client-libraries/cpp/TODO +++ b/client-libraries/cpp/TODO @@ -1,9 +1,16 @@ -+ finish command implementations -= finish unit tests - Only a few left, to test the SORT command's edge cases (e.g. BY pattern) -+ determine if we should not use bool return values and instead throw redis_error. (latter). -+ maybe more fine-grained exceptions (not just redis_error but operation_not_permitted_error, etc.) -- benchmarking +command handlers: +- support DEL as vararg +- support MLLEN and MSCARD + +unit tests: +- sort with limit +- sort lexicographically +- sort with pattern and weights + +extras: +- benchmarking "test" app - consistent hashing? -- make all string literals constants so they can be easily changed (minor) + +maybe/someday: +- make all string literals constants so they can be easily changed - add conveniences that store a std::set in its entirety (same for std::list, std::vector) diff --git a/client-libraries/cpp/redisclient.cpp b/client-libraries/cpp/redisclient.cpp index 75e6e878..beaa876a 100644 --- a/client-libraries/cpp/redisclient.cpp +++ b/client-libraries/cpp/redisclient.cpp @@ -663,16 +663,24 @@ namespace redis const client::string_type & by_pattern, client::int_type limit_start, client::int_type limit_end, - const client::string_type & get_pattern, + const client::string_vector & get_patterns, client::sort_order order, bool lexicographically) { - send_(makecmd("SORT") << key - << " BY " << by_pattern - << " LIMIT " << limit_start << ' ' << limit_end - << " GET " << get_pattern - << (order == sort_order_ascending ? " ASC" : " DESC") - << (lexicographically ? " ALPHA" : "")); + makecmd m("SORT"); + + m << key + << " BY " << by_pattern + << " LIMIT " << limit_start << ' ' << limit_end; + + client::string_vector::const_iterator it = get_patterns.begin(); + for ( ; it != get_patterns.end(); ++it) + m << " GET " << *it; + + m << (order == sort_order_ascending ? " ASC" : " DESC") + << (lexicographically ? " ALPHA" : ""); + + send_(m); return recv_multi_bulk_reply_(out); } @@ -681,7 +689,7 @@ namespace redis { send_(makecmd("SAVE", true)); recv_ok_reply_(); - e.g. } + } void client::bgsave() { diff --git a/client-libraries/cpp/redisclient.h b/client-libraries/cpp/redisclient.h index b1fbb582..d3fee780 100644 --- a/client-libraries/cpp/redisclient.h +++ b/client-libraries/cpp/redisclient.h @@ -421,7 +421,7 @@ namespace redis const string_type & by_pattern, int_type limit_start, int_type limit_end, - const string_type & get_pattern, + const string_vector & get_patterns, sort_order order = sort_order_ascending, bool lexicographically = false); diff --git a/client-libraries/lua/redis.lua b/client-libraries/lua/redis.lua index 2455ceb3..87351745 100644 --- a/client-libraries/lua/redis.lua +++ b/client-libraries/lua/redis.lua @@ -316,7 +316,7 @@ redis_commands = { function(client, command) -- let's fire and forget! the connection is closed as soon -- as the SHUTDOWN command is received by the server. - network.write(command .. protocol.newline) + network.write(client, command .. protocol.newline) end ), diff --git a/client-libraries/ruby/.gitignore b/client-libraries/ruby/.gitignore index 10d0977b..2fd676b3 100644 --- a/client-libraries/ruby/.gitignore +++ b/client-libraries/ruby/.gitignore @@ -1,4 +1,5 @@ nohup.out redis/* rdsrv -pkg/* \ No newline at end of file +pkg/* +.idea diff --git a/client-libraries/ruby/README.markdown b/client-libraries/ruby/README.markdown index 2518c421..b36633d6 100644 --- a/client-libraries/ruby/README.markdown +++ b/client-libraries/ruby/README.markdown @@ -12,7 +12,10 @@ See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for m ## Dependencies -1. redis - +1. rspec - + sudo gem install rspec + +2. redis - rake redis:install @@ -20,7 +23,7 @@ See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for m rake dtach:install -3. svn - git is the new black, but we need it for the google codes. +3. git - git is the new black. ## Setup diff --git a/client-libraries/ruby/Rakefile b/client-libraries/ruby/Rakefile index bdc9f373..9bed311a 100644 --- a/client-libraries/ruby/Rakefile +++ b/client-libraries/ruby/Rakefile @@ -8,10 +8,10 @@ require 'tasks/redis.tasks' GEM = 'redis' GEM_NAME = 'redis' -GEM_VERSION = '0.0.3.3' +GEM_VERSION = '0.0.3.4' AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark'] -EMAIL = "matt.clark@punchstock.com" -HOMEPAGE = "http://github.com/winescout/redis-rb" +EMAIL = "ez@engineyard.com" +HOMEPAGE = "http://github.com/ezmobius/redis-rb" SUMMARY = "Ruby client library for redis key value storage server" spec = Gem::Specification.new do |s| @@ -25,10 +25,7 @@ spec = Gem::Specification.new do |s| s.authors = AUTHORS s.email = EMAIL s.homepage = HOMEPAGE - - # Uncomment this to add a dependency - # s.add_dependency "foo" - + s.add_dependency "rspec" s.require_path = 'lib' s.autorequire = GEM s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,spec}/**/*") diff --git a/client-libraries/ruby/benchmarking/suite.rb b/client-libraries/ruby/benchmarking/suite.rb new file mode 100644 index 00000000..f03694b0 --- /dev/null +++ b/client-libraries/ruby/benchmarking/suite.rb @@ -0,0 +1,24 @@ +require 'fileutils' + +def run_in_background(command) + fork { system command } +end + +def with_all_segments(&block) + 0.upto(9) do |segment_number| + block_size = 100000 + start_index = segment_number * block_size + end_index = start_index + block_size - 1 + block.call(start_index, end_index) + end +end + +#with_all_segments do |start_index, end_index| +# puts "Initializing keys from #{start_index} to #{end_index}" +# system "ruby worker.rb initialize #{start_index} #{end_index} 0" +#end + +with_all_segments do |start_index, end_index| + run_in_background "ruby worker.rb write #{start_index} #{end_index} 10" + run_in_background "ruby worker.rb read #{start_index} #{end_index} 1" +end \ No newline at end of file diff --git a/client-libraries/ruby/benchmarking/worker.rb b/client-libraries/ruby/benchmarking/worker.rb new file mode 100644 index 00000000..836d03b0 --- /dev/null +++ b/client-libraries/ruby/benchmarking/worker.rb @@ -0,0 +1,71 @@ +BENCHMARK_ROOT = File.dirname(__FILE__) +REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib") + +$: << REDIS_ROOT +require 'redis' +require 'benchmark' + +def show_usage + puts <<-EOL + Usage: worker.rb [read:write] + EOL +end + +def shift_from_argv + value = ARGV.shift + unless value + show_usage + exit -1 + end + value +end + +operation = shift_from_argv.to_sym +start_index = shift_from_argv.to_i +end_index = shift_from_argv.to_i +sleep_msec = shift_from_argv.to_i +sleep_duration = sleep_msec/1000.0 + +redis = Redis.new + +case operation + when :initialize + + start_index.upto(end_index) do |i| + redis[i] = 0 + end + + when :clear + + start_index.upto(end_index) do |i| + redis.delete(i) + end + + when :read, :write + + puts "Starting to #{operation} at segment #{end_index + 1}" + + loop do + t1 = Time.now + start_index.upto(end_index) do |i| + case operation + when :read + redis.get(i) + when :write + redis.incr(i) + else + raise "Unknown operation: #{operation}" + end + sleep sleep_duration + end + t2 = Time.now + + requests_processed = end_index - start_index + time = t2 - t1 + puts "#{t2.strftime("%H:%M")} [segment #{end_index + 1}] : Processed #{requests_processed} requests in #{time} seconds - #{(requests_processed/time).round} requests/sec" + end + + else + raise "Unknown operation: #{operation}" +end + diff --git a/client-libraries/ruby/lib/pipeline.rb b/client-libraries/ruby/lib/pipeline.rb index deaedd15..f92b96db 100644 --- a/client-libraries/ruby/lib/pipeline.rb +++ b/client-libraries/ruby/lib/pipeline.rb @@ -9,10 +9,7 @@ class Redis @commands = [] end - def get_response - end - - def write(data) + def execute_command(data) @commands << data write_and_read if @commands.size >= BUFFER_SIZE end @@ -22,10 +19,10 @@ class Redis end def write_and_read - @redis.write @commands.join + @redis.execute_command(@commands.join, true) @redis.read_socket @commands.clear end end -end \ No newline at end of file +end diff --git a/client-libraries/ruby/lib/redis.rb b/client-libraries/ruby/lib/redis.rb index b27918bd..b10c42bf 100644 --- a/client-libraries/ruby/lib/redis.rb +++ b/client-libraries/ruby/lib/redis.rb @@ -3,14 +3,15 @@ require 'set' require File.join(File.dirname(__FILE__),'server') require File.join(File.dirname(__FILE__),'pipeline') - class RedisError < StandardError end class RedisRenameError < StandardError end + class Redis ERR = "-".freeze OK = 'OK'.freeze + PONG = 'PONG'.freeze SINGLE = '+'.freeze BULK = '$'.freeze MULTI = '*'.freeze @@ -18,22 +19,21 @@ class Redis attr_reader :server - def initialize(opts={}) @opts = {:host => 'localhost', :port => '6379', :db => 0}.merge(opts) $debug = @opts[:debug] @db = @opts[:db] @server = Server.new(@opts[:host], @opts[:port], (@opts[:timeout]||10)) end - + def pipelined pipeline = Pipeline.new(self) yield pipeline pipeline.finish end - + def to_s - "#{host}:#{port}" + "#{host}:#{port} -> #{@db}" end def port @@ -44,76 +44,42 @@ class Redis @opts[:host] end - def with_socket_management(server, &block) - begin - socket = server.socket - block.call(socket) - #Timeout or server down - rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED, Timeout::Error => e - server.close - puts "Client (#{server.inspect}) disconnected from server: #{e.inspect}\n" if $debug - retry - #Server down - rescue NoMethodError => e - puts "Client (#{server.inspect}) tryin server that is down: #{e.inspect}\n Dying!" if $debug - raise Errno::ECONNREFUSED - #exit - end + def quit + execute_command("QUIT\r\n", true) end - def monitor - with_socket_management(@server) do |socket| - trap("INT") { puts "\nGot ^C! Dying!"; exit } - write "MONITOR\r\n" - puts "Now Monitoring..." - socket.read(12) - loop do - x = socket.gets - puts x unless x.nil? - end - end + def ping + execute_command("PING\r\n") == PONG end - def quit - write "QUIT\r\n" - end - def select_db(index) @db = index - write "SELECT #{index}\r\n" - get_response + execute_command("SELECT #{index}\r\n") end def flush_db - write "FLUSHDB\r\n" - get_response == OK + execute_command("FLUSHDB\r\n") == OK end def flush_all - ensure_retry do - puts "Warning!\nFlushing *ALL* databases!\n5 Seconds to Hit ^C!" - trap('INT') {quit; return false} - sleep 5 - write "FLUSHALL\r\n" - get_response == OK - end + puts "Warning!\nFlushing *ALL* databases!\n5 Seconds to Hit ^C!" + trap('INT') {quit; return false} + sleep 5 + execute_command("FLUSHALL\r\n") == OK end def last_save - write "LASTSAVE\r\n" - get_response.to_i + execute_command("LASTSAVE\r\n").to_i end def bgsave - write "BGSAVE\r\n" - get_response == OK + execute_command("BGSAVE\r\n") == OK end def info info = {} - write("INFO\r\n") - x = get_response - x.each do |kv| + x = execute_command("INFO\r\n") + x.each_line do |kv| k,v = kv.split(':', 2) k,v = k.chomp, v = v.chomp info[k.to_sym] = v @@ -121,55 +87,16 @@ class Redis info end - - def bulk_reply - begin - x = read - puts "bulk_reply read value is #{x.inspect}" if $debug - return x - rescue => e - puts "error in bulk_reply #{e}" if $debug - nil - end - end - - def write(data) - with_socket_management(@server) do |socket| - puts "writing: #{data}" if $debug - socket.write(data) - end - end - - def fetch(len) - with_socket_management(@server) do |socket| - len = [0, len.to_i].max - res = socket.read(len + 2) - res = res.chomp if res - res - end - end - - def read(length = read_proto) - with_socket_management(@server) do |socket| - res = socket.read(length) - puts "read is #{res.inspect}" if $debug - res - end - end - def keys(glob) - write "KEYS #{glob}\r\n" - get_response.split(' ') + execute_command("KEYS #{glob}\r\n").split(' ') end def rename!(oldkey, newkey) - write "RENAME #{oldkey} #{newkey}\r\n" - get_response + execute_command("RENAME #{oldkey} #{newkey}\r\n") end def rename(oldkey, newkey) - write "RENAMENX #{oldkey} #{newkey}\r\n" - case get_response + case execute_command("RENAMENX #{oldkey} #{newkey}\r\n") when -1 raise RedisRenameError, "source key: #{oldkey} does not exist" when 0 @@ -182,13 +109,11 @@ class Redis end def key?(key) - write "EXISTS #{key}\r\n" - get_response == 1 + execute_command("EXISTS #{key}\r\n") == 1 end def delete(key) - write "DEL #{key}\r\n" - get_response == 1 + execute_command("DEL #{key}\r\n") == 1 end def [](key) @@ -196,41 +121,35 @@ class Redis end def get(key) - write "GET #{key}\r\n" - get_response + execute_command("GET #{key}\r\n") end def mget(*keys) - write "MGET #{keys.join(' ')}\r\n" - get_response + execute_command("MGET #{keys.join(' ')}\r\n") end def incr(key, increment=nil) if increment - write "INCRBY #{key} #{increment}\r\n" + execute_command("INCRBY #{key} #{increment}\r\n") else - write "INCR #{key}\r\n" + execute_command("INCR #{key}\r\n") end - get_response end def decr(key, decrement=nil) if decrement - write "DECRBY #{key} #{decrement}\r\n" + execute_command("DECRBY #{key} #{decrement}\r\n") else - write "DECR #{key}\r\n" + execute_command("DECR #{key}\r\n") end - get_response end def randkey - write "RANDOMKEY\r\n" - get_response + execute_command("RANDOMKEY\r\n") end def list_length(key) - write "LLEN #{key}\r\n" - case i = get_response + case i = execute_command("LLEN #{key}\r\n") when -2 raise RedisError, "key: #{key} does not hold a list value" else @@ -239,53 +158,43 @@ class Redis end def type?(key) - write "TYPE #{key}\r\n" - get_response + execute_command("TYPE #{key}\r\n") end - def push_tail(key, string) - write "RPUSH #{key} #{string.to_s.size}\r\n#{string.to_s}\r\n" - get_response + def push_tail(key, val) + execute_command("RPUSH #{key} #{value_to_wire(val)}\r\n") end - def push_head(key, string) - write "LPUSH #{key} #{string.to_s.size}\r\n#{string.to_s}\r\n" - get_response + def push_head(key, val) + execute_command("LPUSH #{key} #{value_to_wire(val)}\r\n") end def pop_head(key) - write "LPOP #{key}\r\n" - get_response + execute_command("LPOP #{key}\r\n") end def pop_tail(key) - write "RPOP #{key}\r\n" - get_response + execute_command("RPOP #{key}\r\n") end def list_set(key, index, val) - write "LSET #{key} #{index} #{val.to_s.size}\r\n#{val}\r\n" - get_response == OK + execute_command("LSET #{key} #{index} #{value_to_wire(val)}\r\n") == OK end def list_range(key, start, ending) - write "LRANGE #{key} #{start} #{ending}\r\n" - get_response + execute_command("LRANGE #{key} #{start} #{ending}\r\n") end def list_trim(key, start, ending) - write "LTRIM #{key} #{start} #{ending}\r\n" - get_response + execute_command("LTRIM #{key} #{start} #{ending}\r\n") end def list_index(key, index) - write "LINDEX #{key} #{index}\r\n" - get_response + execute_command("LINDEX #{key} #{index}\r\n") end - def list_rm(key, count, value) - write "LREM #{key} #{count} #{value.to_s.size}\r\n#{value}\r\n" - case num = get_response + def list_rm(key, count, val) + case num = execute_command("LREM #{key} #{count} #{value_to_wire(val)}\r\n") when -1 raise RedisError, "key: #{key} does not exist" when -2 @@ -296,8 +205,7 @@ class Redis end def set_add(key, member) - write "SADD #{key} #{member.to_s.size}\r\n#{member}\r\n" - case get_response + case execute_command("SADD #{key} #{value_to_wire(member)}\r\n") when 1 true when 0 @@ -308,8 +216,7 @@ class Redis end def set_delete(key, member) - write "SREM #{key} #{member.to_s.size}\r\n#{member}\r\n" - case get_response + case execute_command("SREM #{key} #{value_to_wire(member)}\r\n") when 1 true when 0 @@ -320,8 +227,7 @@ class Redis end def set_count(key) - write "SCARD #{key}\r\n" - case i = get_response + case i = execute_command("SCARD #{key}\r\n") when -2 raise RedisError, "key: #{key} contains a non set value" else @@ -330,8 +236,7 @@ class Redis end def set_member?(key, member) - write "SISMEMBER #{key} #{member.to_s.size}\r\n#{member}\r\n" - case get_response + case execute_command("SISMEMBER #{key} #{value_to_wire(member)}\r\n") when 1 true when 0 @@ -342,57 +247,93 @@ class Redis end def set_members(key) - write "SMEMBERS #{key}\r\n" - Set.new(get_response) + Set.new(execute_command("SMEMBERS #{key}\r\n")) end def set_intersect(*keys) - write "SINTER #{keys.join(' ')}\r\n" - Set.new(get_response) + Set.new(execute_command("SINTER #{keys.join(' ')}\r\n")) end def set_inter_store(destkey, *keys) - write "SINTERSTORE #{destkey} #{keys.join(' ')}\r\n" - get_response + execute_command("SINTERSTORE #{destkey} #{keys.join(' ')}\r\n") end def set_union(*keys) - write "SUNION #{keys.join(' ')}\r\n" - Set.new(get_response) + Set.new(execute_command("SUNION #{keys.join(' ')}\r\n")) end def set_union_store(destkey, *keys) - write "SUNIONSTORE #{destkey} #{keys.join(' ')}\r\n" - get_response + execute_command("SUNIONSTORE #{destkey} #{keys.join(' ')}\r\n") end def set_diff(*keys) - write "SDIFF #{keys.join(' ')}\r\n" - Set.new(get_response) + Set.new(execute_command("SDIFF #{keys.join(' ')}\r\n")) end def set_diff_store(destkey, *keys) - write "SDIFFSTORE #{destkey} #{keys.join(' ')}\r\n" - get_response + execute_command("SDIFFSTORE #{destkey} #{keys.join(' ')}\r\n") end def set_move(srckey, destkey, member) - write "SMOVE #{srckey} #{destkey} #{member.to_s.size}\r\n#{member}\r\n" - get_response == 1 + execute_command("SMOVE #{srckey} #{destkey} #{value_to_wire(member)}\r\n") == 1 end def sort(key, opts={}) cmd = "SORT #{key}" cmd << " BY #{opts[:by]}" if opts[:by] - cmd << " GET #{opts[:get]}" if opts[:get] + cmd << " GET #{[opts[:get]].flatten * ' GET '}" if opts[:get] cmd << " INCR #{opts[:incr]}" if opts[:incr] cmd << " DEL #{opts[:del]}" if opts[:del] cmd << " DECR #{opts[:decr]}" if opts[:decr] cmd << " #{opts[:order]}" if opts[:order] cmd << " LIMIT #{opts[:limit].join(' ')}" if opts[:limit] cmd << "\r\n" - write(cmd) - get_response + execute_command(cmd) + end + + def []=(key, val) + set(key,val) + end + + def set(key, val, expiry=nil) + s = execute_command("SET #{key} #{value_to_wire(val)}\r\n") == OK + return expire(key, expiry) if s && expiry + s + end + + def dbsize + execute_command("DBSIZE\r\n") + end + + def expire(key, expiry=nil) + execute_command("EXPIRE #{key} #{expiry}\r\n") == 1 + end + + def set_unless_exists(key, val) + execute_command("SETNX #{key} #{value_to_wire(val)}\r\n") == 1 + end + + def bulk_reply + begin + x = read + puts "bulk_reply read value is #{x.inspect}" if $debug + return x + rescue => e + puts "error in bulk_reply #{e}" if $debug + nil + end + end + + def write(data) + puts "writing: #{data}" if $debug + @socket.write(data) + end + + def read(length = 0) + length = read_proto unless length > 0 + res = @socket.read(length) + puts "read is #{res.inspect}" if $debug + res end def multi_bulk @@ -418,28 +359,6 @@ class Redis r end - def []=(key, val) - set(key,val) - end - - - def set(key, val, expiry=nil) - write("SET #{key} #{val.to_s.size}\r\n#{val}\r\n") - s = get_response == OK - return expire(key, expiry) if s && expiry - s - end - - def expire(key, expiry=nil) - write("EXPIRE #{key} #{expiry}\r\n") - get_response == 1 - end - - def set_unless_exists(key, val) - write "SETNX #{key} #{val.to_s.size}\r\n#{val}\r\n" - get_response == 1 - end - def status_code_reply begin res = read_proto @@ -452,13 +371,26 @@ class Redis raise RedisError end end - + + def execute_command(command, ignore_response=false) + ss = server.socket + unless ss.object_id == @socket.object_id + @socket = ss + puts "Socket changed, selecting DB" if $debug + unless command[0..6] == 'SELECT' + #BTM - Ugh- DRY but better than infinite recursion + write("SELECT #{@db}\r\n") + get_response + end + end + write(command) + get_response unless ignore_response + rescue Errno::ECONNRESET, Errno::EPIPE, NoMethodError, Timeout::Error => e + raise RedisError, "Connection error" + end + def get_response - begin - rtype = get_reply - rescue => e - raise RedisError, e.inspect - end + rtype = get_reply puts "reply_type is #{rtype.inspect}" if $debug case rtype when SINGLE @@ -512,13 +444,21 @@ class Redis end def read_proto - with_socket_management(@server) do |socket| - if res = socket.gets - x = res.chomp - puts "read_proto is #{x.inspect}\n\n" if $debug - x.to_i - end + res = @socket.readline + x = res.chomp + puts "read_proto is #{x.inspect}\n\n" if $debug + x.to_i + end + + private + def value_to_wire(value) + value_str = value.to_s + if value_str.respond_to?(:bytesize) + value_size = value_str.bytesize + else + value_size = value_str.size end + "#{value_size}\r\n#{value_str}" end end diff --git a/client-libraries/ruby/lib/server.rb b/client-libraries/ruby/lib/server.rb index c5ac808c..4fb54937 100644 --- a/client-libraries/ruby/lib/server.rb +++ b/client-libraries/ruby/lib/server.rb @@ -24,12 +24,6 @@ end class Server - ## - # The amount of time to wait before attempting to re-establish a - # connection with a server that is marked dead. - - RETRY_DELAY = 30.0 - ## # The host the redis server is running on. @@ -40,16 +34,6 @@ class Server attr_reader :port - ## - # - - attr_reader :replica - - ## - # The time of next retry if the connection is dead. - - attr_reader :retry - ## # A text status string describing the state of the server. @@ -67,7 +51,6 @@ class Server @port = port.to_i @sock = nil - @retry = nil @status = 'NOT CONNECTED' @timeout = timeout end @@ -83,38 +66,31 @@ class Server # Returns the connected socket object on success or nil on failure. def socket - return @sock if @sock and not @sock.closed? - - @sock = nil - - # If the host was dead, don't retry for a while. - return if @retry and @retry > Time.now - + return @sock if socket_alive? + close # Attempt to connect if not already connected. begin @sock = connect_to(@host, @port, @timeout) @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 - @retry = nil @status = 'CONNECTED' rescue Errno::EPIPE, Errno::ECONNREFUSED => e - puts "Socket died... socket: #{@sock.inspect}\n" if $debug - @sock.close + puts "Socket died... : #{e}\n" if $debug retry rescue SocketError, SystemCallError, IOError => err puts "Unable to open socket: #{err.class.name}, #{err.message}" if $debug - mark_dead err end @sock end def connect_to(host, port, timeout=nil) - socket = TCPSocket.new(host, port, 0) + socket = TCPSocket.new(host, port) + socket.set_encoding(Encoding::BINARY) if socket.respond_to?(:set_encoding) if timeout socket.instance_eval <<-EOR - alias :blocking_gets :gets - def gets(*args) + alias :blocking_readline :readline + def readline(*args) RedisTimer.timeout(#{timeout}) do - self.blocking_gets(*args) + self.blocking_readline(*args) end end alias :blocking_read :read @@ -134,27 +110,22 @@ class Server socket end - ## # Close the connection to the redis server targeted by this - # object. The server is not considered dead. + # object. def close - @sock.close if @sock && !@sock.closed? + @sock.close if !@sock.nil? && !@sock.closed? @sock = nil - @retry = nil @status = "NOT CONNECTED" end - ## - # Mark the server as dead and close its socket. - def mark_dead(error) - @sock.close if @sock && !@sock.closed? - @sock = nil - @retry = Time.now #+ RETRY_DELAY - - reason = "#{error.class.name}: #{error.message}" - @status = sprintf "%s:%s DEAD (%s), will retry at %s", @host, @port, reason, @retry - puts @status - end - + private + def socket_alive? + #BTM - TODO - FileStat is borked under JRuby + unless defined?(JRUBY_VERSION) + !@sock.nil? && !@sock.closed? && @sock.stat.readable? + else + !@sock.nil? && !@sock.closed? + end + end end diff --git a/client-libraries/ruby/redis-rb.gemspec b/client-libraries/ruby/redis-rb.gemspec index 5e284b30..e8d8b6eb 100644 --- a/client-libraries/ruby/redis-rb.gemspec +++ b/client-libraries/ruby/redis-rb.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = %q{redis} - s.version = "0.0.3.4" + s.version = "0.0.3.5" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark"] diff --git a/client-libraries/ruby/spec/redis_spec.rb b/client-libraries/ruby/spec/redis_spec.rb index 71a63259..de302f34 100644 --- a/client-libraries/ruby/spec/redis_spec.rb +++ b/client-libraries/ruby/spec/redis_spec.rb @@ -13,8 +13,8 @@ end describe "redis" do before(:all) do - @r = Redis.new - @r.select_db(15) # use database 15 for testing so we dont accidentally step on you real data + # use database 15 for testing so we dont accidentally step on you real data + @r = Redis.new :db => 15 end before(:each) do @@ -29,6 +29,9 @@ describe "redis" do @r.quit end + it 'should be able to PING' do + @r.ping.should == true + end it "should be able to GET a key" do @r['foo'].should == 'bar' @@ -102,7 +105,16 @@ describe "redis" do lambda {@r.rename 'foo', 'bar'}.should raise_error(RedisRenameError) @r['bar'].should == 'ohai' end - # + # + it "should be able to get DBSIZE of the database" do + @r.delete 'foo' + dbsize_without_foo = @r.dbsize + @r['foo'] = 0 + dbsize_with_foo = @r.dbsize + + dbsize_with_foo.should == dbsize_without_foo + 1 + end + # it "should be able to EXPIRE a key" do @r['foo'] = 'bar' @r.expire('foo', 1) @@ -287,6 +299,7 @@ describe "redis" do @r.set_inter_store('newone', 'set', 'set2').should == 'OK' @r.set_members('newone').should == Set.new(['key2']) @r.delete('set') + @r.delete('set2') end # it "should be able to do set union" do @@ -296,6 +309,7 @@ describe "redis" do @r.set_add "set2", 'key3' @r.set_union('set', 'set2').should == Set.new(['key1','key2','key3']) @r.delete('set') + @r.delete('set2') end # it "should be able to do set union and store the results in a key" do @@ -306,28 +320,29 @@ describe "redis" do @r.set_union_store('newone', 'set', 'set2').should == 'OK' @r.set_members('newone').should == Set.new(['key1','key2','key3']) @r.delete('set') + @r.delete('set2') end - - # these don't seem to be implemented in redis head? - # it "should be able to do set difference" do - # @r.set_add "set", 'key1' - # @r.set_add "set", 'key2' - # @r.set_add "set2", 'key2' - # @r.set_add "set2", 'key3' - # @r.set_diff('set', 'set2').should == Set.new(['key1','key3']) - # @r.delete('set') - # end - # # - # it "should be able to do set difference and store the results in a key" do - # @r.set_add "set", 'key1' - # @r.set_add "set", 'key2' - # @r.set_add "set2", 'key2' - # @r.set_add "set2", 'key3' - # count = @r.set_diff_store('newone', 'set', 'set2') - # count.should == 3 - # @r.set_members('newone').should == Set.new(['key1','key3']) - # @r.delete('set') - # end + # + it "should be able to do set difference" do + @r.set_add "set", 'a' + @r.set_add "set", 'b' + @r.set_add "set2", 'b' + @r.set_add "set2", 'c' + @r.set_diff('set', 'set2').should == Set.new(['a']) + @r.delete('set') + @r.delete('set2') + end + # + it "should be able to do set difference and store the results in a key" do + @r.set_add "set", 'a' + @r.set_add "set", 'b' + @r.set_add "set2", 'b' + @r.set_add "set2", 'c' + @r.set_diff_store('newone', 'set', 'set2') + @r.set_members('newone').should == Set.new(['a']) + @r.delete('set') + @r.delete('set2') + end # it "should be able move elements from one set to another" do @r.set_add 'set1', 'a' @@ -350,6 +365,23 @@ describe "redis" do @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 + @r['dog:1:name'] = 'louie' + @r['dog:1:breed'] = 'mutt' + @r.push_tail 'dogs', 1 + @r['dog:2:name'] = 'lucy' + @r['dog:2:breed'] = 'poodle' + @r.push_tail 'dogs', 2 + @r['dog:3:name'] = 'max' + @r['dog:3:breed'] = 'hound' + @r.push_tail 'dogs', 3 + @r['dog:4:name'] = 'taj' + @r['dog:4:breed'] = 'terrier' + @r.push_tail 'dogs', 4 + @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" 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| @@ -407,4 +439,7 @@ describe "redis" do @r.pop_head('list').should == '42' @r.delete('list') end + + it "should select db on connection" + it "should re-select db on reconnection" end diff --git a/client-libraries/ruby/spec/server_spec.rb b/client-libraries/ruby/spec/server_spec.rb new file mode 100644 index 00000000..cb2beb56 --- /dev/null +++ b/client-libraries/ruby/spec/server_spec.rb @@ -0,0 +1,22 @@ +require File.dirname(__FILE__) + '/spec_helper' + +describe "Server" do + before(:each) do + @server = Server.new 'localhost', '6379' + end + + it "should checkout active connections" do + threads = [] + 10.times do + threads << Thread.new do + lambda { + socket = @server.socket + socket.close + socket.write("INFO\r\n") + socket.read(1) + }.should_not raise_error(Exception) + end + end + end + +end diff --git a/client-libraries/ruby/tasks/redis.tasks.rb b/client-libraries/ruby/tasks/redis.tasks.rb index 657d248d..580af215 100644 --- a/client-libraries/ruby/tasks/redis.tasks.rb +++ b/client-libraries/ruby/tasks/redis.tasks.rb @@ -64,7 +64,7 @@ namespace :redis do RedisRunner.attach end - desc 'Install the lastest redis from svn' + desc 'Install the lastest verison of Redis from Github (requires git, duh)' task :install => [:about, :download, :make] do %w(redis-benchmark redis-cli redis-server).each do |bin| sh "sudo cp /tmp/redis/#{bin} /usr/bin/" diff --git a/redis.c b/redis.c index 5ced9a29..98371ed4 100644 --- a/redis.c +++ b/redis.c @@ -1528,7 +1528,7 @@ static void acceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) { char *err = "-ERR max number of clients reached\r\n"; /* That's a best effort error message, don't check write errors */ - (void)write(c->fd,err,strlen(err)); + (void) write(c->fd,err,strlen(err)); freeClient(c); return; } -- 2.45.2