defunkt-redis 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.markdown +34 -0
- data/Rakefile +62 -0
- data/lib/dist_redis.rb +134 -0
- data/lib/hash_ring.rb +128 -0
- data/lib/pipeline.rb +22 -0
- data/lib/redis.rb +326 -0
- data/spec/redis_spec.rb +504 -0
- data/spec/spec_helper.rb +4 -0
- metadata +67 -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,34 @@
|
|
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. rspec -
|
16
|
+
sudo gem install rspec
|
17
|
+
|
18
|
+
2. redis -
|
19
|
+
|
20
|
+
rake redis:install
|
21
|
+
|
22
|
+
2. dtach -
|
23
|
+
|
24
|
+
rake dtach:install
|
25
|
+
|
26
|
+
3. git - git is the new black.
|
27
|
+
|
28
|
+
## Setup
|
29
|
+
|
30
|
+
Use the tasks mentioned above (in Dependencies) to get your machine setup.
|
31
|
+
|
32
|
+
## Examples
|
33
|
+
|
34
|
+
Check the examples/ directory. *Note* you need to have redis-server running first.
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
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.1'
|
12
|
+
AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
|
13
|
+
EMAIL = "ez@engineyard.com"
|
14
|
+
HOMEPAGE = "http://github.com/ezmobius/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
|
+
s.add_dependency "rspec"
|
29
|
+
s.require_path = 'lib'
|
30
|
+
s.autorequire = GEM
|
31
|
+
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
desc "Run specs"
|
37
|
+
Spec::Rake::SpecTask.new do |t|
|
38
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
39
|
+
t.spec_opts = %w(-fs --color)
|
40
|
+
end
|
41
|
+
|
42
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
43
|
+
pkg.gem_spec = spec
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "install the gem locally"
|
47
|
+
task :install => [:package] do
|
48
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "create a gemspec file"
|
52
|
+
task :make_spec do
|
53
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
54
|
+
file.puts spec.to_ruby
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Run all examples with RCov"
|
59
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
60
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
61
|
+
t.rcov = true
|
62
|
+
end
|
data/lib/dist_redis.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'hash_ring'
|
3
|
+
class DistRedis
|
4
|
+
attr_reader :ring
|
5
|
+
def initialize(opts={})
|
6
|
+
hosts = []
|
7
|
+
|
8
|
+
db = opts[:db] || nil
|
9
|
+
timeout = opts[:timeout] || nil
|
10
|
+
|
11
|
+
raise Error, "No hosts given" unless opts[:hosts]
|
12
|
+
|
13
|
+
opts[:hosts].each do |h|
|
14
|
+
host, port = h.split(':')
|
15
|
+
hosts << Redis.new(:host => host, :port => port, :db => db, :timeout => timeout)
|
16
|
+
end
|
17
|
+
|
18
|
+
@ring = HashRing.new hosts
|
19
|
+
end
|
20
|
+
|
21
|
+
def node_for_key(key)
|
22
|
+
key = $1 if key =~ /\{(.*)?\}/
|
23
|
+
@ring.get_node(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_server(server)
|
27
|
+
server, port = server.split(':')
|
28
|
+
@ring.add_node Redis.new(:host => server, :port => port)
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(sym, *args, &blk)
|
32
|
+
if redis = node_for_key(args.first.to_s)
|
33
|
+
redis.send sym, *args, &blk
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
40
|
+
# since it will never hit method_missing
|
41
|
+
def type(key)
|
42
|
+
if redis = node_for_key(key)
|
43
|
+
redis.send :type, key
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def keys(glob)
|
48
|
+
keyz = []
|
49
|
+
@ring.nodes.each do |red|
|
50
|
+
keyz.concat red.keys(glob)
|
51
|
+
end
|
52
|
+
keyz
|
53
|
+
end
|
54
|
+
|
55
|
+
def save
|
56
|
+
on_each_node :save
|
57
|
+
end
|
58
|
+
|
59
|
+
def bgsave
|
60
|
+
on_each_node :bgsave
|
61
|
+
end
|
62
|
+
|
63
|
+
def quit
|
64
|
+
on_each_node :quit
|
65
|
+
end
|
66
|
+
|
67
|
+
def flush_all
|
68
|
+
on_each_node :flush_all
|
69
|
+
end
|
70
|
+
alias_method :flushall, :flush_all
|
71
|
+
|
72
|
+
def flush_db
|
73
|
+
on_each_node :flush_db
|
74
|
+
end
|
75
|
+
alias_method :flushdb, :flush_db
|
76
|
+
|
77
|
+
def delete_cloud!
|
78
|
+
@ring.nodes.each do |red|
|
79
|
+
red.keys("*").each do |key|
|
80
|
+
red.delete key
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def on_each_node(command, *args)
|
86
|
+
@ring.nodes.each do |red|
|
87
|
+
red.send(command, *args)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
if __FILE__ == $0
|
95
|
+
|
96
|
+
r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localhost:6382'
|
97
|
+
r['urmom'] = 'urmom'
|
98
|
+
r['urdad'] = 'urdad'
|
99
|
+
r['urmom1'] = 'urmom1'
|
100
|
+
r['urdad1'] = 'urdad1'
|
101
|
+
r['urmom2'] = 'urmom2'
|
102
|
+
r['urdad2'] = 'urdad2'
|
103
|
+
r['urmom3'] = 'urmom3'
|
104
|
+
r['urdad3'] = 'urdad3'
|
105
|
+
p r['urmom']
|
106
|
+
p r['urdad']
|
107
|
+
p r['urmom1']
|
108
|
+
p r['urdad1']
|
109
|
+
p r['urmom2']
|
110
|
+
p r['urdad2']
|
111
|
+
p r['urmom3']
|
112
|
+
p r['urdad3']
|
113
|
+
|
114
|
+
r.push_tail 'listor', 'foo1'
|
115
|
+
r.push_tail 'listor', 'foo2'
|
116
|
+
r.push_tail 'listor', 'foo3'
|
117
|
+
r.push_tail 'listor', 'foo4'
|
118
|
+
r.push_tail 'listor', 'foo5'
|
119
|
+
|
120
|
+
p r.pop_tail('listor')
|
121
|
+
p r.pop_tail('listor')
|
122
|
+
p r.pop_tail('listor')
|
123
|
+
p r.pop_tail('listor')
|
124
|
+
p r.pop_tail('listor')
|
125
|
+
|
126
|
+
puts "key distribution:"
|
127
|
+
|
128
|
+
r.ring.nodes.each do |red|
|
129
|
+
p [red.port, red.keys("*")]
|
130
|
+
end
|
131
|
+
r.delete_cloud!
|
132
|
+
p r.keys('*')
|
133
|
+
|
134
|
+
end
|
data/lib/hash_ring.rb
ADDED
@@ -0,0 +1,128 @@
|
|
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
|
+
@nodes.reject!{|n| n.to_s == node.to_s}
|
35
|
+
@replicas.times do |i|
|
36
|
+
key = Zlib.crc32("#{node}:#{i}")
|
37
|
+
@ring.delete(key)
|
38
|
+
@sorted_keys.reject! {|k| k == key}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# get the node in the hash ring for this key
|
43
|
+
def get_node(key)
|
44
|
+
get_node_pos(key)[0]
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_node_pos(key)
|
48
|
+
return [nil,nil] if @ring.size == 0
|
49
|
+
crc = Zlib.crc32(key)
|
50
|
+
idx = HashRing.binary_search(@sorted_keys, crc)
|
51
|
+
return [@ring[@sorted_keys[idx]], idx]
|
52
|
+
end
|
53
|
+
|
54
|
+
def iter_nodes(key)
|
55
|
+
return [nil,nil] if @ring.size == 0
|
56
|
+
node, pos = get_node_pos(key)
|
57
|
+
@sorted_keys[pos..-1].each do |k|
|
58
|
+
yield @ring[k]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class << self
|
63
|
+
|
64
|
+
# gem install RubyInline to use this code
|
65
|
+
# Native extension to perform the binary search within the hashring.
|
66
|
+
# There's a pure ruby version below so this is purely optional
|
67
|
+
# for performance. In testing 20k gets and sets, the native
|
68
|
+
# binary search shaved about 12% off the runtime (9sec -> 8sec).
|
69
|
+
begin
|
70
|
+
require 'inline'
|
71
|
+
inline do |builder|
|
72
|
+
builder.c <<-EOM
|
73
|
+
int binary_search(VALUE ary, unsigned int r) {
|
74
|
+
int upper = RARRAY_LEN(ary) - 1;
|
75
|
+
int lower = 0;
|
76
|
+
int idx = 0;
|
77
|
+
|
78
|
+
while (lower <= upper) {
|
79
|
+
idx = (lower + upper) / 2;
|
80
|
+
|
81
|
+
VALUE continuumValue = RARRAY_PTR(ary)[idx];
|
82
|
+
unsigned int l = NUM2UINT(continuumValue);
|
83
|
+
if (l == r) {
|
84
|
+
return idx;
|
85
|
+
}
|
86
|
+
else if (l > r) {
|
87
|
+
upper = idx - 1;
|
88
|
+
}
|
89
|
+
else {
|
90
|
+
lower = idx + 1;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
return upper;
|
94
|
+
}
|
95
|
+
EOM
|
96
|
+
end
|
97
|
+
rescue Exception => e
|
98
|
+
# Find the closest index in HashRing with value <= the given value
|
99
|
+
def binary_search(ary, value, &block)
|
100
|
+
upper = ary.size - 1
|
101
|
+
lower = 0
|
102
|
+
idx = 0
|
103
|
+
|
104
|
+
while(lower <= upper) do
|
105
|
+
idx = (lower + upper) / 2
|
106
|
+
comp = ary[idx] <=> value
|
107
|
+
|
108
|
+
if comp == 0
|
109
|
+
return idx
|
110
|
+
elsif comp > 0
|
111
|
+
upper = idx - 1
|
112
|
+
else
|
113
|
+
lower = idx + 1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
return upper
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# ring = HashRing.new ['server1', 'server2', 'server3']
|
125
|
+
# p ring
|
126
|
+
# #
|
127
|
+
# p ring.get_node "kjhjkjlkjlkkh"
|
128
|
+
#
|
data/lib/pipeline.rb
ADDED
@@ -0,0 +1,22 @@
|
|
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 call_command(command)
|
13
|
+
@commands << command
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
@redis.call_command(@commands)
|
18
|
+
@commands.clear
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/redis.rb
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require File.join(File.dirname(__FILE__),'pipeline')
|
3
|
+
|
4
|
+
begin
|
5
|
+
if RUBY_VERSION >= '1.9'
|
6
|
+
require 'timeout'
|
7
|
+
RedisTimer = Timeout
|
8
|
+
else
|
9
|
+
require 'system_timer'
|
10
|
+
RedisTimer = SystemTimer
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
RedisTimer = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
class Redis
|
17
|
+
OK = "OK".freeze
|
18
|
+
MINUS = "-".freeze
|
19
|
+
PLUS = "+".freeze
|
20
|
+
COLON = ":".freeze
|
21
|
+
DOLLAR = "$".freeze
|
22
|
+
ASTERISK = "*".freeze
|
23
|
+
|
24
|
+
BULK_COMMANDS = {
|
25
|
+
"set" => true,
|
26
|
+
"setnx" => true,
|
27
|
+
"rpush" => true,
|
28
|
+
"lpush" => true,
|
29
|
+
"lset" => true,
|
30
|
+
"lrem" => true,
|
31
|
+
"sadd" => true,
|
32
|
+
"srem" => true,
|
33
|
+
"sismember" => true,
|
34
|
+
"echo" => true,
|
35
|
+
"getset" => true,
|
36
|
+
"smove" => true
|
37
|
+
}
|
38
|
+
|
39
|
+
BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
|
40
|
+
|
41
|
+
REPLY_PROCESSOR = {
|
42
|
+
"exists" => BOOLEAN_PROCESSOR,
|
43
|
+
"sismember" => BOOLEAN_PROCESSOR,
|
44
|
+
"sadd" => BOOLEAN_PROCESSOR,
|
45
|
+
"srem" => BOOLEAN_PROCESSOR,
|
46
|
+
"smove" => BOOLEAN_PROCESSOR,
|
47
|
+
"move" => BOOLEAN_PROCESSOR,
|
48
|
+
"setnx" => BOOLEAN_PROCESSOR,
|
49
|
+
"del" => BOOLEAN_PROCESSOR,
|
50
|
+
"renamenx" => BOOLEAN_PROCESSOR,
|
51
|
+
"expire" => BOOLEAN_PROCESSOR,
|
52
|
+
"keys" => lambda{|r| r.split(" ")},
|
53
|
+
"info" => lambda{|r|
|
54
|
+
info = {}
|
55
|
+
r.each_line {|kv|
|
56
|
+
k,v = kv.split(":",2).map{|x| x.chomp}
|
57
|
+
info[k.to_sym] = v
|
58
|
+
}
|
59
|
+
info
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
ALIASES = {
|
64
|
+
"flush_db" => "flushdb",
|
65
|
+
"flush_all" => "flushall",
|
66
|
+
"last_save" => "lastsave",
|
67
|
+
"key?" => "exists",
|
68
|
+
"delete" => "del",
|
69
|
+
"randkey" => "randomkey",
|
70
|
+
"list_length" => "llen",
|
71
|
+
"push_tail" => "rpush",
|
72
|
+
"push_head" => "lpush",
|
73
|
+
"pop_tail" => "rpop",
|
74
|
+
"pop_head" => "lpop",
|
75
|
+
"list_set" => "lset",
|
76
|
+
"list_range" => "lrange",
|
77
|
+
"list_trim" => "ltrim",
|
78
|
+
"list_index" => "lindex",
|
79
|
+
"list_rm" => "lrem",
|
80
|
+
"set_add" => "sadd",
|
81
|
+
"set_delete" => "srem",
|
82
|
+
"set_count" => "scard",
|
83
|
+
"set_member?" => "sismember",
|
84
|
+
"set_members" => "smembers",
|
85
|
+
"set_intersect" => "sinter",
|
86
|
+
"set_intersect_store" => "sinterstore",
|
87
|
+
"set_inter_store" => "sinterstore",
|
88
|
+
"set_union" => "sunion",
|
89
|
+
"set_union_store" => "sunionstore",
|
90
|
+
"set_diff" => "sdiff",
|
91
|
+
"set_diff_store" => "sdiffstore",
|
92
|
+
"set_move" => "smove",
|
93
|
+
"set_unless_exists" => "setnx",
|
94
|
+
"rename_unless_exists" => "renamenx",
|
95
|
+
"type?" => "type"
|
96
|
+
}
|
97
|
+
|
98
|
+
DISABLED_COMMANDS = {
|
99
|
+
"monitor" => true,
|
100
|
+
"sync" => true
|
101
|
+
}
|
102
|
+
|
103
|
+
def initialize(options = {})
|
104
|
+
@host = options[:host] || '127.0.0.1'
|
105
|
+
@port = (options[:port] || 6379).to_i
|
106
|
+
@db = (options[:db] || 0).to_i
|
107
|
+
@timeout = (options[:timeout] || 5).to_i
|
108
|
+
@password = options[:password]
|
109
|
+
@logger = options[:logger]
|
110
|
+
@namespace = options[:namespace]
|
111
|
+
|
112
|
+
@logger.info { self.to_s } if @logger
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_s
|
116
|
+
"Redis Client connected to #{server} against DB #{@db}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def server
|
120
|
+
"#{@host}:#{@port}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def connect_to_server
|
124
|
+
@sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
|
125
|
+
call_command(["auth",@password]) if @password
|
126
|
+
call_command(["select",@db]) unless @db == 0
|
127
|
+
end
|
128
|
+
|
129
|
+
def connect_to(host, port, timeout=nil)
|
130
|
+
# We support connect() timeout only if system_timer is availabe
|
131
|
+
# or if we are running against Ruby >= 1.9
|
132
|
+
# Timeout reading from the socket instead will be supported anyway.
|
133
|
+
if @timeout != 0 and RedisTimer
|
134
|
+
begin
|
135
|
+
sock = TCPSocket.new(host, port)
|
136
|
+
rescue Timeout::Error
|
137
|
+
@sock = nil
|
138
|
+
raise Timeout::Error, "Timeout connecting to the server"
|
139
|
+
end
|
140
|
+
else
|
141
|
+
sock = TCPSocket.new(host, port)
|
142
|
+
end
|
143
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
144
|
+
|
145
|
+
# If the timeout is set we set the low level socket options in order
|
146
|
+
# to make sure a blocking read will return after the specified number
|
147
|
+
# of seconds. This hack is from memcached ruby client.
|
148
|
+
if timeout
|
149
|
+
secs = Integer(timeout)
|
150
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
151
|
+
optval = [secs, usecs].pack("l_2")
|
152
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
153
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
154
|
+
end
|
155
|
+
sock
|
156
|
+
end
|
157
|
+
|
158
|
+
def method_missing(*argv)
|
159
|
+
call_command(argv)
|
160
|
+
end
|
161
|
+
|
162
|
+
def call_command(argv, use_namespace = true)
|
163
|
+
@logger.debug { argv.inspect } if @logger
|
164
|
+
|
165
|
+
# this wrapper to raw_call_command handle reconnection on socket
|
166
|
+
# error. We try to reconnect just one time, otherwise let the error
|
167
|
+
# araise.
|
168
|
+
connect_to_server if !@sock
|
169
|
+
|
170
|
+
begin
|
171
|
+
raw_call_command(argv.dup, use_namespace)
|
172
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
173
|
+
@sock.close
|
174
|
+
@sock = nil
|
175
|
+
connect_to_server
|
176
|
+
raw_call_command(argv.dup)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def raw_call_command(argvp, use_namespace = true)
|
181
|
+
pipeline = argvp[0].is_a?(Array)
|
182
|
+
|
183
|
+
unless pipeline
|
184
|
+
argvv = [argvp]
|
185
|
+
else
|
186
|
+
argvv = argvp
|
187
|
+
end
|
188
|
+
|
189
|
+
command = ''
|
190
|
+
|
191
|
+
argvv.each do |argv|
|
192
|
+
bulk = nil
|
193
|
+
argv[0] = argv[0].to_s.downcase
|
194
|
+
argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
|
195
|
+
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
196
|
+
if BULK_COMMANDS[argv[0]] and argv.length > 1
|
197
|
+
bulk = argv[-1].to_s
|
198
|
+
argv[-1] = bulk.respond_to?(:bytesize) ? bulk.bytesize : bulk.size
|
199
|
+
end
|
200
|
+
|
201
|
+
if @namespace && argv[1] && use_namespace
|
202
|
+
argv[1] = "#{@namespace}:#{argv[1]}"
|
203
|
+
end
|
204
|
+
|
205
|
+
command << "#{argv.join(' ')}\r\n"
|
206
|
+
command << "#{bulk}\r\n" if bulk
|
207
|
+
end
|
208
|
+
|
209
|
+
@sock.write(command)
|
210
|
+
|
211
|
+
results = argvv.map do |argv|
|
212
|
+
processor = REPLY_PROCESSOR[argv[0]]
|
213
|
+
processor ? processor.call(read_reply) : read_reply
|
214
|
+
end
|
215
|
+
|
216
|
+
return pipeline ? results : results[0]
|
217
|
+
end
|
218
|
+
|
219
|
+
def select(*args)
|
220
|
+
raise "SELECT not allowed, use the :db option when creating the object"
|
221
|
+
end
|
222
|
+
|
223
|
+
def [](key)
|
224
|
+
self.get(key)
|
225
|
+
end
|
226
|
+
|
227
|
+
def []=(key,value)
|
228
|
+
set(key,value)
|
229
|
+
end
|
230
|
+
|
231
|
+
def set(key, value, expiry=nil)
|
232
|
+
s = call_command([:set, key, value]) == OK
|
233
|
+
expire(key, expiry) if s && expiry
|
234
|
+
s
|
235
|
+
end
|
236
|
+
|
237
|
+
def sort(key, options = {})
|
238
|
+
cmd = ["SORT"]
|
239
|
+
cmd << key
|
240
|
+
cmd << "BY #{options[:by]}" if options[:by]
|
241
|
+
cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
|
242
|
+
cmd << "#{options[:order]}" if options[:order]
|
243
|
+
cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
|
244
|
+
call_command(cmd)
|
245
|
+
end
|
246
|
+
|
247
|
+
def incr(key, increment = nil)
|
248
|
+
call_command(increment ? ["incrby",key,increment] : ["incr",key])
|
249
|
+
end
|
250
|
+
|
251
|
+
def decr(key,decrement = nil)
|
252
|
+
call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
|
253
|
+
end
|
254
|
+
|
255
|
+
# Similar to memcache.rb's #get_multi, returns a hash mapping
|
256
|
+
# keys to values.
|
257
|
+
def mapped_mget(*keys)
|
258
|
+
mget(*keys).inject({}) do |hash, value|
|
259
|
+
key = keys.shift
|
260
|
+
value.nil? ? hash : hash.merge(key => value)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def mget(*keys)
|
265
|
+
keys = keys.map { |key| "#{@namespace}:#{key}"} if @namespace
|
266
|
+
call_command([:mget] + keys, false)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
270
|
+
# since it will never hit method_missing
|
271
|
+
def type(key)
|
272
|
+
call_command(['type', key])
|
273
|
+
end
|
274
|
+
|
275
|
+
def quit
|
276
|
+
call_command(['quit'])
|
277
|
+
rescue Errno::ECONNRESET
|
278
|
+
end
|
279
|
+
|
280
|
+
def pipelined(&block)
|
281
|
+
pipeline = Pipeline.new self
|
282
|
+
yield pipeline
|
283
|
+
pipeline.execute
|
284
|
+
end
|
285
|
+
|
286
|
+
def read_reply
|
287
|
+
# We read the first byte using read() mainly because gets() is
|
288
|
+
# immune to raw socket timeouts.
|
289
|
+
begin
|
290
|
+
rtype = @sock.read(1)
|
291
|
+
rescue Errno::EAGAIN
|
292
|
+
# We want to make sure it reconnects on the next command after the
|
293
|
+
# timeout. Otherwise the server may reply in the meantime leaving
|
294
|
+
# the protocol in a desync status.
|
295
|
+
@sock = nil
|
296
|
+
raise Errno::EAGAIN, "Timeout reading from the socket"
|
297
|
+
end
|
298
|
+
|
299
|
+
raise Errno::ECONNRESET,"Connection lost" if !rtype
|
300
|
+
line = @sock.gets
|
301
|
+
case rtype
|
302
|
+
when MINUS
|
303
|
+
raise MINUS + line.strip
|
304
|
+
when PLUS
|
305
|
+
line.strip
|
306
|
+
when COLON
|
307
|
+
line.to_i
|
308
|
+
when DOLLAR
|
309
|
+
bulklen = line.to_i
|
310
|
+
return nil if bulklen == -1
|
311
|
+
data = @sock.read(bulklen)
|
312
|
+
@sock.read(2) # CRLF
|
313
|
+
data
|
314
|
+
when ASTERISK
|
315
|
+
objects = line.to_i
|
316
|
+
return nil if bulklen == -1
|
317
|
+
res = []
|
318
|
+
objects.times {
|
319
|
+
res << read_reply
|
320
|
+
}
|
321
|
+
res
|
322
|
+
else
|
323
|
+
raise "Protocol error, got '#{rtype}' as initial reply byte"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
data/spec/redis_spec.rb
ADDED
@@ -0,0 +1,504 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
class Foo
|
5
|
+
attr_accessor :bar
|
6
|
+
def initialize(bar)
|
7
|
+
@bar = bar
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
@bar == other.bar
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "redis" do
|
16
|
+
before(:all) do
|
17
|
+
# use database 15 for testing so we dont accidentally step on you real data
|
18
|
+
@r = Redis.new :db => 15
|
19
|
+
end
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
@r['foo'] = 'bar'
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:each) do
|
26
|
+
@r.flushdb
|
27
|
+
end
|
28
|
+
|
29
|
+
after(:all) do
|
30
|
+
@r.quit
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should be able connect without a timeout" do
|
34
|
+
lambda { Redis.new :timeout => 0 }.should_not raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should be able to provide a logger" do
|
38
|
+
log = StringIO.new
|
39
|
+
r = Redis.new :db => 15, :logger => Logger.new(log)
|
40
|
+
r.ping
|
41
|
+
log.string.should include("ping")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be able to PING" do
|
45
|
+
@r.ping.should == 'PONG'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be able to GET a key" do
|
49
|
+
@r['foo'].should == 'bar'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should be able to SET a key" do
|
53
|
+
@r['foo'] = 'nik'
|
54
|
+
@r['foo'].should == 'nik'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should properly handle trailing newline characters" do
|
58
|
+
@r['foo'] = "bar\n"
|
59
|
+
@r['foo'].should == "bar\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should store and retrieve all possible characters at the beginning and the end of a string" do
|
63
|
+
(0..255).each do |char_idx|
|
64
|
+
string = "#{char_idx.chr}---#{char_idx.chr}"
|
65
|
+
@r['foo'] = string
|
66
|
+
@r['foo'].should == string
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be able to SET a key with an expiry" do
|
71
|
+
@r.set('foo', 'bar', 1)
|
72
|
+
@r['foo'].should == 'bar'
|
73
|
+
sleep 2
|
74
|
+
@r['foo'].should == nil
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should be able to return a TTL for a key" do
|
78
|
+
@r.set('foo', 'bar', 1)
|
79
|
+
@r.ttl('foo').should == 1
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should be able to SETNX" do
|
83
|
+
@r['foo'] = 'nik'
|
84
|
+
@r['foo'].should == 'nik'
|
85
|
+
@r.setnx 'foo', 'bar'
|
86
|
+
@r['foo'].should == 'nik'
|
87
|
+
end
|
88
|
+
#
|
89
|
+
it "should be able to GETSET" do
|
90
|
+
@r.getset('foo', 'baz').should == 'bar'
|
91
|
+
@r['foo'].should == 'baz'
|
92
|
+
end
|
93
|
+
#
|
94
|
+
it "should be able to INCR a key" do
|
95
|
+
@r.del('counter')
|
96
|
+
@r.incr('counter').should == 1
|
97
|
+
@r.incr('counter').should == 2
|
98
|
+
@r.incr('counter').should == 3
|
99
|
+
end
|
100
|
+
#
|
101
|
+
it "should be able to INCRBY a key" do
|
102
|
+
@r.del('counter')
|
103
|
+
@r.incrby('counter', 1).should == 1
|
104
|
+
@r.incrby('counter', 2).should == 3
|
105
|
+
@r.incrby('counter', 3).should == 6
|
106
|
+
end
|
107
|
+
#
|
108
|
+
it "should be able to DECR a key" do
|
109
|
+
@r.del('counter')
|
110
|
+
@r.incr('counter').should == 1
|
111
|
+
@r.incr('counter').should == 2
|
112
|
+
@r.incr('counter').should == 3
|
113
|
+
@r.decr('counter').should == 2
|
114
|
+
@r.decr('counter', 2).should == 0
|
115
|
+
end
|
116
|
+
#
|
117
|
+
it "should be able to RANDKEY" do
|
118
|
+
@r.randkey.should_not be_nil
|
119
|
+
end
|
120
|
+
#
|
121
|
+
it "should be able to RENAME a key" do
|
122
|
+
@r.del 'foo'
|
123
|
+
@r.del'bar'
|
124
|
+
@r['foo'] = 'hi'
|
125
|
+
@r.rename 'foo', 'bar'
|
126
|
+
@r['bar'].should == 'hi'
|
127
|
+
end
|
128
|
+
#
|
129
|
+
it "should be able to RENAMENX a key" do
|
130
|
+
@r.del 'foo'
|
131
|
+
@r.del 'bar'
|
132
|
+
@r['foo'] = 'hi'
|
133
|
+
@r['bar'] = 'ohai'
|
134
|
+
@r.renamenx 'foo', 'bar'
|
135
|
+
@r['bar'].should == 'ohai'
|
136
|
+
end
|
137
|
+
#
|
138
|
+
it "should be able to get DBSIZE of the database" do
|
139
|
+
@r.delete 'foo'
|
140
|
+
dbsize_without_foo = @r.dbsize
|
141
|
+
@r['foo'] = 0
|
142
|
+
dbsize_with_foo = @r.dbsize
|
143
|
+
|
144
|
+
dbsize_with_foo.should == dbsize_without_foo + 1
|
145
|
+
end
|
146
|
+
#
|
147
|
+
it "should be able to EXPIRE a key" do
|
148
|
+
@r['foo'] = 'bar'
|
149
|
+
@r.expire 'foo', 1
|
150
|
+
@r['foo'].should == "bar"
|
151
|
+
sleep 2
|
152
|
+
@r['foo'].should == nil
|
153
|
+
end
|
154
|
+
#
|
155
|
+
it "should be able to EXISTS" do
|
156
|
+
@r['foo'] = 'nik'
|
157
|
+
@r.exists('foo').should be_true
|
158
|
+
@r.del 'foo'
|
159
|
+
@r.exists('foo').should be_false
|
160
|
+
end
|
161
|
+
#
|
162
|
+
it "should be able to KEYS" do
|
163
|
+
@r.keys("f*").each { |key| @r.del key }
|
164
|
+
@r['f'] = 'nik'
|
165
|
+
@r['fo'] = 'nak'
|
166
|
+
@r['foo'] = 'qux'
|
167
|
+
@r.keys("f*").sort.should == ['f','fo', 'foo'].sort
|
168
|
+
end
|
169
|
+
#
|
170
|
+
it "should be able to return a random key (RANDOMKEY)" do
|
171
|
+
3.times { @r.exists(@r.randomkey).should be_true }
|
172
|
+
end
|
173
|
+
#
|
174
|
+
it "should be able to check the TYPE of a key" do
|
175
|
+
@r['foo'] = 'nik'
|
176
|
+
@r.type('foo').should == "string"
|
177
|
+
@r.del 'foo'
|
178
|
+
@r.type('foo').should == "none"
|
179
|
+
end
|
180
|
+
#
|
181
|
+
it "should be able to push to the head of a list (LPUSH)" do
|
182
|
+
@r.lpush "list", 'hello'
|
183
|
+
@r.lpush "list", 42
|
184
|
+
@r.type('list').should == "list"
|
185
|
+
@r.llen('list').should == 2
|
186
|
+
@r.lpop('list').should == '42'
|
187
|
+
end
|
188
|
+
#
|
189
|
+
it "should be able to push to the tail of a list (RPUSH)" do
|
190
|
+
@r.rpush "list", 'hello'
|
191
|
+
@r.type('list').should == "list"
|
192
|
+
@r.llen('list').should == 1
|
193
|
+
end
|
194
|
+
#
|
195
|
+
it "should be able to pop the tail of a list (RPOP)" do
|
196
|
+
@r.rpush "list", 'hello'
|
197
|
+
@r.rpush"list", 'goodbye'
|
198
|
+
@r.type('list').should == "list"
|
199
|
+
@r.llen('list').should == 2
|
200
|
+
@r.rpop('list').should == 'goodbye'
|
201
|
+
end
|
202
|
+
#
|
203
|
+
it "should be able to pop the head of a list (LPOP)" do
|
204
|
+
@r.rpush "list", 'hello'
|
205
|
+
@r.rpush "list", 'goodbye'
|
206
|
+
@r.type('list').should == "list"
|
207
|
+
@r.llen('list').should == 2
|
208
|
+
@r.lpop('list').should == 'hello'
|
209
|
+
end
|
210
|
+
#
|
211
|
+
it "should be able to get the length of a list (LLEN)" do
|
212
|
+
@r.rpush "list", 'hello'
|
213
|
+
@r.rpush "list", 'goodbye'
|
214
|
+
@r.type('list').should == "list"
|
215
|
+
@r.llen('list').should == 2
|
216
|
+
end
|
217
|
+
#
|
218
|
+
it "should be able to get a range of values from a list (LRANGE)" do
|
219
|
+
@r.rpush "list", 'hello'
|
220
|
+
@r.rpush "list", 'goodbye'
|
221
|
+
@r.rpush "list", '1'
|
222
|
+
@r.rpush "list", '2'
|
223
|
+
@r.rpush "list", '3'
|
224
|
+
@r.type('list').should == "list"
|
225
|
+
@r.llen('list').should == 5
|
226
|
+
@r.lrange('list', 2, -1).should == ['1', '2', '3']
|
227
|
+
end
|
228
|
+
#
|
229
|
+
it "should be able to trim a list (LTRIM)" do
|
230
|
+
@r.rpush "list", 'hello'
|
231
|
+
@r.rpush "list", 'goodbye'
|
232
|
+
@r.rpush "list", '1'
|
233
|
+
@r.rpush "list", '2'
|
234
|
+
@r.rpush "list", '3'
|
235
|
+
@r.type('list').should == "list"
|
236
|
+
@r.llen('list').should == 5
|
237
|
+
@r.ltrim 'list', 0, 1
|
238
|
+
@r.llen('list').should == 2
|
239
|
+
@r.lrange('list', 0, -1).should == ['hello', 'goodbye']
|
240
|
+
end
|
241
|
+
#
|
242
|
+
it "should be able to get a value by indexing into a list (LINDEX)" do
|
243
|
+
@r.rpush "list", 'hello'
|
244
|
+
@r.rpush "list", 'goodbye'
|
245
|
+
@r.type('list').should == "list"
|
246
|
+
@r.llen('list').should == 2
|
247
|
+
@r.lindex('list', 1).should == 'goodbye'
|
248
|
+
end
|
249
|
+
#
|
250
|
+
it "should be able to set a value by indexing into a list (LSET)" do
|
251
|
+
@r.rpush "list", 'hello'
|
252
|
+
@r.rpush "list", 'hello'
|
253
|
+
@r.type('list').should == "list"
|
254
|
+
@r.llen('list').should == 2
|
255
|
+
@r.lset('list', 1, 'goodbye').should == 'OK'
|
256
|
+
@r.lindex('list', 1).should == 'goodbye'
|
257
|
+
end
|
258
|
+
#
|
259
|
+
it "should be able to remove values from a list (LREM)" do
|
260
|
+
@r.rpush "list", 'hello'
|
261
|
+
@r.rpush "list", 'goodbye'
|
262
|
+
@r.type('list').should == "list"
|
263
|
+
@r.llen('list').should == 2
|
264
|
+
@r.lrem('list', 1, 'hello').should == 1
|
265
|
+
@r.lrange('list', 0, -1).should == ['goodbye']
|
266
|
+
end
|
267
|
+
#
|
268
|
+
it "should be able add members to a set (SADD)" do
|
269
|
+
@r.sadd "set", 'key1'
|
270
|
+
@r.sadd "set", 'key2'
|
271
|
+
@r.type('set').should == "set"
|
272
|
+
@r.scard('set').should == 2
|
273
|
+
@r.smembers('set').sort.should == ['key1', 'key2'].sort
|
274
|
+
end
|
275
|
+
#
|
276
|
+
it "should be able delete members to a set (SREM)" do
|
277
|
+
@r.sadd "set", 'key1'
|
278
|
+
@r.sadd "set", 'key2'
|
279
|
+
@r.type('set').should == "set"
|
280
|
+
@r.scard('set').should == 2
|
281
|
+
@r.smembers('set').sort.should == ['key1', 'key2'].sort
|
282
|
+
@r.srem('set', 'key1')
|
283
|
+
@r.scard('set').should == 1
|
284
|
+
@r.smembers('set').should == ['key2']
|
285
|
+
end
|
286
|
+
#
|
287
|
+
it "should be able count the members of a set (SCARD)" do
|
288
|
+
@r.sadd "set", 'key1'
|
289
|
+
@r.sadd "set", 'key2'
|
290
|
+
@r.type('set').should == "set"
|
291
|
+
@r.scard('set').should == 2
|
292
|
+
end
|
293
|
+
#
|
294
|
+
it "should be able test for set membership (SISMEMBER)" do
|
295
|
+
@r.sadd "set", 'key1'
|
296
|
+
@r.sadd "set", 'key2'
|
297
|
+
@r.type('set').should == "set"
|
298
|
+
@r.scard('set').should == 2
|
299
|
+
@r.sismember('set', 'key1').should be_true
|
300
|
+
@r.sismember('set', 'key2').should be_true
|
301
|
+
@r.sismember('set', 'notthere').should be_false
|
302
|
+
end
|
303
|
+
#
|
304
|
+
it "should be able to do set intersection (SINTER)" do
|
305
|
+
@r.sadd "set", 'key1'
|
306
|
+
@r.sadd "set", 'key2'
|
307
|
+
@r.sadd "set2", 'key2'
|
308
|
+
@r.sinter('set', 'set2').should == ['key2']
|
309
|
+
end
|
310
|
+
#
|
311
|
+
it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do
|
312
|
+
@r.sadd "set", 'key1'
|
313
|
+
@r.sadd "set", 'key2'
|
314
|
+
@r.sadd "set2", 'key2'
|
315
|
+
@r.sinterstore('newone', 'set', 'set2').should == 1
|
316
|
+
@r.smembers('newone').should == ['key2']
|
317
|
+
end
|
318
|
+
#
|
319
|
+
it "should be able to do set union (SUNION)" do
|
320
|
+
@r.sadd "set", 'key1'
|
321
|
+
@r.sadd "set", 'key2'
|
322
|
+
@r.sadd "set2", 'key2'
|
323
|
+
@r.sadd "set2", 'key3'
|
324
|
+
@r.sunion('set', 'set2').sort.should == ['key1','key2','key3'].sort
|
325
|
+
end
|
326
|
+
#
|
327
|
+
it "should be able to do set union and store the results in a key (SUNIONSTORE)" do
|
328
|
+
@r.sadd "set", 'key1'
|
329
|
+
@r.sadd "set", 'key2'
|
330
|
+
@r.sadd "set2", 'key2'
|
331
|
+
@r.sadd "set2", 'key3'
|
332
|
+
@r.sunionstore('newone', 'set', 'set2').should == 3
|
333
|
+
@r.smembers('newone').sort.should == ['key1','key2','key3'].sort
|
334
|
+
end
|
335
|
+
#
|
336
|
+
it "should be able to do set difference (SDIFF)" do
|
337
|
+
@r.sadd "set", 'a'
|
338
|
+
@r.sadd "set", 'b'
|
339
|
+
@r.sadd "set2", 'b'
|
340
|
+
@r.sadd "set2", 'c'
|
341
|
+
@r.sdiff('set', 'set2').should == ['a']
|
342
|
+
end
|
343
|
+
#
|
344
|
+
it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do
|
345
|
+
@r.sadd "set", 'a'
|
346
|
+
@r.sadd "set", 'b'
|
347
|
+
@r.sadd "set2", 'b'
|
348
|
+
@r.sadd "set2", 'c'
|
349
|
+
@r.sdiffstore('newone', 'set', 'set2')
|
350
|
+
@r.smembers('newone').should == ['a']
|
351
|
+
end
|
352
|
+
#
|
353
|
+
it "should be able move elements from one set to another (SMOVE)" do
|
354
|
+
@r.sadd 'set1', 'a'
|
355
|
+
@r.sadd 'set1', 'b'
|
356
|
+
@r.sadd 'set2', 'x'
|
357
|
+
@r.smove('set1', 'set2', 'a').should be_true
|
358
|
+
@r.sismember('set2', 'a').should be_true
|
359
|
+
@r.delete('set1')
|
360
|
+
end
|
361
|
+
#
|
362
|
+
it "should be able to do crazy SORT queries" do
|
363
|
+
# The 'Dogs' is capitialized on purpose
|
364
|
+
@r['dog_1'] = 'louie'
|
365
|
+
@r.rpush 'Dogs', 1
|
366
|
+
@r['dog_2'] = 'lucy'
|
367
|
+
@r.rpush 'Dogs', 2
|
368
|
+
@r['dog_3'] = 'max'
|
369
|
+
@r.rpush 'Dogs', 3
|
370
|
+
@r['dog_4'] = 'taj'
|
371
|
+
@r.rpush 'Dogs', 4
|
372
|
+
@r.sort('Dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie']
|
373
|
+
@r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj']
|
374
|
+
end
|
375
|
+
|
376
|
+
it "should be able to handle array of :get using SORT" do
|
377
|
+
@r['dog:1:name'] = 'louie'
|
378
|
+
@r['dog:1:breed'] = 'mutt'
|
379
|
+
@r.rpush 'dogs', 1
|
380
|
+
@r['dog:2:name'] = 'lucy'
|
381
|
+
@r['dog:2:breed'] = 'poodle'
|
382
|
+
@r.rpush 'dogs', 2
|
383
|
+
@r['dog:3:name'] = 'max'
|
384
|
+
@r['dog:3:breed'] = 'hound'
|
385
|
+
@r.rpush 'dogs', 3
|
386
|
+
@r['dog:4:name'] = 'taj'
|
387
|
+
@r['dog:4:breed'] = 'terrier'
|
388
|
+
@r.rpush 'dogs', 4
|
389
|
+
@r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]).should == ['louie', 'mutt']
|
390
|
+
@r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha').should == ['taj', 'terrier']
|
391
|
+
end
|
392
|
+
#
|
393
|
+
it "should provide info (INFO)" do
|
394
|
+
[: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|
|
395
|
+
@r.info.keys.should include(x)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
#
|
399
|
+
it "should be able to flush the database (FLUSHDB)" do
|
400
|
+
@r['key1'] = 'keyone'
|
401
|
+
@r['key2'] = 'keytwo'
|
402
|
+
@r.keys('*').sort.should == ['foo', 'key1', 'key2'].sort #foo from before
|
403
|
+
@r.flushdb
|
404
|
+
@r.keys('*').should == []
|
405
|
+
end
|
406
|
+
#
|
407
|
+
it "should raise exception when manually try to change the database" do
|
408
|
+
lambda { @r.select(0) }.should raise_error
|
409
|
+
end
|
410
|
+
#
|
411
|
+
it "should be able to provide the last save time (LASTSAVE)" do
|
412
|
+
savetime = @r.lastsave
|
413
|
+
Time.at(savetime).class.should == Time
|
414
|
+
Time.at(savetime).should <= Time.now
|
415
|
+
end
|
416
|
+
|
417
|
+
it "should be able to MGET keys" do
|
418
|
+
@r['foo'] = 1000
|
419
|
+
@r['bar'] = 2000
|
420
|
+
@r.mget('foo', 'bar').should == ['1000', '2000']
|
421
|
+
@r.mget('foo', 'bar', 'baz').should == ['1000', '2000', nil]
|
422
|
+
end
|
423
|
+
|
424
|
+
it "should be able to mapped MGET keys" do
|
425
|
+
@r['foo'] = 1000
|
426
|
+
@r['bar'] = 2000
|
427
|
+
@r.mapped_mget('foo', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
|
428
|
+
@r.mapped_mget('foo', 'baz', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
|
429
|
+
end
|
430
|
+
|
431
|
+
it "should bgsave" do
|
432
|
+
@r.bgsave.should == 'OK'
|
433
|
+
end
|
434
|
+
|
435
|
+
it "should be able to ECHO" do
|
436
|
+
@r.echo("message in a bottle\n").should == "message in a bottle\n"
|
437
|
+
end
|
438
|
+
|
439
|
+
it "should raise error when invoke MONITOR" do
|
440
|
+
lambda { @r.monitor }.should raise_error
|
441
|
+
end
|
442
|
+
|
443
|
+
it "should raise error when invoke SYNC" do
|
444
|
+
lambda { @r.sync }.should raise_error
|
445
|
+
end
|
446
|
+
|
447
|
+
it "should handle multiple servers" do
|
448
|
+
require 'dist_redis'
|
449
|
+
@r = DistRedis.new(:hosts=> ['localhost:6379', '127.0.0.1:6379'], :db => 15)
|
450
|
+
|
451
|
+
100.times do |idx|
|
452
|
+
@r[idx] = "foo#{idx}"
|
453
|
+
end
|
454
|
+
|
455
|
+
100.times do |idx|
|
456
|
+
@r[idx].should == "foo#{idx}"
|
457
|
+
end
|
458
|
+
|
459
|
+
@r.keys('*').sort.uniq.should == ('0'...'100').to_a.sort + ['foo']
|
460
|
+
end
|
461
|
+
|
462
|
+
it "should be able to pipeline writes" do
|
463
|
+
@r.pipelined do |pipeline|
|
464
|
+
pipeline.lpush 'list', "hello"
|
465
|
+
pipeline.lpush 'list', 42
|
466
|
+
end
|
467
|
+
|
468
|
+
@r.type('list').should == "list"
|
469
|
+
@r.llen('list').should == 2
|
470
|
+
@r.lpop('list').should == '42'
|
471
|
+
end
|
472
|
+
|
473
|
+
it "should AUTH when connecting with a password" do
|
474
|
+
r = Redis.new(:password => 'secret')
|
475
|
+
r.stub!(:connect_to)
|
476
|
+
r.should_receive(:call_command).with(['auth', 'secret'])
|
477
|
+
r.connect_to_server
|
478
|
+
end
|
479
|
+
|
480
|
+
it "should be able to use a namespace" do
|
481
|
+
r = Redis.new(:namespace => :ns, :db => 15)
|
482
|
+
r.flushdb
|
483
|
+
|
484
|
+
r['foo'].should == nil
|
485
|
+
r['foo'] = 'chris'
|
486
|
+
r['foo'].should == 'chris'
|
487
|
+
@r['foo'] = 'bob'
|
488
|
+
@r['foo'].should == 'bob'
|
489
|
+
|
490
|
+
r.incr('counter', 2)
|
491
|
+
r['counter'].to_i.should == 2
|
492
|
+
@r['counter'].should == nil
|
493
|
+
end
|
494
|
+
|
495
|
+
it "should be able to use a namespace with mget" do
|
496
|
+
r = Redis.new(:namespace => :ns, :db => 15)
|
497
|
+
|
498
|
+
r['foo'] = 1000
|
499
|
+
r['bar'] = 2000
|
500
|
+
r.mapped_mget('foo', 'bar').should == { 'foo' => '1000', 'bar' => '2000' }
|
501
|
+
r.mapped_mget('foo', 'baz', 'bar').should == { 'foo' => '1000', 'bar' => '2000' }
|
502
|
+
end
|
503
|
+
|
504
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: defunkt-redis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.2"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ezra Zygmuntowicz
|
8
|
+
- Taylor Weibley
|
9
|
+
- Matthew Clark
|
10
|
+
- Brian McKinney
|
11
|
+
- Salvatore Sanfilippo
|
12
|
+
- Luca Guidi
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2009-08-26 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Ruby client library for redis key value storage server
|
22
|
+
email: ez@engineyard.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- LICENSE
|
29
|
+
files:
|
30
|
+
- LICENSE
|
31
|
+
- README.markdown
|
32
|
+
- Rakefile
|
33
|
+
- lib/dist_redis.rb
|
34
|
+
- lib/hash_ring.rb
|
35
|
+
- lib/pipeline.rb
|
36
|
+
- lib/redis.rb
|
37
|
+
- spec/redis_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/ezmobius/redis-rb
|
41
|
+
licenses:
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.3.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 2
|
65
|
+
summary: Ruby client library for redis key value storage server
|
66
|
+
test_files: []
|
67
|
+
|