kali-redis 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,36 @@
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
+ See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb)
14
+
15
+ ## Dependencies
16
+
17
+ 1. rspec -
18
+ sudo gem install rspec
19
+
20
+ 2. redis -
21
+
22
+ rake redis:install
23
+
24
+ 2. dtach -
25
+
26
+ rake dtach:install
27
+
28
+ 3. git - git is the new black.
29
+
30
+ ## Setup
31
+
32
+ Use the tasks mentioned above (in Dependencies) to get your machine setup.
33
+
34
+ ## Examples
35
+
36
+ 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 = 'kali-redis'
10
+ GEM_NAME = 'kali'
11
+ GEM_VERSION = '0.1.1'
12
+ AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi', 'Mathieu Poumeyrol']
13
+ EMAIL = "ez@engineyard.com"
14
+ HOMEPAGE = "http://github.com/kali/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_development_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,124 @@
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
+ def keys(glob)
40
+ @ring.nodes.map do |red|
41
+ red.keys(glob)
42
+ end
43
+ end
44
+
45
+ def save
46
+ on_each_node :save
47
+ end
48
+
49
+ def bgsave
50
+ on_each_node :bgsave
51
+ end
52
+
53
+ def quit
54
+ on_each_node :quit
55
+ end
56
+
57
+ def flush_all
58
+ on_each_node :flush_all
59
+ end
60
+ alias_method :flushall, :flush_all
61
+
62
+ def flush_db
63
+ on_each_node :flush_db
64
+ end
65
+ alias_method :flushdb, :flush_db
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
+ def on_each_node(command, *args)
76
+ @ring.nodes.each do |red|
77
+ red.send(command, *args)
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+
84
+ if __FILE__ == $0
85
+
86
+ r = DistRedis.new 'localhost:6379', 'localhost:6380', 'localhost:6381', 'localhost:6382'
87
+ r['urmom'] = 'urmom'
88
+ r['urdad'] = 'urdad'
89
+ r['urmom1'] = 'urmom1'
90
+ r['urdad1'] = 'urdad1'
91
+ r['urmom2'] = 'urmom2'
92
+ r['urdad2'] = 'urdad2'
93
+ r['urmom3'] = 'urmom3'
94
+ r['urdad3'] = 'urdad3'
95
+ p r['urmom']
96
+ p r['urdad']
97
+ p r['urmom1']
98
+ p r['urdad1']
99
+ p r['urmom2']
100
+ p r['urdad2']
101
+ p r['urmom3']
102
+ p r['urdad3']
103
+
104
+ r.push_tail 'listor', 'foo1'
105
+ r.push_tail 'listor', 'foo2'
106
+ r.push_tail 'listor', 'foo3'
107
+ r.push_tail 'listor', 'foo4'
108
+ r.push_tail 'listor', 'foo5'
109
+
110
+ p r.pop_tail('listor')
111
+ p r.pop_tail('listor')
112
+ p r.pop_tail('listor')
113
+ p r.pop_tail('listor')
114
+ p r.pop_tail('listor')
115
+
116
+ puts "key distribution:"
117
+
118
+ r.ring.nodes.each do |red|
119
+ p [red.port, red.keys("*")]
120
+ end
121
+ r.delete_cloud!
122
+ p r.keys('*')
123
+
124
+ end
data/lib/hash_ring.rb ADDED
@@ -0,0 +1,135 @@
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
+ if (upper < 0) {
94
+ upper = RARRAY_LEN(ary) - 1;
95
+ }
96
+ return upper;
97
+ }
98
+ EOM
99
+ end
100
+ rescue Exception => e
101
+ # Find the closest index in HashRing with value <= the given value
102
+ def binary_search(ary, value, &block)
103
+ upper = ary.size - 1
104
+ lower = 0
105
+ idx = 0
106
+
107
+ while(lower <= upper) do
108
+ idx = (lower + upper) / 2
109
+ comp = ary[idx] <=> value
110
+
111
+ if comp == 0
112
+ return idx
113
+ elsif comp > 0
114
+ upper = idx - 1
115
+ else
116
+ lower = idx + 1
117
+ end
118
+ end
119
+
120
+ if upper < 0
121
+ upper = ary.size - 1
122
+ end
123
+ return upper
124
+ end
125
+
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ # ring = HashRing.new ['server1', 'server2', 'server3']
132
+ # p ring
133
+ # #
134
+ # p ring.get_node "kjhjkjlkjlkkh"
135
+ #
data/lib/pipeline.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Redis
2
+ class Pipeline < Redis
3
+ BUFFER_SIZE = 50_000
4
+
5
+ def initialize(redis)
6
+ @redis = redis
7
+ @commands = []
8
+ end
9
+
10
+ def call_command(command)
11
+ @commands << command
12
+ end
13
+
14
+ def execute
15
+ return if @commands.empty?
16
+ @redis.call_command(@commands)
17
+ @commands.clear
18
+ end
19
+
20
+ end
21
+ end
data/lib/redis.rb ADDED
@@ -0,0 +1,373 @@
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
+ "rpoplpush" => true,
35
+ "echo" => true,
36
+ "getset" => true,
37
+ "smove" => true,
38
+ "zadd" => true,
39
+ "zincrby" => true,
40
+ "zrem" => true,
41
+ "zscore" => true
42
+ }
43
+
44
+ MULTI_BULK_COMMANDS = {
45
+ "mset" => true,
46
+ "msetnx" => true,
47
+ "zhadd" => true,
48
+ }
49
+
50
+ BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
51
+
52
+ REPLY_PROCESSOR = {
53
+ "exists" => BOOLEAN_PROCESSOR,
54
+ "sismember" => BOOLEAN_PROCESSOR,
55
+ "sadd" => BOOLEAN_PROCESSOR,
56
+ "srem" => BOOLEAN_PROCESSOR,
57
+ "smove" => BOOLEAN_PROCESSOR,
58
+ "zadd" => BOOLEAN_PROCESSOR,
59
+ "zrem" => BOOLEAN_PROCESSOR,
60
+ "move" => BOOLEAN_PROCESSOR,
61
+ "setnx" => BOOLEAN_PROCESSOR,
62
+ "del" => BOOLEAN_PROCESSOR,
63
+ "renamenx" => BOOLEAN_PROCESSOR,
64
+ "expire" => BOOLEAN_PROCESSOR,
65
+ "keys" => lambda{|r| r.split(" ")},
66
+ "info" => lambda{|r|
67
+ info = {}
68
+ r.each_line {|kv|
69
+ k,v = kv.split(":",2).map{|x| x.chomp}
70
+ info[k.to_sym] = v
71
+ }
72
+ info
73
+ }
74
+ }
75
+
76
+ ALIASES = {
77
+ "flush_db" => "flushdb",
78
+ "flush_all" => "flushall",
79
+ "last_save" => "lastsave",
80
+ "key?" => "exists",
81
+ "delete" => "del",
82
+ "randkey" => "randomkey",
83
+ "list_length" => "llen",
84
+ "push_tail" => "rpush",
85
+ "push_head" => "lpush",
86
+ "pop_tail" => "rpop",
87
+ "pop_head" => "lpop",
88
+ "list_set" => "lset",
89
+ "list_range" => "lrange",
90
+ "list_trim" => "ltrim",
91
+ "list_index" => "lindex",
92
+ "list_rm" => "lrem",
93
+ "set_add" => "sadd",
94
+ "set_delete" => "srem",
95
+ "set_count" => "scard",
96
+ "set_member?" => "sismember",
97
+ "set_members" => "smembers",
98
+ "set_intersect" => "sinter",
99
+ "set_intersect_store" => "sinterstore",
100
+ "set_inter_store" => "sinterstore",
101
+ "set_union" => "sunion",
102
+ "set_union_store" => "sunionstore",
103
+ "set_diff" => "sdiff",
104
+ "set_diff_store" => "sdiffstore",
105
+ "set_move" => "smove",
106
+ "set_unless_exists" => "setnx",
107
+ "rename_unless_exists" => "renamenx",
108
+ "type?" => "type",
109
+ "zset_add" => "zadd",
110
+ "zset_count" => "zcard",
111
+ "zset_range_by_score" => "zrangebyscore",
112
+ "zset_reverse_range" => "zrevrange",
113
+ "zset_range" => "zrange",
114
+ "zset_delete" => "zrem",
115
+ "zset_score" => "zscore",
116
+ "zset_incr_by" => "zincrby",
117
+ "zset_increment_by" => "zincrby"
118
+ }
119
+
120
+ DISABLED_COMMANDS = {
121
+ "monitor" => true,
122
+ "sync" => true
123
+ }
124
+
125
+ def initialize(options = {})
126
+ @host = options[:host] || '127.0.0.1'
127
+ @port = (options[:port] || 6379).to_i
128
+ @db = (options[:db] || 0).to_i
129
+ @timeout = (options[:timeout] || 5).to_i
130
+ @password = options[:password]
131
+ @logger = options[:logger]
132
+ @thread_safe = options[:thread_safe]
133
+ @mutex = Mutex.new if @thread_safe
134
+ @sock = nil
135
+
136
+ @logger.info { self.to_s } if @logger
137
+ end
138
+
139
+ def to_s
140
+ "Redis Client connected to #{server} against DB #{@db}"
141
+ end
142
+
143
+ def server
144
+ "#{@host}:#{@port}"
145
+ end
146
+
147
+ def connect_to_server
148
+ @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
149
+ call_command(["auth",@password]) if @password
150
+ call_command(["select",@db]) unless @db == 0
151
+ end
152
+
153
+ def connect_to(host, port, timeout=nil)
154
+ # We support connect() timeout only if system_timer is availabe
155
+ # or if we are running against Ruby >= 1.9
156
+ # Timeout reading from the socket instead will be supported anyway.
157
+ if @timeout != 0 and RedisTimer
158
+ begin
159
+ sock = TCPSocket.new(host, port)
160
+ rescue Timeout::Error
161
+ @sock = nil
162
+ raise Timeout::Error, "Timeout connecting to the server"
163
+ end
164
+ else
165
+ sock = TCPSocket.new(host, port)
166
+ end
167
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
168
+
169
+ # If the timeout is set we set the low level socket options in order
170
+ # to make sure a blocking read will return after the specified number
171
+ # of seconds. This hack is from memcached ruby client.
172
+ if timeout
173
+ secs = Integer(timeout)
174
+ usecs = Integer((timeout - secs) * 1_000_000)
175
+ optval = [secs, usecs].pack("l_2")
176
+ begin
177
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
178
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
179
+ rescue Exception => ex
180
+ # Solaris, for one, does not like/support socket timeouts.
181
+ @logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
182
+ end
183
+ end
184
+ sock
185
+ end
186
+
187
+ def method_missing(*argv)
188
+ call_command(argv)
189
+ end
190
+
191
+ def call_command(argv)
192
+ @logger.debug { argv.inspect } if @logger
193
+
194
+ # this wrapper to raw_call_command handle reconnection on socket
195
+ # error. We try to reconnect just one time, otherwise let the error
196
+ # araise.
197
+ connect_to_server if !@sock
198
+
199
+ begin
200
+ raw_call_command(argv.dup)
201
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
202
+ @sock.close rescue nil
203
+ @sock = nil
204
+ connect_to_server
205
+ raw_call_command(argv.dup)
206
+ end
207
+ end
208
+
209
+ def raw_call_command(argvp)
210
+ pipeline = argvp[0].is_a?(Array)
211
+
212
+ unless pipeline
213
+ argvv = [argvp]
214
+ else
215
+ argvv = argvp
216
+ end
217
+
218
+ if MULTI_BULK_COMMANDS[argvv.flatten[0].to_s]
219
+ # TODO improve this code
220
+ argvp = argvv.flatten
221
+ command = ["*#{argvp.size}"]
222
+ argvp.each do |v|
223
+ v = v.to_s
224
+ command << "$#{get_size(v)}"
225
+ command << v
226
+ end
227
+ command = command.map {|cmd| "#{cmd}\r\n"}.join
228
+ else
229
+ command = ""
230
+ argvv.each do |argv|
231
+ bulk = nil
232
+ argv[0] = argv[0].to_s.downcase
233
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
234
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
235
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
236
+ bulk = argv[-1].to_s
237
+ argv[-1] = get_size(bulk)
238
+ end
239
+ command << "#{argv.join(' ')}\r\n"
240
+ command << "#{bulk}\r\n" if bulk
241
+ end
242
+ end
243
+ results = maybe_lock { process_command(command, argvv) }
244
+
245
+ return pipeline ? results : results[0]
246
+ end
247
+
248
+ def process_command(command, argvv)
249
+ @sock.write(command)
250
+ argvv.map do |argv|
251
+ processor = REPLY_PROCESSOR[argv[0]]
252
+ processor ? processor.call(read_reply) : read_reply
253
+ end
254
+ end
255
+
256
+ def maybe_lock(&block)
257
+ if @thread_safe
258
+ @mutex.synchronize &block
259
+ else
260
+ block.call
261
+ end
262
+ end
263
+
264
+ def select(*args)
265
+ raise "SELECT not allowed, use the :db option when creating the object"
266
+ end
267
+
268
+ def [](key)
269
+ self.get(key)
270
+ end
271
+
272
+ def []=(key,value)
273
+ set(key,value)
274
+ end
275
+
276
+ def set(key, value, expiry=nil)
277
+ s = call_command([:set, key, value]) == OK
278
+ expire(key, expiry) if s && expiry
279
+ s
280
+ end
281
+
282
+ def sort(key, options = {})
283
+ cmd = ["SORT"]
284
+ cmd << key
285
+ cmd << "BY #{options[:by]}" if options[:by]
286
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
287
+ cmd << "#{options[:order]}" if options[:order]
288
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
289
+ call_command(cmd)
290
+ end
291
+
292
+ def incr(key, increment = nil)
293
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
294
+ end
295
+
296
+ def decr(key,decrement = nil)
297
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
298
+ end
299
+
300
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
301
+ # keys to values.
302
+ def mapped_mget(*keys)
303
+ result = {}
304
+ mget(*keys).each do |value|
305
+ key = keys.shift
306
+ result.merge!(key => value) unless value.nil?
307
+ end
308
+ result
309
+ end
310
+
311
+ # Ruby defines a now deprecated type method so we need to override it here
312
+ # since it will never hit method_missing
313
+ def type(key)
314
+ call_command(['type', key])
315
+ end
316
+
317
+ def quit
318
+ call_command(['quit'])
319
+ rescue Errno::ECONNRESET
320
+ end
321
+
322
+ def pipelined(&block)
323
+ pipeline = Pipeline.new self
324
+ yield pipeline
325
+ pipeline.execute
326
+ end
327
+
328
+ def read_reply
329
+ # We read the first byte using read() mainly because gets() is
330
+ # immune to raw socket timeouts.
331
+ begin
332
+ rtype = @sock.read(1)
333
+ rescue Errno::EAGAIN
334
+ # We want to make sure it reconnects on the next command after the
335
+ # timeout. Otherwise the server may reply in the meantime leaving
336
+ # the protocol in a desync status.
337
+ @sock = nil
338
+ raise Errno::EAGAIN, "Timeout reading from the socket"
339
+ end
340
+
341
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
342
+ line = @sock.gets
343
+ case rtype
344
+ when MINUS
345
+ raise MINUS + line.strip
346
+ when PLUS
347
+ line.strip
348
+ when COLON
349
+ line.to_i
350
+ when DOLLAR
351
+ bulklen = line.to_i
352
+ return nil if bulklen == -1
353
+ data = @sock.read(bulklen)
354
+ @sock.read(2) # CRLF
355
+ data
356
+ when ASTERISK
357
+ objects = line.to_i
358
+ return nil if bulklen == -1
359
+ res = []
360
+ objects.times {
361
+ res << read_reply
362
+ }
363
+ res
364
+ else
365
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
366
+ end
367
+ end
368
+
369
+ private
370
+ def get_size(string)
371
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
372
+ end
373
+ end