Created
March 19, 2009 07:57
-
-
Save jeremy/81637 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 273f56bd6c23bea5ca910af689aa44d4ca56cb7b Mon Sep 17 00:00:00 2001 | |
From: Jeremy Kemper <[email protected]> | |
Date: Fri, 27 Feb 2009 14:13:32 -0800 | |
Subject: [PATCH 1/4] Rather than check socket.closed? on every read/write, connect once and reconnect on exceptions | |
--- | |
lib/redis.rb | 45 ++++++++++++++++++++++++--------------------- | |
1 files changed, 24 insertions(+), 21 deletions(-) | |
diff --git a/lib/redis.rb b/lib/redis.rb | |
index ba1218c..66fcead 100644 | |
--- a/lib/redis.rb | |
+++ b/lib/redis.rb | |
@@ -8,9 +8,12 @@ class Redis | |
OK = "+OK".freeze | |
ERROR = "-".freeze | |
NIL = 'nil'.freeze | |
- | |
+ | |
+ attr_reader :socket | |
+ | |
def initialize(opts={}) | |
@opts = {:host => 'localhost', :port => '6379'}.merge(opts) | |
+ connect | |
end | |
# SET <key> <value> | |
@@ -450,13 +453,24 @@ class Redis | |
write "QUIT\r\n" | |
read_proto | |
end | |
- | |
- private | |
- | |
+ | |
+ def connect | |
+ @socket = TCPSocket.new(@opts[:host], @opts[:port]) | |
+ @socket.sync = true | |
+ @socket | |
+ end | |
+ | |
def close | |
- socket.close unless socket.closed? | |
+ @socket.close if @socket && [email protected]? | |
end | |
- | |
+ | |
+ def reconnect | |
+ close | |
+ connect | |
+ end | |
+ | |
+ private | |
+ | |
def timeout_retry(time, retries, &block) | |
timeout(time, &block) | |
rescue TimeoutError | |
@@ -464,35 +478,24 @@ class Redis | |
retry unless retries < 0 | |
end | |
- def socket | |
- connect if (!@socket or @socket.closed?) | |
- @socket | |
- end | |
- | |
- def connect | |
- @socket = TCPSocket.new(@opts[:host], @opts[:port]) | |
- @socket.sync = true | |
- @socket | |
- end | |
- | |
def read(length) | |
retries = 3 | |
- res = socket.read(length) | |
+ res = @socket.read(length) | |
rescue | |
retries -= 1 | |
if retries > 0 | |
- connect | |
+ reconnect | |
retry | |
end | |
end | |
def write(data) | |
retries = 3 | |
- socket.write(data) | |
+ @socket.write(data) | |
rescue | |
retries -= 1 | |
if retries > 0 | |
- connect | |
+ reconnect | |
retry | |
end | |
end | |
-- | |
1.6.2 | |
From e02a8f9bd579514c149093e2d9feebce581c518c Mon Sep 17 00:00:00 2001 | |
From: Jeremy Kemper <[email protected]> | |
Date: Fri, 27 Feb 2009 14:20:05 -0800 | |
Subject: [PATCH 2/4] Require local redis lib first | |
--- | |
profile.rb | 3 +-- | |
1 files changed, 1 insertions(+), 2 deletions(-) | |
diff --git a/profile.rb b/profile.rb | |
index fcb160b..e67700d 100644 | |
--- a/profile.rb | |
+++ b/profile.rb | |
@@ -1,8 +1,7 @@ | |
require 'rubygems' | |
require 'ruby-prof' | |
-$:.push File.join(File.dirname(__FILE__), 'lib') | |
+require "#{File.dirname(__FILE__)}/lib/redis" | |
-require 'redis' | |
@r = Redis.new | |
@r['foo'] = "The first line we sent to the server is some text" | |
-- | |
1.6.2 | |
From be0c0084c62a883eee1a87575a85b0c34ab59749 Mon Sep 17 00:00:00 2001 | |
From: Jeremy Kemper <[email protected]> | |
Date: Fri, 27 Feb 2009 16:52:04 -0800 | |
Subject: [PATCH 3/4] Squish common operations | |
--- | |
lib/redis.rb | 263 ++++++++++++++-------------------------------------------- | |
1 files changed, 64 insertions(+), 199 deletions(-) | |
diff --git a/lib/redis.rb b/lib/redis.rb | |
index 66fcead..332dfa1 100644 | |
--- a/lib/redis.rb | |
+++ b/lib/redis.rb | |
@@ -21,30 +21,20 @@ class Redis | |
# Set the string <value> as value of the key. | |
# The string can't be longer than 1073741824 bytes (1 GB). | |
def []=(key, val) | |
- write "SET #{key} #{val.size}\r\n#{val}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("SET #{key} #{val.size}\r\n#{val}\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
- | |
+ | |
# SETNX <key> <value> | |
# Time complexity: O(1) | |
# SETNX works exactly like SET with the only difference that | |
# if the key already exists no operation is performed. | |
# SETNX actually means "SET if Not eXists". | |
def set_unless_exists(key, val) | |
- write "SETNX #{key} #{val.size}\r\n#{val}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("SETNX #{key} #{val.size}\r\n#{val}\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
- | |
+ | |
# GET <key> | |
# Time complexity: O(1) | |
# Get the value of the specified key. If the key | |
@@ -52,15 +42,8 @@ class Redis | |
# If the value stored at <key> is not a string an error | |
# is returned because GET can only handle string values. | |
def [](key) | |
- write "GET #{key}\r\n" | |
- res = read_proto | |
- if res != NIL | |
- val = read(res.to_i) | |
- nibble_end | |
- val | |
- else | |
- nil | |
- end | |
+ res = perform("GET #{key}\r\n") | |
+ fetch(res) if res != NIL | |
end | |
# INCR <key> | |
@@ -70,8 +53,7 @@ class Redis | |
# value was zero). If the value at <key> is not a string value | |
# an error is returned. | |
def incr(key) | |
- write "INCR #{key}\r\n" | |
- read_proto.to_i | |
+ perform("INCR #{key}\r\n").to_i | |
end | |
# !! SEEMS BROKEN IN REDIS SERVER RIGHT NOW !! | |
@@ -79,8 +61,7 @@ class Redis | |
# INCRBY works just like INCR but instead to increment by 1 the | |
# increment is <num>. | |
def incrby(key, num) | |
- write "INCRBY #{key} #{num}\r\n" | |
- read_proto.to_i | |
+ perform("INCRBY #{key} #{num}\r\n").to_i | |
end | |
# DECR <key> | |
@@ -90,8 +71,7 @@ class Redis | |
# value was zero). If the value at <key> is not a string value | |
# an error is returned. | |
def decr(key) | |
- write "DECR #{key}\r\n" | |
- read_proto.to_i | |
+ perform("DECR #{key}\r\n").to_i | |
end | |
# !! SEEMS BROKEN IN REDIS SERVER RIGHT NOW !! | |
@@ -99,16 +79,14 @@ class Redis | |
# DECRBY works just like DECR but instead to decrement by 1 the | |
# decrement is <value>. | |
def decrby(key, num) | |
- write "DECRBY #{key} #{num}\r\n" | |
- read_proto.to_i | |
+ perform("DECRBY #{key} #{num}\r\n").to_i | |
end | |
# RANDOMKEY | |
# Time complexity: O(1) | |
# Returns a random key from the currently seleted DB. | |
def randkey | |
- write "RANDOMKEY\r\n" | |
- read_proto | |
+ perform("RANDOMKEY\r\n") | |
end | |
# RENAME <oldkey> <newkey> | |
@@ -116,26 +94,16 @@ class Redis | |
# destination name are the same an error is returned. If <newkey> | |
# already exists it is overwritten. | |
def rename!(oldkey, newkey) | |
- write "RENAME #{oldkey} #{newkey}\r\n" | |
- res = read_proto | |
- if res == OK | |
- newkey | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("RENAME #{oldkey} #{newkey}\r\n") | |
+ res == OK ? newkey : raise(RedisError, res.inspect) | |
end | |
# RENAMENX <oldkey> <newkey> | |
# Just like RENAME but fails if the destination key <newkey> | |
# already exists. | |
def rename(oldkey, newkey) | |
- write "RENAMENX #{oldkey} #{newkey}\r\n" | |
- res = read_proto | |
- if res == OK | |
- newkey | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("RENAMENX #{oldkey} #{newkey}\r\n") | |
+ res == OK ? newkey : raise(RedisError, res.inspect) | |
end | |
# EXISTS <key> | |
@@ -145,8 +113,7 @@ class Redis | |
# Note that even keys set with an empty string as value will | |
# return "1". | |
def key?(key) | |
- write "EXISTS #{key}\r\n" | |
- read_proto.to_i == 1 | |
+ perform("EXISTS #{key}\r\n") == '1' | |
end | |
# DEL <key> | |
@@ -155,12 +122,8 @@ class Redis | |
# no operation is performed. The command always returns success. | |
# | |
def delete(key) | |
- write "DEL #{key}\r\n" | |
- if read_proto == OK | |
- true | |
- else | |
- raise RedisError | |
- end | |
+ res = perform("DEL #{key}\r\n") | |
+ res == OK || raise(RedisError) | |
end | |
# KEYS <pattern> | |
@@ -170,13 +133,8 @@ class Redis | |
# database the keys "foo" and "foobar" the command "KEYS foo*" | |
# will return "foo foobar". | |
def keys(glob) | |
- write "KEYS #{glob}\r\n" | |
- res = read_proto | |
- if res | |
- keys = read(res.to_i).split(" ") | |
- nibble_end | |
- keys | |
- end | |
+ res = perform("KEYS #{glob}\r\n") | |
+ fetch(res).split(/ /) | |
end | |
# !! SEEMS BROKEN IN REDIS SERVER RIGHT NOW !! | |
@@ -186,8 +144,7 @@ class Redis | |
# string. The type can be one of "NONE","STRING","LIST","SET". | |
# NONE is returned if the key does not exist. | |
def type?(key) | |
- write "TYPE #{key}\r\n" | |
- read_proto | |
+ perform("TYPE #{key}\r\n") | |
end | |
# RPUSH <key> <string> | |
@@ -197,13 +154,8 @@ class Redis | |
# the append operation. If the key exists but is not a List an error | |
# is returned. | |
def push_head(key, string) | |
- write "RPUSH #{key} #{string.size}\r\n#{string}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("RPUSH #{key} #{string.size}\r\n#{string}\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# LPUSH <key> <string> | |
@@ -213,13 +165,8 @@ class Redis | |
# the append operation. If the key exists but is not a List an error | |
# is returned. | |
def push_tail(key, string) | |
- write "LPUSH #{key} #{string.size}\r\n#{string}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("LPUSH #{key} #{string.size}\r\n#{string}\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# | |
@@ -230,8 +177,8 @@ class Redis | |
# empty lists). If the value stored at key is not a list an error | |
# is returned. | |
def list_length(key) | |
- write "LLEN #{key}\r\n" | |
- Integer(read_proto) | |
+ res = perform("LLEN #{key}\r\n") | |
+ Integer(res) unless res == '0' | |
end | |
# | |
@@ -253,18 +200,12 @@ class Redis | |
# If end over the end of the list Redis will threat it just like | |
# the last element of the list. | |
def list_range(key, start, ending) | |
- write "LRANGE #{key} #{start} #{ending}\r\n" | |
- res = read_proto | |
+ res = perform("LRANGE #{key} #{start} #{ending}\r\n") | |
if res[0] = ERROR | |
raise RedisError, read_proto | |
else | |
items = Integer(read_proto) | |
- list = [] | |
- items.times do | |
- list << read(Integer(read_proto)) | |
- nibble_end | |
- end | |
- list | |
+ (0..items).map { fetch(Integer(read_proto)) } | |
end | |
end | |
@@ -299,13 +240,8 @@ class Redis | |
# in this way LTRIM is an O(1) operation because in the average case | |
# just one element is removed from the tail of the list. | |
def list_trim(key, start, ending) | |
- write "LTRIM #{key} #{start} #{ending}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("LTRIM #{key} #{start} #{ending}\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# | |
@@ -322,15 +258,8 @@ class Redis | |
# Note that even if the average time complexity is O(n) asking for | |
# the first or the last element of the list is O(1). | |
def list_index(key, index) | |
- write "LINDEX #{key} #{index}\r\n" | |
- res = read_proto | |
- if res != NIL | |
- val = read(res.to_i) | |
- nibble_end | |
- val | |
- else | |
- nil | |
- end | |
+ res = perform("LINDEX #{key} #{index}\r\n") | |
+ fetch(res) if res != NIL | |
end | |
# | |
@@ -343,30 +272,16 @@ class Redis | |
# If the <key> does not exist or the list is already empty the special | |
# value 'nil' is returned. | |
def list_pop_head(key) | |
- write "LPOP #{key} #{index}\r\n" | |
- res = read_proto | |
- if res != NIL | |
- val = read(res.to_i) | |
- nibble_end | |
- val | |
- else | |
- nil | |
- end | |
+ res = perform("LPOP #{key} #{index}\r\n") | |
+ fetch(res) if res != NIL | |
end | |
# RPOP <key> | |
# This command works exactly like LPOP, but the last element instead | |
# of the first element of the list is returned/deleted. | |
def list_pop_tail(key) | |
- write "RPOP #{key} #{index}\r\n" | |
- res = read_proto | |
- if res != NIL | |
- val = read(res.to_i) | |
- nibble_end | |
- val | |
- else | |
- nil | |
- end | |
+ res = perform("RPOP #{key} #{index}\r\n") | |
+ fetch(res) if res != NIL | |
end | |
# SELECT <index> | |
@@ -374,13 +289,8 @@ class Redis | |
# For default every new client connection is automatically selected | |
# to DB 0. | |
def select_db(index) | |
- write "SELECT #{index}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("SELECT\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# | |
# MOVE <key> <index> | |
@@ -388,13 +298,8 @@ class Redis | |
# destination DB. If a key with the same name exists in the destination | |
# DB an error is returned. | |
def move(key, index) | |
- write "MOVE #{index}\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("MOVE\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# SAVE | |
@@ -402,13 +307,8 @@ class Redis | |
# completed, no connection is served in the meanwhile. An OK code | |
# is returned when the DB was fully stored in disk. | |
def save | |
- write "SAVE\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("SAVE\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# BGSAVE | |
@@ -417,13 +317,8 @@ class Redis | |
# saves the DB on disk then exit. A client my be able to check if the | |
# operation succeeded using the LASTSAVE command. | |
def bgsave | |
- write "BGSAVE\r\n" | |
- res = read_proto | |
- if res == OK | |
- true | |
- else | |
- raise RedisError, res.inspect | |
- end | |
+ res = perform("BGSAVE\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# | |
@@ -432,9 +327,9 @@ class Redis | |
# A client may check if a BGSAVE command succeeded reading the LASTSAVE | |
# value, then issuing a BGSAVE command and checking at regular intervals | |
# every N seconds if LASTSAVE changed. | |
- def bgsave | |
- write "LASTSAVE\r\n" | |
- read_proto | |
+ def lastsave | |
+ res = perform("LASTSAVE\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
# | |
@@ -444,14 +339,14 @@ class Redis | |
# This is not guaranteed if the client uses simply "SAVE" and then | |
# "QUIT" because other clients may alter the DB data between the two | |
# commands. | |
- def bgsave | |
- write "SHUTDOWN\r\n" | |
- read_proto | |
+ def shutdown | |
+ res = perform("SHUTDOWN\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
def quit | |
- write "QUIT\r\n" | |
- read_proto | |
+ res = perform("QUIT\r\n") | |
+ res == OK || raise(RedisError, res.inspect) | |
end | |
def connect | |
@@ -469,48 +364,18 @@ class Redis | |
connect | |
end | |
- private | |
+ protected | |
- def timeout_retry(time, retries, &block) | |
- timeout(time, &block) | |
- rescue TimeoutError | |
- retries -= 1 | |
- retry unless retries < 0 | |
- end | |
- | |
- def read(length) | |
- retries = 3 | |
- res = @socket.read(length) | |
- rescue | |
- retries -= 1 | |
- if retries > 0 | |
- reconnect | |
- retry | |
- end | |
- end | |
- | |
- def write(data) | |
- retries = 3 | |
- @socket.write(data) | |
- rescue | |
- retries -= 1 | |
- if retries > 0 | |
- reconnect | |
- retry | |
- end | |
+ def perform(command) | |
+ @socket.write(command) | |
+ @socket.gets.chop | |
end | |
- | |
- def nibble_end | |
- read(2) | |
+ | |
+ def fetch(res) | |
+ @socket.read(res.to_i + 2).chop | |
end | |
- | |
+ | |
def read_proto | |
- buff = "" | |
- while (char = read(1)) | |
- buff << char | |
- break if buff[-2..-1] == "\r\n" | |
- end | |
- buff[0..-3] | |
+ @socket.gets.chop | |
end | |
- | |
end | |
-- | |
1.6.2 | |
From c109cfe5447aebca8bcefae0dcf4a45fd2c630eb Mon Sep 17 00:00:00 2001 | |
From: Jeremy Kemper <[email protected]> | |
Date: Fri, 27 Feb 2009 16:54:06 -0800 | |
Subject: [PATCH 4/4] speed | |
--- | |
profile.rb | 16 +++++++++------- | |
speed.rb | 16 ++++++++++++++++ | |
2 files changed, 25 insertions(+), 7 deletions(-) | |
create mode 100644 speed.rb | |
diff --git a/profile.rb b/profile.rb | |
index e67700d..e5dc8c0 100644 | |
--- a/profile.rb | |
+++ b/profile.rb | |
@@ -2,18 +2,20 @@ require 'rubygems' | |
require 'ruby-prof' | |
require "#{File.dirname(__FILE__)}/lib/redis" | |
-@r = Redis.new | |
-@r['foo'] = "The first line we sent to the server is some text" | |
mode = ARGV.shift || 'process_time' | |
+n = (ARGV.shift || 200).to_i | |
+ | |
+r = Redis.new | |
RubyProf.measure_mode = RubyProf.const_get(mode.upcase) | |
RubyProf.start | |
-100.times do |i| | |
- @r["foo#{i}"] = "The first line we sent to the server is some text" | |
- 10.times do | |
- @r["foo#{i}"] | |
- end | |
+ | |
+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) | |
diff --git a/speed.rb b/speed.rb | |
new file mode 100644 | |
index 0000000..51e5811 | |
--- /dev/null | |
+++ b/speed.rb | |
@@ -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) | |
-- | |
1.6.2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment