timcharper-redis 0.0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +31 -0
- data/Rakefile +59 -0
- data/lib/dist_redis.rb +111 -0
- data/lib/hash_ring.rb +127 -0
- data/lib/pipeline.rb +31 -0
- data/lib/redis.rb +504 -0
- data/lib/server.rb +128 -0
- data/spec/redis_spec.rb +401 -0
- data/spec/spec_helper.rb +4 -0
- metadata +64 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ezra Zygmuntowicz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# redis-rb
|
2
|
+
|
3
|
+
A ruby client library for the redis key value storage system.
|
4
|
+
|
5
|
+
## Information about redis
|
6
|
+
|
7
|
+
Redis is a key value store with some interesting features:
|
8
|
+
1. It's fast.
|
9
|
+
2. Keys are strings but values can have types of "NONE", "STRING", "LIST", or "SET". List's can be atomically push'd, pop'd, lpush'd, lpop'd and indexed. This allows you to store things like lists of comments under one key while retaining the ability to append comments without reading and putting back the whole list.
|
10
|
+
|
11
|
+
See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for more information.
|
12
|
+
|
13
|
+
## Dependencies
|
14
|
+
|
15
|
+
1. redis -
|
16
|
+
|
17
|
+
rake redis:install
|
18
|
+
|
19
|
+
2. dtach -
|
20
|
+
|
21
|
+
rake dtach:install
|
22
|
+
|
23
|
+
3. svn - git is the new black, but we need it for the google codes.
|
24
|
+
|
25
|
+
## Setup
|
26
|
+
|
27
|
+
Use the tasks mentioned above (in Dependencies) to get your machine setup.
|
28
|
+
|
29
|
+
## Examples
|
30
|
+
|
31
|
+
Check the examples/ directory. *Note* you need to have redis-server running first.
|
data/Rakefile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'tasks/redis.tasks'
|
7
|
+
|
8
|
+
|
9
|
+
GEM = 'redis'
|
10
|
+
GEM_NAME = 'redis'
|
11
|
+
GEM_VERSION = '0.0.3.3'
|
12
|
+
AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark']
|
13
|
+
EMAIL = "matt.clark@punchstock.com"
|
14
|
+
HOMEPAGE = "http://github.com/winescout/redis-rb"
|
15
|
+
SUMMARY = "Ruby client library for redis key value storage server"
|
16
|
+
|
17
|
+
spec = Gem::Specification.new do |s|
|
18
|
+
s.name = GEM
|
19
|
+
s.version = GEM_VERSION
|
20
|
+
s.platform = Gem::Platform::RUBY
|
21
|
+
s.has_rdoc = true
|
22
|
+
s.extra_rdoc_files = ["LICENSE"]
|
23
|
+
s.summary = SUMMARY
|
24
|
+
s.description = s.summary
|
25
|
+
s.authors = AUTHORS
|
26
|
+
s.email = EMAIL
|
27
|
+
s.homepage = HOMEPAGE
|
28
|
+
|
29
|
+
# Uncomment this to add a dependency
|
30
|
+
# s.add_dependency "foo"
|
31
|
+
|
32
|
+
s.require_path = 'lib'
|
33
|
+
s.autorequire = GEM
|
34
|
+
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,spec}/**/*")
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
desc "Run specs"
|
40
|
+
Spec::Rake::SpecTask.new do |t|
|
41
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
42
|
+
t.spec_opts = %w(-fs --color)
|
43
|
+
end
|
44
|
+
|
45
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
46
|
+
pkg.gem_spec = spec
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "install the gem locally"
|
50
|
+
task :install => [:package] do
|
51
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "create a gemspec file"
|
55
|
+
task :make_spec do
|
56
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
57
|
+
file.puts spec.to_ruby
|
58
|
+
end
|
59
|
+
end
|
data/lib/dist_redis.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'hash_ring'
|
3
|
+
class DistRedis
|
4
|
+
attr_reader :ring
|
5
|
+
def initialize(*servers)
|
6
|
+
srvs = []
|
7
|
+
servers.each do |s|
|
8
|
+
server, port = s.split(':')
|
9
|
+
srvs << Redis.new(:host => server, :port => port)
|
10
|
+
end
|
11
|
+
@ring = HashRing.new srvs
|
12
|
+
end
|
13
|
+
|
14
|
+
def node_for_key(key)
|
15
|
+
if key =~ /\{(.*)?\}/
|
16
|
+
key = $1
|
17
|
+
end
|
18
|
+
@ring.get_node(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_server(server)
|
22
|
+
server, port = server.split(':')
|
23
|
+
@ring.add_node Redis.new(:host => server, :port => port)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(sym, *args, &blk)
|
27
|
+
if redis = node_for_key(args.first.to_s)
|
28
|
+
redis.send sym, *args, &blk
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def keys(glob)
|
35
|
+
keyz = []
|
36
|
+
@ring.nodes.each do |red|
|
37
|
+
keyz.concat red.keys(glob)
|
38
|
+
end
|
39
|
+
keyz
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
@ring.nodes.each do |red|
|
44
|
+
red.save
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def bgsave
|
49
|
+
@ring.nodes.each do |red|
|
50
|
+
red.bgsave
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def quit
|
55
|
+
@ring.nodes.each do |red|
|
56
|
+
red.quit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete_cloud!
|
61
|
+
@ring.nodes.each do |red|
|
62
|
+
red.keys("*").each do |key|
|
63
|
+
red.delete key
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
if __FILE__ == $0
|
72
|
+
|
73
|
+
r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localhost:6382'
|
74
|
+
r['urmom'] = 'urmom'
|
75
|
+
r['urdad'] = 'urdad'
|
76
|
+
r['urmom1'] = 'urmom1'
|
77
|
+
r['urdad1'] = 'urdad1'
|
78
|
+
r['urmom2'] = 'urmom2'
|
79
|
+
r['urdad2'] = 'urdad2'
|
80
|
+
r['urmom3'] = 'urmom3'
|
81
|
+
r['urdad3'] = 'urdad3'
|
82
|
+
p r['urmom']
|
83
|
+
p r['urdad']
|
84
|
+
p r['urmom1']
|
85
|
+
p r['urdad1']
|
86
|
+
p r['urmom2']
|
87
|
+
p r['urdad2']
|
88
|
+
p r['urmom3']
|
89
|
+
p r['urdad3']
|
90
|
+
|
91
|
+
r.push_tail 'listor', 'foo1'
|
92
|
+
r.push_tail 'listor', 'foo2'
|
93
|
+
r.push_tail 'listor', 'foo3'
|
94
|
+
r.push_tail 'listor', 'foo4'
|
95
|
+
r.push_tail 'listor', 'foo5'
|
96
|
+
|
97
|
+
p r.pop_tail('listor')
|
98
|
+
p r.pop_tail('listor')
|
99
|
+
p r.pop_tail('listor')
|
100
|
+
p r.pop_tail('listor')
|
101
|
+
p r.pop_tail('listor')
|
102
|
+
|
103
|
+
puts "key distribution:"
|
104
|
+
|
105
|
+
r.ring.nodes.each do |red|
|
106
|
+
p [red.port, red.keys("*")]
|
107
|
+
end
|
108
|
+
r.delete_cloud!
|
109
|
+
p r.keys('*')
|
110
|
+
|
111
|
+
end
|
data/lib/hash_ring.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
class HashRing
|
4
|
+
|
5
|
+
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
6
|
+
|
7
|
+
attr_reader :ring, :sorted_keys, :replicas, :nodes
|
8
|
+
|
9
|
+
# nodes is a list of objects that have a proper to_s representation.
|
10
|
+
# replicas indicates how many virtual points should be used pr. node,
|
11
|
+
# replicas are required to improve the distribution.
|
12
|
+
def initialize(nodes=[], replicas=POINTS_PER_SERVER)
|
13
|
+
@replicas = replicas
|
14
|
+
@ring = {}
|
15
|
+
@nodes = []
|
16
|
+
@sorted_keys = []
|
17
|
+
nodes.each do |node|
|
18
|
+
add_node(node)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds a `node` to the hash ring (including a number of replicas).
|
23
|
+
def add_node(node)
|
24
|
+
@nodes << node
|
25
|
+
@replicas.times do |i|
|
26
|
+
key = Zlib.crc32("#{node}:#{i}")
|
27
|
+
@ring[key] = node
|
28
|
+
@sorted_keys << key
|
29
|
+
end
|
30
|
+
@sorted_keys.sort!
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_node(node)
|
34
|
+
@replicas.times do |i|
|
35
|
+
key = Zlib.crc32("#{node}:#{count}")
|
36
|
+
@ring.delete(key)
|
37
|
+
@sorted_keys.reject! {|k| k == key}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# get the node in the hash ring for this key
|
42
|
+
def get_node(key)
|
43
|
+
get_node_pos(key)[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_node_pos(key)
|
47
|
+
return [nil,nil] if @ring.size == 0
|
48
|
+
crc = Zlib.crc32(key)
|
49
|
+
idx = HashRing.binary_search(@sorted_keys, crc)
|
50
|
+
return [@ring[@sorted_keys[idx]], idx]
|
51
|
+
end
|
52
|
+
|
53
|
+
def iter_nodes(key)
|
54
|
+
return [nil,nil] if @ring.size == 0
|
55
|
+
node, pos = get_node_pos(key)
|
56
|
+
@sorted_keys[pos..-1].each do |k|
|
57
|
+
yield @ring[k]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class << self
|
62
|
+
|
63
|
+
# gem install RubyInline to use this code
|
64
|
+
# Native extension to perform the binary search within the hashring.
|
65
|
+
# There's a pure ruby version below so this is purely optional
|
66
|
+
# for performance. In testing 20k gets and sets, the native
|
67
|
+
# binary search shaved about 12% off the runtime (9sec -> 8sec).
|
68
|
+
begin
|
69
|
+
require 'inline'
|
70
|
+
inline do |builder|
|
71
|
+
builder.c <<-EOM
|
72
|
+
int binary_search(VALUE ary, unsigned int r) {
|
73
|
+
int upper = RARRAY_LEN(ary) - 1;
|
74
|
+
int lower = 0;
|
75
|
+
int idx = 0;
|
76
|
+
|
77
|
+
while (lower <= upper) {
|
78
|
+
idx = (lower + upper) / 2;
|
79
|
+
|
80
|
+
VALUE continuumValue = RARRAY_PTR(ary)[idx];
|
81
|
+
unsigned int l = NUM2UINT(continuumValue);
|
82
|
+
if (l == r) {
|
83
|
+
return idx;
|
84
|
+
}
|
85
|
+
else if (l > r) {
|
86
|
+
upper = idx - 1;
|
87
|
+
}
|
88
|
+
else {
|
89
|
+
lower = idx + 1;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
return upper;
|
93
|
+
}
|
94
|
+
EOM
|
95
|
+
end
|
96
|
+
rescue Exception => e
|
97
|
+
# Find the closest index in HashRing with value <= the given value
|
98
|
+
def binary_search(ary, value, &block)
|
99
|
+
upper = ary.size - 1
|
100
|
+
lower = 0
|
101
|
+
idx = 0
|
102
|
+
|
103
|
+
while(lower <= upper) do
|
104
|
+
idx = (lower + upper) / 2
|
105
|
+
comp = ary[idx] <=> value
|
106
|
+
|
107
|
+
if comp == 0
|
108
|
+
return idx
|
109
|
+
elsif comp > 0
|
110
|
+
upper = idx - 1
|
111
|
+
else
|
112
|
+
lower = idx + 1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
return upper
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
# ring = HashRing.new ['server1', 'server2', 'server3']
|
124
|
+
# p ring
|
125
|
+
# #
|
126
|
+
# p ring.get_node "kjhjkjlkjlkkh"
|
127
|
+
#
|
data/lib/pipeline.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "redis"
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Pipeline < Redis
|
5
|
+
BUFFER_SIZE = 50_000
|
6
|
+
|
7
|
+
def initialize(redis)
|
8
|
+
@redis = redis
|
9
|
+
@commands = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_response
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(data)
|
16
|
+
@commands << data
|
17
|
+
write_and_read if @commands.size >= BUFFER_SIZE
|
18
|
+
end
|
19
|
+
|
20
|
+
def finish
|
21
|
+
write_and_read
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_and_read
|
25
|
+
@redis.write @commands.join
|
26
|
+
@redis.read_socket
|
27
|
+
@commands.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/redis.rb
ADDED
@@ -0,0 +1,504 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'set'
|
3
|
+
require File.join(File.dirname(__FILE__),'server')
|
4
|
+
require File.join(File.dirname(__FILE__),'pipeline')
|
5
|
+
|
6
|
+
|
7
|
+
class RedisError < StandardError
|
8
|
+
end
|
9
|
+
class RedisRenameError < StandardError
|
10
|
+
end
|
11
|
+
class Redis
|
12
|
+
ERR = "-".freeze
|
13
|
+
OK = 'OK'.freeze
|
14
|
+
SINGLE = '+'.freeze
|
15
|
+
BULK = '$'.freeze
|
16
|
+
MULTI = '*'.freeze
|
17
|
+
INT = ':'.freeze
|
18
|
+
|
19
|
+
attr_reader :server
|
20
|
+
|
21
|
+
|
22
|
+
def initialize(opts={})
|
23
|
+
@opts = {:host => 'localhost', :port => '6379', :db => 0}.merge(opts)
|
24
|
+
$debug = @opts[:debug]
|
25
|
+
@db = @opts[:db]
|
26
|
+
@server = Server.new(@opts[:host], @opts[:port], (@opts[:timeout]||10))
|
27
|
+
end
|
28
|
+
|
29
|
+
def pipelined
|
30
|
+
pipeline = Pipeline.new(self)
|
31
|
+
yield pipeline
|
32
|
+
pipeline.finish
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"#{host}:#{port}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def port
|
40
|
+
@opts[:port]
|
41
|
+
end
|
42
|
+
|
43
|
+
def host
|
44
|
+
@opts[:host]
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_socket_management(server, &block)
|
48
|
+
begin
|
49
|
+
block.call(server.socket)
|
50
|
+
#Timeout or server down
|
51
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED => e
|
52
|
+
server.close
|
53
|
+
puts "Client (#{server.inspect}) disconnected from server: #{e.inspect}\n" if $debug
|
54
|
+
retry
|
55
|
+
#Server down
|
56
|
+
rescue NoMethodError => e
|
57
|
+
puts "Client (#{server.inspect}) tryin server that is down: #{e.inspect}\n Dying!" if $debug
|
58
|
+
raise Errno::ECONNREFUSED
|
59
|
+
#exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def monitor
|
64
|
+
with_socket_management(@server) do |socket|
|
65
|
+
trap("INT") { puts "\nGot ^C! Dying!"; exit }
|
66
|
+
write "MONITOR\r\n"
|
67
|
+
puts "Now Monitoring..."
|
68
|
+
socket.read(12)
|
69
|
+
loop do
|
70
|
+
x = socket.gets
|
71
|
+
puts x unless x.nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def quit
|
77
|
+
write "QUIT\r\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_db(index)
|
81
|
+
@db = index
|
82
|
+
write "SELECT #{index}\r\n"
|
83
|
+
get_response
|
84
|
+
end
|
85
|
+
|
86
|
+
def flush_db
|
87
|
+
write "FLUSHDB\r\n"
|
88
|
+
get_response == OK
|
89
|
+
end
|
90
|
+
|
91
|
+
def flush_all
|
92
|
+
ensure_retry do
|
93
|
+
puts "Warning!\nFlushing *ALL* databases!\n5 Seconds to Hit ^C!"
|
94
|
+
trap('INT') {quit; return false}
|
95
|
+
sleep 5
|
96
|
+
write "FLUSHALL\r\n"
|
97
|
+
get_response == OK
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def last_save
|
102
|
+
write "LASTSAVE\r\n"
|
103
|
+
get_response.to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def bgsave
|
107
|
+
write "BGSAVE\r\n"
|
108
|
+
get_response == OK
|
109
|
+
end
|
110
|
+
|
111
|
+
def info
|
112
|
+
info = {}
|
113
|
+
write("INFO\r\n")
|
114
|
+
x = get_response
|
115
|
+
x.each do |kv|
|
116
|
+
k,v = kv.split(':', 2)
|
117
|
+
k,v = k.chomp, v = v.chomp
|
118
|
+
info[k.to_sym] = v
|
119
|
+
end
|
120
|
+
info
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
def bulk_reply
|
125
|
+
begin
|
126
|
+
x = read
|
127
|
+
puts "bulk_reply read value is #{x.inspect}" if $debug
|
128
|
+
return x
|
129
|
+
rescue => e
|
130
|
+
puts "error in bulk_reply #{e}" if $debug
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def write(data)
|
136
|
+
with_socket_management(@server) do |socket|
|
137
|
+
puts "writing: #{data}" if $debug
|
138
|
+
socket.write(data)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def fetch(len)
|
143
|
+
with_socket_management(@server) do |socket|
|
144
|
+
len = [0, len.to_i].max
|
145
|
+
res = socket.read(len + 2)
|
146
|
+
res = res.chomp if res
|
147
|
+
res
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def read(length = read_proto)
|
152
|
+
with_socket_management(@server) do |socket|
|
153
|
+
res = socket.read(length)
|
154
|
+
puts "read is #{res.inspect}" if $debug
|
155
|
+
res
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def keys(glob)
|
160
|
+
write "KEYS #{glob}\r\n"
|
161
|
+
get_response.split(' ')
|
162
|
+
end
|
163
|
+
|
164
|
+
def rename!(oldkey, newkey)
|
165
|
+
write "RENAME #{oldkey} #{newkey}\r\n"
|
166
|
+
get_response
|
167
|
+
end
|
168
|
+
|
169
|
+
def rename(oldkey, newkey)
|
170
|
+
write "RENAMENX #{oldkey} #{newkey}\r\n"
|
171
|
+
case get_response
|
172
|
+
when -1
|
173
|
+
raise RedisRenameError, "source key: #{oldkey} does not exist"
|
174
|
+
when 0
|
175
|
+
raise RedisRenameError, "target key: #{oldkey} already exists"
|
176
|
+
when -3
|
177
|
+
raise RedisRenameError, "source and destination keys are the same"
|
178
|
+
when 1
|
179
|
+
true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def key?(key)
|
184
|
+
write "EXISTS #{key}\r\n"
|
185
|
+
get_response == 1
|
186
|
+
end
|
187
|
+
|
188
|
+
def delete(key)
|
189
|
+
write "DEL #{key}\r\n"
|
190
|
+
get_response == 1
|
191
|
+
end
|
192
|
+
|
193
|
+
def [](key)
|
194
|
+
get(key)
|
195
|
+
end
|
196
|
+
|
197
|
+
def get(key)
|
198
|
+
write "GET #{key}\r\n"
|
199
|
+
get_response
|
200
|
+
end
|
201
|
+
|
202
|
+
def mget(*keys)
|
203
|
+
write "MGET #{keys.join(' ')}\r\n"
|
204
|
+
get_response
|
205
|
+
end
|
206
|
+
|
207
|
+
def incr(key, increment=nil)
|
208
|
+
if increment
|
209
|
+
write "INCRBY #{key} #{increment}\r\n"
|
210
|
+
else
|
211
|
+
write "INCR #{key}\r\n"
|
212
|
+
end
|
213
|
+
get_response
|
214
|
+
end
|
215
|
+
|
216
|
+
def decr(key, decrement=nil)
|
217
|
+
if decrement
|
218
|
+
write "DECRBY #{key} #{decrement}\r\n"
|
219
|
+
else
|
220
|
+
write "DECR #{key}\r\n"
|
221
|
+
end
|
222
|
+
get_response
|
223
|
+
end
|
224
|
+
|
225
|
+
def randkey
|
226
|
+
write "RANDOMKEY\r\n"
|
227
|
+
get_response
|
228
|
+
end
|
229
|
+
|
230
|
+
def list_length(key)
|
231
|
+
write "LLEN #{key}\r\n"
|
232
|
+
case i = get_response
|
233
|
+
when -2
|
234
|
+
raise RedisError, "key: #{key} does not hold a list value"
|
235
|
+
else
|
236
|
+
i
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def type?(key)
|
241
|
+
write "TYPE #{key}\r\n"
|
242
|
+
get_response
|
243
|
+
end
|
244
|
+
|
245
|
+
def push_tail(key, string)
|
246
|
+
write "RPUSH #{key} #{string.to_s.size}\r\n#{string.to_s}\r\n"
|
247
|
+
get_response
|
248
|
+
end
|
249
|
+
|
250
|
+
def push_head(key, string)
|
251
|
+
write "LPUSH #{key} #{string.to_s.size}\r\n#{string.to_s}\r\n"
|
252
|
+
get_response
|
253
|
+
end
|
254
|
+
|
255
|
+
def pop_head(key)
|
256
|
+
write "LPOP #{key}\r\n"
|
257
|
+
get_response
|
258
|
+
end
|
259
|
+
|
260
|
+
def pop_tail(key)
|
261
|
+
write "RPOP #{key}\r\n"
|
262
|
+
get_response
|
263
|
+
end
|
264
|
+
|
265
|
+
def list_set(key, index, val)
|
266
|
+
write "LSET #{key} #{index} #{val.to_s.size}\r\n#{val}\r\n"
|
267
|
+
get_response == OK
|
268
|
+
end
|
269
|
+
|
270
|
+
def list_range(key, start, ending)
|
271
|
+
write "LRANGE #{key} #{start} #{ending}\r\n"
|
272
|
+
get_response
|
273
|
+
end
|
274
|
+
|
275
|
+
def list_trim(key, start, ending)
|
276
|
+
write "LTRIM #{key} #{start} #{ending}\r\n"
|
277
|
+
get_response
|
278
|
+
end
|
279
|
+
|
280
|
+
def list_index(key, index)
|
281
|
+
write "LINDEX #{key} #{index}\r\n"
|
282
|
+
get_response
|
283
|
+
end
|
284
|
+
|
285
|
+
def list_rm(key, count, value)
|
286
|
+
write "LREM #{key} #{count} #{value.to_s.size}\r\n#{value}\r\n"
|
287
|
+
case num = get_response
|
288
|
+
when -1
|
289
|
+
raise RedisError, "key: #{key} does not exist"
|
290
|
+
when -2
|
291
|
+
raise RedisError, "key: #{key} does not hold a list value"
|
292
|
+
else
|
293
|
+
num
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def set_add(key, member)
|
298
|
+
write "SADD #{key} #{member.to_s.size}\r\n#{member}\r\n"
|
299
|
+
case get_response
|
300
|
+
when 1
|
301
|
+
true
|
302
|
+
when 0
|
303
|
+
false
|
304
|
+
when -2
|
305
|
+
raise RedisError, "key: #{key} contains a non set value"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def set_delete(key, member)
|
310
|
+
write "SREM #{key} #{member.to_s.size}\r\n#{member}\r\n"
|
311
|
+
case get_response
|
312
|
+
when 1
|
313
|
+
true
|
314
|
+
when 0
|
315
|
+
false
|
316
|
+
when -2
|
317
|
+
raise RedisError, "key: #{key} contains a non set value"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def set_count(key)
|
322
|
+
write "SCARD #{key}\r\n"
|
323
|
+
case i = get_response
|
324
|
+
when -2
|
325
|
+
raise RedisError, "key: #{key} contains a non set value"
|
326
|
+
else
|
327
|
+
i
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def set_member?(key, member)
|
332
|
+
write "SISMEMBER #{key} #{member.to_s.size}\r\n#{member}\r\n"
|
333
|
+
case get_response
|
334
|
+
when 1
|
335
|
+
true
|
336
|
+
when 0
|
337
|
+
false
|
338
|
+
when -2
|
339
|
+
raise RedisError, "key: #{key} contains a non set value"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def set_members(key)
|
344
|
+
write "SMEMBERS #{key}\r\n"
|
345
|
+
Set.new(get_response)
|
346
|
+
end
|
347
|
+
|
348
|
+
def set_intersect(*keys)
|
349
|
+
write "SINTER #{keys.join(' ')}\r\n"
|
350
|
+
Set.new(get_response)
|
351
|
+
end
|
352
|
+
|
353
|
+
def set_inter_store(destkey, *keys)
|
354
|
+
write "SINTERSTORE #{destkey} #{keys.join(' ')}\r\n"
|
355
|
+
get_response
|
356
|
+
end
|
357
|
+
|
358
|
+
def set_union(*keys)
|
359
|
+
write "SUNION #{keys.join(' ')}\r\n"
|
360
|
+
Set.new(get_response)
|
361
|
+
end
|
362
|
+
|
363
|
+
def set_union_store(destkey, *keys)
|
364
|
+
write "SUNIONSTORE #{destkey} #{keys.join(' ')}\r\n"
|
365
|
+
get_response
|
366
|
+
end
|
367
|
+
|
368
|
+
def set_diff(*keys)
|
369
|
+
write "SDIFF #{keys.join(' ')}\r\n"
|
370
|
+
Set.new(get_response)
|
371
|
+
end
|
372
|
+
|
373
|
+
def set_diff_store(destkey, *keys)
|
374
|
+
write "SDIFFSTORE #{destkey} #{keys.join(' ')}\r\n"
|
375
|
+
get_response
|
376
|
+
end
|
377
|
+
|
378
|
+
def sort(key, opts={})
|
379
|
+
cmd = "SORT #{key}"
|
380
|
+
cmd << " BY #{opts[:by]}" if opts[:by]
|
381
|
+
cmd << " GET #{opts[:get]}" if opts[:get]
|
382
|
+
cmd << " INCR #{opts[:incr]}" if opts[:incr]
|
383
|
+
cmd << " DEL #{opts[:del]}" if opts[:del]
|
384
|
+
cmd << " DECR #{opts[:decr]}" if opts[:decr]
|
385
|
+
cmd << " #{opts[:order]}" if opts[:order]
|
386
|
+
cmd << " LIMIT #{opts[:limit].join(' ')}" if opts[:limit]
|
387
|
+
cmd << "\r\n"
|
388
|
+
write(cmd)
|
389
|
+
get_response
|
390
|
+
end
|
391
|
+
|
392
|
+
def multi_bulk
|
393
|
+
res = read_proto
|
394
|
+
puts "mb res is #{res.inspect}" if $debug
|
395
|
+
list = []
|
396
|
+
Integer(res).times do
|
397
|
+
vf = get_response
|
398
|
+
puts "curren vf is #{vf.inspect}" if $debug
|
399
|
+
list << vf
|
400
|
+
puts "current list is #{list.inspect}" if $debug
|
401
|
+
end
|
402
|
+
list
|
403
|
+
end
|
404
|
+
|
405
|
+
def get_reply
|
406
|
+
begin
|
407
|
+
r = read(1)
|
408
|
+
raise RedisError if (r == "\r" || r == "\n")
|
409
|
+
rescue RedisError
|
410
|
+
retry
|
411
|
+
end
|
412
|
+
r
|
413
|
+
end
|
414
|
+
|
415
|
+
def []=(key, val)
|
416
|
+
set(key,val)
|
417
|
+
end
|
418
|
+
|
419
|
+
|
420
|
+
def set(key, val, expiry=nil)
|
421
|
+
write("SET #{key} #{val.to_s.size}\r\n#{val}\r\n")
|
422
|
+
s = get_response == OK
|
423
|
+
return expire(key, expiry) if s && expiry
|
424
|
+
s
|
425
|
+
end
|
426
|
+
|
427
|
+
def expire(key, expiry=nil)
|
428
|
+
write("EXPIRE #{key} #{expiry}\r\n")
|
429
|
+
get_response == 1
|
430
|
+
end
|
431
|
+
|
432
|
+
def set_unless_exists(key, val)
|
433
|
+
write "SETNX #{key} #{val.to_s.size}\r\n#{val}\r\n"
|
434
|
+
get_response == 1
|
435
|
+
end
|
436
|
+
|
437
|
+
def status_code_reply
|
438
|
+
begin
|
439
|
+
res = read_proto
|
440
|
+
if res.index('-') == 0
|
441
|
+
raise RedisError, res
|
442
|
+
else
|
443
|
+
true
|
444
|
+
end
|
445
|
+
rescue RedisError
|
446
|
+
raise RedisError
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def get_response
|
451
|
+
begin
|
452
|
+
rtype = get_reply
|
453
|
+
rescue => e
|
454
|
+
raise RedisError, e.inspect
|
455
|
+
end
|
456
|
+
puts "reply_type is #{rtype.inspect}" if $debug
|
457
|
+
case rtype
|
458
|
+
when SINGLE
|
459
|
+
single_line
|
460
|
+
when BULK
|
461
|
+
bulk_reply
|
462
|
+
when MULTI
|
463
|
+
multi_bulk
|
464
|
+
when INT
|
465
|
+
integer_reply
|
466
|
+
when ERR
|
467
|
+
raise RedisError, single_line
|
468
|
+
else
|
469
|
+
raise RedisError, "Unknown response.."
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def integer_reply
|
474
|
+
Integer(read_proto)
|
475
|
+
end
|
476
|
+
|
477
|
+
def single_line
|
478
|
+
buff = ""
|
479
|
+
while buff[-2..-1] != "\r\n"
|
480
|
+
buff << read(1)
|
481
|
+
end
|
482
|
+
puts "single_line value is #{buff[0..-3].inspect}" if $debug
|
483
|
+
buff[0..-3]
|
484
|
+
end
|
485
|
+
|
486
|
+
def read_socket
|
487
|
+
with_socket_management(@server) do |socket|
|
488
|
+
while res = socket.read(8096)
|
489
|
+
break if res.size != 8096
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def read_proto
|
495
|
+
with_socket_management(@server) do |socket|
|
496
|
+
if res = socket.gets
|
497
|
+
x = res.chomp
|
498
|
+
puts "read_proto is #{x.inspect}\n\n" if $debug
|
499
|
+
x.to_i
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
end
|