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 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
@@ -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
@@ -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,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
+