Ruby client library updated. Important changes in this new version!

This commit is contained in:
antirez 2009-03-27 12:14:35 +01:00
parent 1a4601492c
commit 29fac6170a
14 changed files with 739 additions and 850 deletions

3
client-libraries/ruby/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
nohup.out
redis/*
rdsrv

View File

@ -1,12 +0,0 @@
== redis
A ruby client library for the redis key value storage system:
http://code.google.com/p/redis/wiki/README
redis is a key value store with some interesting features:
1. fast
2. keys are strings but values can have types of "NONE","STRING","LIST","SET"
list's can be atomicaly push'd, pop'd and lpush'd, lpop'd and indexed so you
can store things like lists of comments under one key and still be able to
append comments without reading and putting back the whole list.

View File

@ -7,7 +7,7 @@ require 'tasks/redis.tasks'
GEM = 'redis'
GEM_VERSION = '0.0.2'
GEM_VERSION = '0.0.3'
AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley']
EMAIL = "ez@engineyard.com"
HOMEPAGE = "http://github.com/ezmobius/redis-rb"

View File

@ -0,0 +1,13 @@
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

View File

@ -1,11 +0,0 @@
require 'benchmark'
$:.push File.join(File.dirname(__FILE__), 'lib')
require 'redis'
times = 20000
@r = Redis.new
(0..1000000).each{|x|
@r[x] = "Hello World"
puts x if (x > 0 and x % 10000) == 0
}

View File

@ -163,6 +163,9 @@ end
# Another name for Timeout::Error, defined for backwards compatibility with
# earlier versions of timeout.rb.
class Object
remove_const(:TimeoutError) if const_defined?(:TimeoutError)
end
TimeoutError = Timeout::Error # :nodoc:
if __FILE__ == $0

View File

@ -24,7 +24,7 @@ class DistRedis
end
def method_missing(sym, *args, &blk)
if redis = node_for_key(args.first)
if redis = node_for_key(args.first.to_s)
redis.send sym, *args, &blk
else
super
@ -94,11 +94,11 @@ r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localho
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'
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:"

View File

@ -1,10 +1,15 @@
require 'digest/md5'
require 'zlib'
class HashRing
POINTS_PER_SERVER = 160 # this is the default in libmemcached
attr_reader :ring, :sorted_keys, :replicas, :nodes
# nodes is a list of objects that have a proper to_s representation.
# replicas indicates how many virtual points should be used pr. node,
# replicas are required to improve the distribution.
def initialize(nodes=[], replicas=3)
def initialize(nodes=[], replicas=POINTS_PER_SERVER)
@replicas = replicas
@ring = {}
@nodes = []
@ -18,7 +23,7 @@ class HashRing
def add_node(node)
@nodes << node
@replicas.times do |i|
key = gen_key("#{node}:#{i}")
key = Zlib.crc32("#{node}:#{i}")
@ring[key] = node
@sorted_keys << key
end
@ -27,7 +32,7 @@ class HashRing
def remove_node(node)
@replicas.times do |i|
key = gen_key("#{node}:#{count}")
key = Zlib.crc32("#{node}:#{count}")
@ring.delete(key)
@sorted_keys.reject! {|k| k == key}
end
@ -40,15 +45,9 @@ class HashRing
def get_node_pos(key)
return [nil,nil] if @ring.size == 0
key = gen_key(key)
nodes = @sorted_keys
nodes.size.times do |i|
node = nodes[i]
if key <= node
return [@ring[node], i]
end
end
[@ring[nodes[0]], 0]
crc = Zlib.crc32(key)
idx = HashRing.binary_search(@sorted_keys, crc)
return [@ring[@sorted_keys[idx]], idx]
end
def iter_nodes(key)
@ -59,11 +58,66 @@ class HashRing
end
end
def gen_key(key)
key = Digest::MD5.hexdigest(key)
((key[3] << 24) | (key[2] << 16) | (key[1] << 8) | key[0])
class << self
# gem install RubyInline to use this code
# Native extension to perform the binary search within the hashring.
# There's a pure ruby version below so this is purely optional
# for performance. In testing 20k gets and sets, the native
# binary search shaved about 12% off the runtime (9sec -> 8sec).
begin
require 'inline'
inline do |builder|
builder.c <<-EOM
int binary_search(VALUE ary, unsigned int r) {
int upper = RARRAY_LEN(ary) - 1;
int lower = 0;
int idx = 0;
while (lower <= upper) {
idx = (lower + upper) / 2;
VALUE continuumValue = RARRAY_PTR(ary)[idx];
unsigned int l = NUM2UINT(continuumValue);
if (l == r) {
return idx;
}
else if (l > r) {
upper = idx - 1;
}
else {
lower = idx + 1;
}
}
return upper;
}
EOM
end
rescue Exception => e
# Find the closest index in HashRing with value <= the given value
def binary_search(ary, value, &block)
upper = ary.size - 1
lower = 0
idx = 0
while(lower <= upper) do
idx = (lower + upper) / 2
comp = ary[idx] <=> value
if comp == 0
return idx
elsif comp > 0
upper = idx - 1
else
lower = idx + 1
end
end
return upper
end
end
end
end
# ring = HashRing.new ['server1', 'server2', 'server3']

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
##
# This class represents a redis server instance.
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.
attr_reader :host
##
# The port the redis server is listening on.
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.
attr_reader :status
##
# Create a new Redis::Server object for the redis instance
# listening on the given host and port.
def initialize(host, port = DEFAULT_PORT)
raise ArgumentError, "No host specified" if host.nil? or host.empty?
raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
@host = host
@port = port.to_i
@sock = nil
@retry = nil
@status = 'NOT CONNECTED'
@timeout = 1
end
##
# Return a string representation of the server object.
def inspect
"<Redis::Server: %s:%d (%s)>" % [@host, @port, @status]
end
##
# Try to connect to the redis server targeted by this object.
# 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
# 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
retry
rescue SocketError, SystemCallError, IOError => err
puts "Unable to open socket: #{err.class.name}, #{err.message}" if $debug
mark_dead err
end
return @sock
end
def connect_to(host, port, timeout=nil)
addrs = Socket.getaddrinfo('localhost', nil)
addr = addrs.detect { |ad| ad[0] == 'AF_INET' }
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
#addr = Socket.getaddrinfo(host, nil)
#sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
if timeout
secs = Integer(timeout)
usecs = Integer((timeout - secs) * 1_000_000)
optval = [secs, usecs].pack("l_2")
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
end
sock.connect(Socket.pack_sockaddr_in('6379', addr[3]))
sock
end
##
# Close the connection to the redis server targeted by this
# object. The server is not considered dead.
def close
@sock.close if @sock && !@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
end

View File

@ -0,0 +1,22 @@
require 'rubygems'
require 'ruby-prof'
require "#{File.dirname(__FILE__)}/lib/redis"
mode = ARGV.shift || 'process_time'
n = (ARGV.shift || 200).to_i
r = Redis.new
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
RubyProf.start
n.times do |i|
key = "foo#{i}"
r[key] = key * 10
r[key]
end
results = RubyProf.stop
File.open("profile.#{mode}", 'w') do |out|
RubyProf::CallTreePrinter.new(results).print(out)
end

View File

@ -12,7 +12,7 @@ class Foo
end
describe "redis" do
before do
before(:each) do
@r = Redis.new
@r.select_db(15) # use database 15 for testing so we dont accidentally step on you real data
@r['foo'] = 'bar'
@ -20,15 +20,9 @@ describe "redis" do
after do
@r.keys('*').each {|k| @r.delete k }
@r.quit
end
it "should properly marshall objects" do
class MyFail; def fail; 'it will' end; end
@r['fail'] = MyFail.new
@r['fail'].fail.should == 'it will'
end
it "should be able to GET a key" do
@r['foo'].should == 'bar'
@ -45,14 +39,14 @@ describe "redis" do
@r.set_unless_exists 'foo', 'bar'
@r['foo'].should == 'nik'
end
#
it "should be able to INCR(increment) a key" do
@r.delete('counter')
@r.incr('counter').should == 1
@r.incr('counter').should == 2
@r.incr('counter').should == 3
end
#
it "should be able to DECR(decrement) a key" do
@r.delete('counter')
@r.incr('counter').should == 1
@ -62,11 +56,11 @@ describe "redis" do
@r.decr('counter').should == 1
@r.decr('counter').should == 0
end
#
it "should be able to RANDKEY(return a random key)" do
@r.randkey.should_not be_nil
end
#
it "should be able to RENAME a key" do
@r.delete 'foo'
@r.delete 'bar'
@ -74,23 +68,23 @@ describe "redis" do
@r.rename! 'foo', 'bar'
@r['bar'].should == 'hi'
end
#
it "should be able to RENAMENX(rename unless the new key already exists) a key" do
@r.delete 'foo'
@r.delete 'bar'
@r['foo'] = 'hi'
@r['bar'] = 'ohai'
lambda {@r.rename 'foo', 'bar'}.should raise_error(RedisError)
lambda {@r.rename 'foo', 'bar'}.should raise_error(RedisRenameError)
@r['bar'].should == 'ohai'
end
#
it "should be able to EXISTS(check if key exists)" do
@r['foo'] = 'nik'
@r.key?('foo').should be_true
@r.delete 'foo'
@r.key?('foo').should be_false
end
#
it "should be able to KEYS(glob for keys)" do
@r.keys("f*").each do |key|
@r.delete key
@ -100,14 +94,14 @@ describe "redis" do
@r['foo'] = 'qux'
@r.keys("f*").sort.should == ['f','fo', 'foo'].sort
end
#
it "should be able to check the TYPE of a key" do
@r['foo'] = 'nik'
@r.type?('foo').should == "string"
@r.delete 'foo'
@r.type?('foo').should == "none"
end
#
it "should be able to push to the head of a list" do
@r.push_head "list", 'hello'
@r.push_head "list", 42
@ -116,14 +110,14 @@ describe "redis" do
@r.pop_head('list').should == '42'
@r.delete('list')
end
#
it "should be able to push to the tail of a list" do
@r.push_tail "list", 'hello'
@r.type?('list').should == "list"
@r.list_length('list').should == 1
@r.delete('list')
end
#
it "should be able to pop the tail of a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -132,7 +126,7 @@ describe "redis" do
@r.pop_tail('list').should == 'goodbye'
@r.delete('list')
end
#
it "should be able to pop the head of a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -141,7 +135,7 @@ describe "redis" do
@r.pop_head('list').should == 'hello'
@r.delete('list')
end
#
it "should be able to get the length of a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -149,7 +143,7 @@ describe "redis" do
@r.list_length('list').should == 2
@r.delete('list')
end
#
it "should be able to get a range of values from a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -161,7 +155,7 @@ describe "redis" do
@r.list_range('list', 2, -1).should == ['1', '2', '3']
@r.delete('list')
end
#
it "should be able to trim a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -175,7 +169,7 @@ describe "redis" do
@r.list_range('list', 0, -1).should == ['hello', 'goodbye']
@r.delete('list')
end
#
it "should be able to get a value by indexing into a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@ -184,7 +178,7 @@ describe "redis" do
@r.list_index('list', 1).should == 'goodbye'
@r.delete('list')
end
#
it "should be able to set a value by indexing into a list" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'hello'
@ -194,7 +188,17 @@ describe "redis" do
@r.list_index('list', 1).should == 'goodbye'
@r.delete('list')
end
#
it "should be able to remove values from a list LREM" do
@r.push_tail "list", 'hello'
@r.push_tail "list", 'goodbye'
@r.type?('list').should == "list"
@r.list_length('list').should == 2
@r.list_rm('list', 1, 'hello').should == 1
@r.list_range('list', 0, -1).should == ['goodbye']
@r.delete('list')
end
#
it "should be able add members to a set" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -203,7 +207,7 @@ describe "redis" do
@r.set_members('set').sort.should == ['key1', 'key2'].sort
@r.delete('set')
end
#
it "should be able delete members to a set" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -215,7 +219,7 @@ describe "redis" do
@r.set_members('set').should == Set.new(['key2'])
@r.delete('set')
end
#
it "should be able count the members of a set" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -223,7 +227,7 @@ describe "redis" do
@r.set_count('set').should == 2
@r.delete('set')
end
#
it "should be able test for set membership" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -234,7 +238,7 @@ describe "redis" do
@r.set_member?('set', 'notthere').should be_false
@r.delete('set')
end
#
it "should be able to do set intersection" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -242,7 +246,7 @@ describe "redis" do
@r.set_intersect('set', 'set2').should == Set.new(['key2'])
@r.delete('set')
end
#
it "should be able to do set intersection and store the results in a key" do
@r.set_add "set", 'key1'
@r.set_add "set", 'key2'
@ -251,7 +255,7 @@ describe "redis" do
@r.set_members('newone').should == Set.new(['key2'])
@r.delete('set')
end
#
it "should be able to do crazy SORT queries" do
@r['dog_1'] = 'louie'
@r.push_tail 'dogs', 1
@ -264,4 +268,50 @@ 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 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|
@r.info.keys.should include(x)
end
end
#
it "should be able to flush the database" do
@r['key1'] = 'keyone'
@r['key2'] = 'keytwo'
@r.keys('*').sort.should == ['foo', 'key1', 'key2'] #foo from before
@r.flush_db
@r.keys('*').should == []
end
#
it "should be able to provide the last save time" do
savetime = @r.last_save
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 bgsave" do
lambda {@r.bgsave}.should_not raise_error(RedisError)
end
it "should handle multiple servers" do
require 'dist_redis'
@r = DistRedis.new('localhost:6379', '127.0.0.1:6379')
@r.select_db(15) # use database 15 for testing so we dont accidentally step on you real data
100.times do |idx|
@r[idx] = "foo#{idx}"
end
100.times do |idx|
@r[idx].should == "foo#{idx}"
end
end
end

View File

@ -0,0 +1,16 @@
require 'benchmark'
require "#{File.dirname(__FILE__)}/lib/redis"
r = Redis.new
n = (ARGV.shift || 20000).to_i
elapsed = Benchmark.realtime do
# n sets, n gets
n.times do |i|
key = "foo#{i}"
r[key] = key * 10
r[key]
end
end
puts '%.2f Kops' % (2 * n / 1000 / elapsed)

View File

@ -52,6 +52,12 @@ namespace :redis do
task :stop do
RedisRunner.stop
end
desc 'Restart redis'
task :restart do
RedisRunner.stop
RedisRunner.start
end
desc 'Attach to redis dtach socket'
task :attach do
@ -60,9 +66,12 @@ namespace :redis do
desc 'Install the lastest redis from svn'
task :install => [:about, :download, :make] do
sh 'sudo cp /tmp/redis/redis-server /usr/bin/'
sh 'sudo cp /tmp/redis/redis-benchmark /usr/bin/'
puts 'Installed redis-server and redis-benchmark to /usr/bin/'
%w(redis-benchmark redis-cli redis-server).each do |bin|
sh "sudo cp /tmp/redis/#{bin} /usr/bin/"
end
puts "Installed redis-benchmark, redis-cli and redis-server to /usr/bin/"
unless File.exists?('/etc/redis.conf')
sh 'sudo cp /tmp/redis/redis.conf /etc/'
puts "Installed redis.conf to /etc/ \n You should look at this file!"
@ -76,8 +85,9 @@ namespace :redis do
desc "Download package"
task :download do
system 'svn checkout http://redis.googlecode.com/svn/trunk /tmp/redis' unless File.exists?(RedisRunner.redisdir)
system 'svn up' if File.exists?("#{RedisRunner.redisdir}/.svn")
sh 'rm -rf /tmp/redis/' if File.exists?("#{RedisRunner.redisdir}/.svn")
sh 'git clone git://github.com/antirez/redis.git /tmp/redis' unless File.exists?(RedisRunner.redisdir)
sh "cd #{RedisRunner.redisdir} && git pull" if File.exists?("#{RedisRunner.redisdir}/.git")
end
end