sliceofpi-redis-rb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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-rb'
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,118 @@
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, :db => db)
16
+ end
17
+
18
+ @ring = HashRing.new hosts
19
+ end
20
+
21
+ def node_for_key(key)
22
+ if key =~ /\{(.*)?\}/
23
+ key = $1
24
+ end
25
+ @ring.get_node(key)
26
+ end
27
+
28
+ def add_server(server)
29
+ server, port = server.split(':')
30
+ @ring.add_node Redis.new(:host => server, :port => port)
31
+ end
32
+
33
+ def method_missing(sym, *args, &blk)
34
+ if redis = node_for_key(args.first.to_s)
35
+ redis.send sym, *args, &blk
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ def keys(glob)
42
+ keyz = []
43
+ @ring.nodes.each do |red|
44
+ keyz.concat red.keys(glob)
45
+ end
46
+ keyz
47
+ end
48
+
49
+ def save
50
+ @ring.nodes.each do |red|
51
+ red.save
52
+ end
53
+ end
54
+
55
+ def bgsave
56
+ @ring.nodes.each do |red|
57
+ red.bgsave
58
+ end
59
+ end
60
+
61
+ def quit
62
+ @ring.nodes.each do |red|
63
+ red.quit
64
+ end
65
+ end
66
+
67
+ def delete_cloud!
68
+ @ring.nodes.each do |red|
69
+ red.keys("*").each do |key|
70
+ red.delete key
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+
78
+ if __FILE__ == $0
79
+
80
+ r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localhost:6382'
81
+ r['urmom'] = 'urmom'
82
+ r['urdad'] = 'urdad'
83
+ r['urmom1'] = 'urmom1'
84
+ r['urdad1'] = 'urdad1'
85
+ r['urmom2'] = 'urmom2'
86
+ r['urdad2'] = 'urdad2'
87
+ r['urmom3'] = 'urmom3'
88
+ r['urdad3'] = 'urdad3'
89
+ p r['urmom']
90
+ p r['urdad']
91
+ p r['urmom1']
92
+ p r['urdad1']
93
+ p r['urmom2']
94
+ p r['urdad2']
95
+ p r['urmom3']
96
+ p r['urdad3']
97
+
98
+ r.push_tail 'listor', 'foo1'
99
+ r.push_tail 'listor', 'foo2'
100
+ r.push_tail 'listor', 'foo3'
101
+ r.push_tail 'listor', 'foo4'
102
+ r.push_tail 'listor', 'foo5'
103
+
104
+ p r.pop_tail('listor')
105
+ p r.pop_tail('listor')
106
+ p r.pop_tail('listor')
107
+ p r.pop_tail('listor')
108
+ p r.pop_tail('listor')
109
+
110
+ puts "key distribution:"
111
+
112
+ r.ring.nodes.each do |red|
113
+ p [red.port, red.keys("*")]
114
+ end
115
+ r.delete_cloud!
116
+ p r.keys('*')
117
+
118
+ 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,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,303 @@
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 == 0 ? false : r}
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
+ $debug = options[:debug]
109
+ @password = options[:password]
110
+ connect_to_server
111
+ end
112
+
113
+ def to_s
114
+ "Redis Client connected to #{@host}:#{@port} against DB #{@db}"
115
+ end
116
+
117
+ def connect_to_server
118
+ @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
119
+ call_command(["auth",@password]) if @password
120
+ call_command(["select",@db]) unless @db == 0
121
+ end
122
+
123
+ def connect_to(host, port, timeout=nil)
124
+ # We support connect() timeout only if system_timer is availabe
125
+ # or if we are running against Ruby >= 1.9
126
+ # Timeout reading from the socket instead will be supported anyway.
127
+ if @timeout != 0 and RedisTimer
128
+ begin
129
+ sock = TCPSocket.new(host, port)
130
+ rescue Timeout::Error
131
+ @sock = nil
132
+ raise Timeout::Error, "Timeout connecting to the server"
133
+ end
134
+ else
135
+ sock = TCPSocket.new(host, port)
136
+ end
137
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
138
+
139
+ # If the timeout is set we set the low level socket options in order
140
+ # to make sure a blocking read will return after the specified number
141
+ # of seconds. This hack is from memcached ruby client.
142
+ if timeout
143
+ secs = Integer(timeout)
144
+ usecs = Integer((timeout - secs) * 1_000_000)
145
+ optval = [secs, usecs].pack("l_2")
146
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
147
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
148
+ end
149
+ sock
150
+ end
151
+
152
+ def method_missing(*argv)
153
+ call_command(argv)
154
+ end
155
+
156
+ def call_command(argv)
157
+ puts argv.inspect if $debug
158
+ # this wrapper to raw_call_command handle reconnection on socket
159
+ # error. We try to reconnect just one time, otherwise let the error
160
+ # araise.
161
+ connect_to_server if !@sock
162
+ begin
163
+ raw_call_command(argv.dup)
164
+ rescue Errno::ECONNRESET, Errno::EPIPE
165
+ @sock.close
166
+ @sock = nil
167
+ connect_to_server
168
+ raw_call_command(argv.dup)
169
+ end
170
+ end
171
+
172
+ def raw_call_command(argvp)
173
+ pipeline = argvp[0].is_a?(Array)
174
+
175
+ unless pipeline
176
+ argvv = [argvp]
177
+ else
178
+ argvv = argvp
179
+ end
180
+
181
+ command = ''
182
+
183
+ argvv.each do |argv|
184
+ bulk = nil
185
+ argv[0] = argv[0].to_s.downcase
186
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
187
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
188
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
189
+ bulk = argv[-1].to_s
190
+ argv[-1] = bulk.length
191
+ end
192
+ command << argv.join(' ') + "\r\n"
193
+ command << bulk + "\r\n" if bulk
194
+ end
195
+
196
+ @sock.write(command)
197
+
198
+ results = argvv.map do |argv|
199
+ processor = REPLY_PROCESSOR[argv[0]]
200
+ processor ? processor.call(read_reply) : read_reply
201
+ end
202
+
203
+ return pipeline ? results : results[0]
204
+ end
205
+
206
+ def select(*args)
207
+ raise "SELECT not allowed, use the :db option when creating the object"
208
+ end
209
+
210
+ def [](key)
211
+ self.get(key)
212
+ end
213
+
214
+ def []=(key,value)
215
+ set(key,value)
216
+ end
217
+
218
+ def set(key, value, expiry=nil)
219
+ s = call_command([:set, key, value]) == OK
220
+ expire(key, expiry) if s && expiry
221
+ s
222
+ end
223
+
224
+ def setnx(key, value)
225
+ call_command([:setnx, key, value])
226
+ end
227
+
228
+ def sort(key, options = {})
229
+ cmd = []
230
+ cmd << "SORT #{key}"
231
+ cmd << "BY #{options[:by]}" if options[:by]
232
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
233
+ cmd << "#{options[:order]}" if options[:order]
234
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
235
+ call_command(cmd)
236
+ end
237
+
238
+ def incr(key, increment = nil)
239
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
240
+ end
241
+
242
+ def decr(key,decrement = nil)
243
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
244
+ end
245
+
246
+ # Ruby defines a now deprecated type method so we need to override it here
247
+ # since it will never hit method_missing
248
+ def type(key)
249
+ call_command(['type', key])
250
+ end
251
+
252
+ def quit
253
+ call_command(['quit'])
254
+ rescue Errno::ECONNRESET
255
+ end
256
+
257
+ def pipelined(&block)
258
+ pipeline = Pipeline.new self
259
+ yield pipeline
260
+ pipeline.execute
261
+ end
262
+
263
+ def read_reply
264
+ # We read the first byte using read() mainly because gets() is
265
+ # immune to raw socket timeouts.
266
+ begin
267
+ rtype = @sock.read(1)
268
+ rescue Errno::EAGAIN
269
+ # We want to make sure it reconnects on the next command after the
270
+ # timeout. Otherwise the server may reply in the meantime leaving
271
+ # the protocol in a desync status.
272
+ @sock = nil
273
+ raise Errno::EAGAIN, "Timeout reading from the socket"
274
+ end
275
+
276
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
277
+ line = @sock.gets
278
+ case rtype
279
+ when MINUS
280
+ raise MINUS + line.strip
281
+ when PLUS
282
+ line.strip
283
+ when COLON
284
+ line.to_i
285
+ when DOLLAR
286
+ bulklen = line.to_i
287
+ return nil if bulklen == -1
288
+ data = @sock.read(bulklen)
289
+ @sock.read(2) # CRLF
290
+ data
291
+ when ASTERISK
292
+ objects = line.to_i
293
+ return nil if bulklen == -1
294
+ res = []
295
+ objects.times {
296
+ res << read_reply
297
+ }
298
+ res
299
+ else
300
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,462 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class Foo
4
+ attr_accessor :bar
5
+ def initialize(bar)
6
+ @bar = bar
7
+ end
8
+
9
+ def ==(other)
10
+ @bar == other.bar
11
+ end
12
+ end
13
+
14
+ describe "redis" do
15
+ before(:all) do
16
+ # use database 15 for testing so we dont accidentally step on you real data
17
+ @r = Redis.new :db => 15
18
+ end
19
+
20
+ before(:each) do
21
+ @r['foo'] = 'bar'
22
+ end
23
+
24
+ after(:each) do
25
+ @r.keys('*').each {|k| @r.del k}
26
+ end
27
+
28
+ after(:all) do
29
+ @r.quit
30
+ end
31
+
32
+ it "should be able connect without a timeout" do
33
+ lambda { Redis.new :timeout => 0 }.should_not raise_error
34
+ end
35
+
36
+ it "should be able to PING" do
37
+ @r.ping.should == 'PONG'
38
+ end
39
+
40
+ it "should be able to GET a key" do
41
+ @r['foo'].should == 'bar'
42
+ end
43
+
44
+ it "should be able to SET a key" do
45
+ @r['foo'] = 'nik'
46
+ @r['foo'].should == 'nik'
47
+ end
48
+
49
+ it "should properly handle trailing newline characters" do
50
+ @r['foo'] = "bar\n"
51
+ @r['foo'].should == "bar\n"
52
+ end
53
+
54
+ it "should store and retrieve all possible characters at the beginning and the end of a string" do
55
+ (0..255).each do |char_idx|
56
+ string = "#{char_idx.chr}---#{char_idx.chr}"
57
+ @r['foo'] = string
58
+ @r['foo'].should == string
59
+ end
60
+ end
61
+
62
+ it "should be able to SET a key with an expiry" do
63
+ @r.set('foo', 'bar', 1)
64
+ @r['foo'].should == 'bar'
65
+ sleep 2
66
+ @r['foo'].should == nil
67
+ end
68
+
69
+ it "should be able to return a TTL for a key" do
70
+ @r.set('foo', 'bar', 1)
71
+ @r.ttl('foo').should == 1
72
+ end
73
+
74
+ it "should be able to SETNX" do
75
+ @r['foo'] = 'nik'
76
+ @r['foo'].should == 'nik'
77
+ @r.setnx 'foo', 'bar'
78
+ @r['foo'].should == 'nik'
79
+ end
80
+ #
81
+ it "should be able to GETSET" do
82
+ @r.getset('foo', 'baz').should == 'bar'
83
+ @r['foo'].should == 'baz'
84
+ end
85
+ #
86
+ it "should be able to INCR a key" do
87
+ @r.del('counter')
88
+ @r.incr('counter').should == 1
89
+ @r.incr('counter').should == 2
90
+ @r.incr('counter').should == 3
91
+ end
92
+ #
93
+ it "should be able to INCRBY a key" do
94
+ @r.del('counter')
95
+ @r.incrby('counter', 1).should == 1
96
+ @r.incrby('counter', 2).should == 3
97
+ @r.incrby('counter', 3).should == 6
98
+ end
99
+ #
100
+ it "should be able to DECR a key" do
101
+ @r.del('counter')
102
+ @r.incr('counter').should == 1
103
+ @r.incr('counter').should == 2
104
+ @r.incr('counter').should == 3
105
+ @r.decr('counter').should == 2
106
+ @r.decr('counter', 2).should == 0
107
+ end
108
+ #
109
+ it "should be able to RANDKEY" do
110
+ @r.randkey.should_not be_nil
111
+ end
112
+ #
113
+ it "should be able to RENAME a key" do
114
+ @r.del 'foo'
115
+ @r.del'bar'
116
+ @r['foo'] = 'hi'
117
+ @r.rename 'foo', 'bar'
118
+ @r['bar'].should == 'hi'
119
+ end
120
+ #
121
+ it "should be able to RENAMENX a key" do
122
+ @r.del 'foo'
123
+ @r.del 'bar'
124
+ @r['foo'] = 'hi'
125
+ @r['bar'] = 'ohai'
126
+ @r.renamenx 'foo', 'bar'
127
+ @r['bar'].should == 'ohai'
128
+ end
129
+ #
130
+ it "should be able to get DBSIZE of the database" do
131
+ @r.delete 'foo'
132
+ dbsize_without_foo = @r.dbsize
133
+ @r['foo'] = 0
134
+ dbsize_with_foo = @r.dbsize
135
+
136
+ dbsize_with_foo.should == dbsize_without_foo + 1
137
+ end
138
+ #
139
+ it "should be able to EXPIRE a key" do
140
+ @r['foo'] = 'bar'
141
+ @r.expire 'foo', 1
142
+ @r['foo'].should == "bar"
143
+ sleep 2
144
+ @r['foo'].should == nil
145
+ end
146
+ #
147
+ it "should be able to EXISTS" do
148
+ @r['foo'] = 'nik'
149
+ @r.exists('foo').should be_true
150
+ @r.del 'foo'
151
+ @r.exists('foo').should be_false
152
+ end
153
+ #
154
+ it "should be able to KEYS" do
155
+ @r.keys("f*").each { |key| @r.del key }
156
+ @r['f'] = 'nik'
157
+ @r['fo'] = 'nak'
158
+ @r['foo'] = 'qux'
159
+ @r.keys("f*").sort.should == ['f','fo', 'foo'].sort
160
+ end
161
+ #
162
+ it "should be able to return a random key (RANDOMKEY)" do
163
+ 3.times { @r.exists(@r.randomkey).should be_true }
164
+ end
165
+ #BTM - TODO
166
+ it "should be able to check the TYPE of a key" do
167
+ @r['foo'] = 'nik'
168
+ @r.type('foo').should == "string"
169
+ @r.del 'foo'
170
+ @r.type('foo').should == "none"
171
+ end
172
+ #
173
+ it "should be able to push to the head of a list (LPUSH)" do
174
+ @r.lpush "list", 'hello'
175
+ @r.lpush "list", 42
176
+ @r.type('list').should == "list"
177
+ @r.llen('list').should == 2
178
+ @r.lpop('list').should == '42'
179
+ end
180
+ #
181
+ it "should be able to push to the tail of a list (RPUSH)" do
182
+ @r.rpush "list", 'hello'
183
+ @r.type('list').should == "list"
184
+ @r.llen('list').should == 1
185
+ end
186
+ #
187
+ it "should be able to pop the tail of a list (RPOP)" do
188
+ @r.rpush "list", 'hello'
189
+ @r.rpush"list", 'goodbye'
190
+ @r.type('list').should == "list"
191
+ @r.llen('list').should == 2
192
+ @r.rpop('list').should == 'goodbye'
193
+ end
194
+ #
195
+ it "should be able to pop the head of a list (LPOP)" 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.lpop('list').should == 'hello'
201
+ end
202
+ #
203
+ it "should be able to get the length of a list (LLEN)" do
204
+ @r.rpush "list", 'hello'
205
+ @r.rpush "list", 'goodbye'
206
+ @r.type('list').should == "list"
207
+ @r.llen('list').should == 2
208
+ end
209
+ #
210
+ it "should be able to get a range of values from a list (LRANGE)" do
211
+ @r.rpush "list", 'hello'
212
+ @r.rpush "list", 'goodbye'
213
+ @r.rpush "list", '1'
214
+ @r.rpush "list", '2'
215
+ @r.rpush "list", '3'
216
+ @r.type('list').should == "list"
217
+ @r.llen('list').should == 5
218
+ @r.lrange('list', 2, -1).should == ['1', '2', '3']
219
+ end
220
+ #
221
+ it "should be able to trim a list (LTRIM)" do
222
+ @r.rpush "list", 'hello'
223
+ @r.rpush "list", 'goodbye'
224
+ @r.rpush "list", '1'
225
+ @r.rpush "list", '2'
226
+ @r.rpush "list", '3'
227
+ @r.type('list').should == "list"
228
+ @r.llen('list').should == 5
229
+ @r.ltrim 'list', 0, 1
230
+ @r.llen('list').should == 2
231
+ @r.lrange('list', 0, -1).should == ['hello', 'goodbye']
232
+ end
233
+ #
234
+ it "should be able to get a value by indexing into a list (LINDEX)" do
235
+ @r.rpush "list", 'hello'
236
+ @r.rpush "list", 'goodbye'
237
+ @r.type('list').should == "list"
238
+ @r.llen('list').should == 2
239
+ @r.lindex('list', 1).should == 'goodbye'
240
+ end
241
+ #
242
+ it "should be able to set a value by indexing into a list (LSET)" do
243
+ @r.rpush "list", 'hello'
244
+ @r.rpush "list", 'hello'
245
+ @r.type('list').should == "list"
246
+ @r.llen('list').should == 2
247
+ @r.lset('list', 1, 'goodbye').should == 'OK'
248
+ @r.lindex('list', 1).should == 'goodbye'
249
+ end
250
+ #
251
+ it "should be able to remove values from a list (LREM)" do
252
+ @r.rpush "list", 'hello'
253
+ @r.rpush "list", 'goodbye'
254
+ @r.type('list').should == "list"
255
+ @r.llen('list').should == 2
256
+ @r.lrem('list', 1, 'hello').should == 1
257
+ @r.lrange('list', 0, -1).should == ['goodbye']
258
+ end
259
+ #
260
+ it "should be able add members to a set (SADD)" do
261
+ @r.sadd "set", 'key1'
262
+ @r.sadd "set", 'key2'
263
+ @r.type('set').should == "set"
264
+ @r.scard('set').should == 2
265
+ @r.smembers('set').sort.should == ['key1', 'key2'].sort
266
+ end
267
+ #
268
+ it "should be able delete members to a set (SREM)" 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
+ @r.srem('set', 'key1')
275
+ @r.scard('set').should == 1
276
+ @r.smembers('set').should == ['key2']
277
+ end
278
+ #
279
+ it "should be able count the members of a set (SCARD)" do
280
+ @r.sadd "set", 'key1'
281
+ @r.sadd "set", 'key2'
282
+ @r.type('set').should == "set"
283
+ @r.scard('set').should == 2
284
+ end
285
+ #
286
+ it "should be able test for set membership (SISMEMBER)" do
287
+ @r.sadd "set", 'key1'
288
+ @r.sadd "set", 'key2'
289
+ @r.type('set').should == "set"
290
+ @r.scard('set').should == 2
291
+ @r.sismember('set', 'key1').should be_true
292
+ @r.sismember('set', 'key2').should be_true
293
+ @r.sismember('set', 'notthere').should be_false
294
+ end
295
+ #
296
+ it "should be able to do set intersection (SINTER)" do
297
+ @r.sadd "set", 'key1'
298
+ @r.sadd "set", 'key2'
299
+ @r.sadd "set2", 'key2'
300
+ @r.sinter('set', 'set2').should == ['key2']
301
+ end
302
+ #
303
+ it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do
304
+ @r.sadd "set", 'key1'
305
+ @r.sadd "set", 'key2'
306
+ @r.sadd "set2", 'key2'
307
+ @r.sinterstore('newone', 'set', 'set2').should == 1
308
+ @r.smembers('newone').should == ['key2']
309
+ end
310
+ #
311
+ it "should be able to do set union (SUNION)" do
312
+ @r.sadd "set", 'key1'
313
+ @r.sadd "set", 'key2'
314
+ @r.sadd "set2", 'key2'
315
+ @r.sadd "set2", 'key3'
316
+ @r.sunion('set', 'set2').sort.should == ['key1','key2','key3'].sort
317
+ end
318
+ #
319
+ it "should be able to do set union and store the results in a key (SUNIONSTORE)" do
320
+ @r.sadd "set", 'key1'
321
+ @r.sadd "set", 'key2'
322
+ @r.sadd "set2", 'key2'
323
+ @r.sadd "set2", 'key3'
324
+ @r.sunionstore('newone', 'set', 'set2').should == 3
325
+ @r.smembers('newone').sort.should == ['key1','key2','key3'].sort
326
+ end
327
+ #
328
+ it "should be able to do set difference (SDIFF)" do
329
+ @r.sadd "set", 'a'
330
+ @r.sadd "set", 'b'
331
+ @r.sadd "set2", 'b'
332
+ @r.sadd "set2", 'c'
333
+ @r.sdiff('set', 'set2').should == ['a']
334
+ end
335
+ #
336
+ it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do
337
+ @r.sadd "set", 'a'
338
+ @r.sadd "set", 'b'
339
+ @r.sadd "set2", 'b'
340
+ @r.sadd "set2", 'c'
341
+ @r.sdiffstore('newone', 'set', 'set2')
342
+ @r.smembers('newone').should == ['a']
343
+ end
344
+ #
345
+ it "should be able move elements from one set to another (SMOVE)" do
346
+ @r.sadd 'set1', 'a'
347
+ @r.sadd 'set1', 'b'
348
+ @r.sadd 'set2', 'x'
349
+ @r.smove('set1', 'set2', 'a').should be_true
350
+ @r.sismember('set2', 'a').should be_true
351
+ @r.delete('set1')
352
+ end
353
+ #
354
+ it "should be able to do crazy SORT queries" do
355
+ @r['dog_1'] = 'louie'
356
+ @r.rpush 'dogs', 1
357
+ @r['dog_2'] = 'lucy'
358
+ @r.rpush 'dogs', 2
359
+ @r['dog_3'] = 'max'
360
+ @r.rpush 'dogs', 3
361
+ @r['dog_4'] = 'taj'
362
+ @r.rpush 'dogs', 4
363
+ @r.sort('dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie']
364
+ @r.sort('dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj']
365
+ end
366
+
367
+ it "should be able to handle array of :get using SORT" do
368
+ @r['dog:1:name'] = 'louie'
369
+ @r['dog:1:breed'] = 'mutt'
370
+ @r.rpush 'dogs', 1
371
+ @r['dog:2:name'] = 'lucy'
372
+ @r['dog:2:breed'] = 'poodle'
373
+ @r.rpush 'dogs', 2
374
+ @r['dog:3:name'] = 'max'
375
+ @r['dog:3:breed'] = 'hound'
376
+ @r.rpush 'dogs', 3
377
+ @r['dog:4:name'] = 'taj'
378
+ @r['dog:4:breed'] = 'terrier'
379
+ @r.rpush 'dogs', 4
380
+ @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]).should == ['louie', 'mutt']
381
+ @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha').should == ['taj', 'terrier']
382
+ end
383
+ #
384
+ it "should provide info (INFO)" do
385
+ [: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|
386
+ @r.info.keys.should include(x)
387
+ end
388
+ end
389
+ #
390
+ it "should be able to flush the database (FLUSHDB)" do
391
+ @r['key1'] = 'keyone'
392
+ @r['key2'] = 'keytwo'
393
+ @r.keys('*').sort.should == ['foo', 'key1', 'key2'].sort #foo from before
394
+ @r.flushdb
395
+ @r.keys('*').should == []
396
+ end
397
+ #
398
+ it "should raise exception when manually try to change the database" do
399
+ lambda { @r.select(0) }.should raise_error
400
+ end
401
+ #
402
+ it "should be able to provide the last save time (LASTSAVE)" do
403
+ savetime = @r.lastsave
404
+ Time.at(savetime).class.should == Time
405
+ Time.at(savetime).should <= Time.now
406
+ end
407
+
408
+ it "should be able to MGET keys" do
409
+ @r['foo'] = 1000
410
+ @r['bar'] = 2000
411
+ @r.mget('foo', 'bar').should == ['1000', '2000']
412
+ @r.mget('foo', 'bar', 'baz').should == ['1000', '2000', nil]
413
+ end
414
+
415
+ it "should bgsave" do
416
+ @r.bgsave.should == 'OK'
417
+ end
418
+
419
+ it "should be able to ECHO" do
420
+ @r.echo("message in a bottle\n").should == "message in a bottle\n"
421
+ end
422
+
423
+ it "should raise error when invoke MONITOR" do
424
+ lambda { @r.monitor }.should raise_error
425
+ end
426
+
427
+ it "should raise error when invoke SYNC" do
428
+ lambda { @r.sync }.should raise_error
429
+ end
430
+
431
+ it "should handle multiple servers" do
432
+ require 'dist_redis'
433
+ @r = DistRedis.new(:hosts=> ['localhost:6379', '127.0.0.1:6379'], :db => 15)
434
+
435
+ 100.times do |idx|
436
+ @r[idx] = "foo#{idx}"
437
+ end
438
+
439
+ 100.times do |idx|
440
+ @r[idx].should == "foo#{idx}"
441
+ end
442
+ end
443
+
444
+ it "should be able to pipeline writes" do
445
+ @r.pipelined do |pipeline|
446
+ pipeline.lpush 'list', "hello"
447
+ pipeline.lpush 'list', 42
448
+ end
449
+
450
+ @r.type('list').should == "list"
451
+ @r.llen('list').should == 2
452
+ @r.lpop('list').should == '42'
453
+ end
454
+
455
+ it "should AUTH when connecting with a password" do
456
+ r = Redis.new(:password => 'secret')
457
+ r.stub!(:connect_to)
458
+ r.should_receive(:call_command).with(['auth', 'secret'])
459
+ r.connect_to_server
460
+ end
461
+
462
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ $TESTING=true
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'redis'
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sliceofpi-redis-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
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-06-23 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
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Ruby client library for redis key value storage server
65
+ test_files: []
66
+