redis 0.1.2 → 0.2.0

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/Rakefile CHANGED
@@ -5,10 +5,12 @@ require 'date'
5
5
  require 'spec/rake/spectask'
6
6
  require 'tasks/redis.tasks'
7
7
 
8
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
9
+ require 'redis'
8
10
 
9
11
  GEM = 'redis'
10
12
  GEM_NAME = 'redis'
11
- GEM_VERSION = '0.1.2'
13
+ GEM_VERSION = RedisRb::VERSION
12
14
  AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
13
15
  EMAIL = "ez@engineyard.com"
14
16
  HOMEPAGE = "http://github.com/ezmobius/redis-rb"
data/lib/edis.rb ADDED
@@ -0,0 +1,3 @@
1
+ # This file allows for the running of redis-rb with a nice
2
+ # command line look-and-feel: irb -rubygems -redis foo.rb
3
+ require 'redis'
@@ -0,0 +1,414 @@
1
+ module RedisRb
2
+ class Client
3
+ OK = "OK".freeze
4
+ MINUS = "-".freeze
5
+ PLUS = "+".freeze
6
+ COLON = ":".freeze
7
+ DOLLAR = "$".freeze
8
+ ASTERISK = "*".freeze
9
+
10
+ BULK_COMMANDS = {
11
+ "set" => true,
12
+ "setnx" => true,
13
+ "rpush" => true,
14
+ "lpush" => true,
15
+ "lset" => true,
16
+ "lrem" => true,
17
+ "sadd" => true,
18
+ "srem" => true,
19
+ "sismember" => true,
20
+ "echo" => true,
21
+ "getset" => true,
22
+ "smove" => true,
23
+ "zadd" => true,
24
+ "zincrby" => true,
25
+ "zrem" => true,
26
+ "zscore" => true,
27
+ "zrank" => true,
28
+ "zrevrank" => true,
29
+ "hget" => true,
30
+ "hdel" => true,
31
+ "hexists" => true
32
+ }
33
+
34
+ MULTI_BULK_COMMANDS = {
35
+ "mset" => true,
36
+ "msetnx" => true,
37
+ "hset" => true
38
+ }
39
+
40
+ BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
41
+
42
+ REPLY_PROCESSOR = {
43
+ "exists" => BOOLEAN_PROCESSOR,
44
+ "sismember" => BOOLEAN_PROCESSOR,
45
+ "sadd" => BOOLEAN_PROCESSOR,
46
+ "srem" => BOOLEAN_PROCESSOR,
47
+ "smove" => BOOLEAN_PROCESSOR,
48
+ "zadd" => BOOLEAN_PROCESSOR,
49
+ "zrem" => BOOLEAN_PROCESSOR,
50
+ "move" => BOOLEAN_PROCESSOR,
51
+ "setnx" => BOOLEAN_PROCESSOR,
52
+ "del" => BOOLEAN_PROCESSOR,
53
+ "renamenx" => BOOLEAN_PROCESSOR,
54
+ "expire" => BOOLEAN_PROCESSOR,
55
+ "hset" => BOOLEAN_PROCESSOR,
56
+ "hexists" => BOOLEAN_PROCESSOR,
57
+ "info" => lambda{|r|
58
+ info = {}
59
+ r.each_line {|kv|
60
+ k,v = kv.split(":",2).map{|x| x.chomp}
61
+ info[k.to_sym] = v
62
+ }
63
+ info
64
+ },
65
+ "keys" => lambda{|r|
66
+ if r.is_a?(Array)
67
+ r
68
+ else
69
+ r.split(" ")
70
+ end
71
+ },
72
+ "hgetall" => lambda{|r|
73
+ Hash[*r]
74
+ }
75
+ }
76
+
77
+ ALIASES = {
78
+ "flush_db" => "flushdb",
79
+ "flush_all" => "flushall",
80
+ "last_save" => "lastsave",
81
+ "key?" => "exists",
82
+ "delete" => "del",
83
+ "randkey" => "randomkey",
84
+ "list_length" => "llen",
85
+ "push_tail" => "rpush",
86
+ "push_head" => "lpush",
87
+ "pop_tail" => "rpop",
88
+ "pop_head" => "lpop",
89
+ "list_set" => "lset",
90
+ "list_range" => "lrange",
91
+ "list_trim" => "ltrim",
92
+ "list_index" => "lindex",
93
+ "list_rm" => "lrem",
94
+ "set_add" => "sadd",
95
+ "set_delete" => "srem",
96
+ "set_count" => "scard",
97
+ "set_member?" => "sismember",
98
+ "set_members" => "smembers",
99
+ "set_intersect" => "sinter",
100
+ "set_intersect_store" => "sinterstore",
101
+ "set_inter_store" => "sinterstore",
102
+ "set_union" => "sunion",
103
+ "set_union_store" => "sunionstore",
104
+ "set_diff" => "sdiff",
105
+ "set_diff_store" => "sdiffstore",
106
+ "set_move" => "smove",
107
+ "set_unless_exists" => "setnx",
108
+ "rename_unless_exists" => "renamenx",
109
+ "type?" => "type",
110
+ "zset_add" => "zadd",
111
+ "zset_count" => "zcard",
112
+ "zset_range_by_score" => "zrangebyscore",
113
+ "zset_reverse_range" => "zrevrange",
114
+ "zset_range" => "zrange",
115
+ "zset_delete" => "zrem",
116
+ "zset_score" => "zscore",
117
+ "zset_incr_by" => "zincrby",
118
+ "zset_increment_by" => "zincrby"
119
+ }
120
+
121
+ DISABLED_COMMANDS = {
122
+ "monitor" => true,
123
+ "sync" => true
124
+ }
125
+
126
+ def initialize(options = {})
127
+ @host = options[:host] || '127.0.0.1'
128
+ @port = (options[:port] || 6379).to_i
129
+ @db = (options[:db] || 0).to_i
130
+ @timeout = (options[:timeout] || 5).to_i
131
+ @password = options[:password]
132
+ @logger = options[:logger]
133
+ @thread_safe = options[:thread_safe]
134
+ @binary_keys = options[:binary_keys]
135
+ @mutex = Mutex.new if @thread_safe
136
+ @sock = nil
137
+
138
+ @logger.info { self.to_s } if @logger
139
+ end
140
+
141
+ def to_s
142
+ "Redis Client connected to #{server} against DB #{@db}"
143
+ end
144
+
145
+ def server
146
+ "#{@host}:#{@port}"
147
+ end
148
+
149
+ def connect_to_server
150
+ @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
151
+ call_command(["auth",@password]) if @password
152
+ call_command(["select",@db]) unless @db == 0
153
+ end
154
+
155
+ def connect_to(host, port, timeout=nil)
156
+ # We support connect() timeout only if system_timer is availabe
157
+ # or if we are running against Ruby >= 1.9
158
+ # Timeout reading from the socket instead will be supported anyway.
159
+ if @timeout != 0 and RedisTimer
160
+ begin
161
+ sock = TCPSocket.new(host, port)
162
+ rescue Timeout::Error
163
+ @sock = nil
164
+ raise Timeout::Error, "Timeout connecting to the server"
165
+ end
166
+ else
167
+ sock = TCPSocket.new(host, port)
168
+ end
169
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
170
+
171
+ # If the timeout is set we set the low level socket options in order
172
+ # to make sure a blocking read will return after the specified number
173
+ # of seconds. This hack is from memcached ruby client.
174
+ if timeout
175
+ secs = Integer(timeout)
176
+ usecs = Integer((timeout - secs) * 1_000_000)
177
+ optval = [secs, usecs].pack("l_2")
178
+ begin
179
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
180
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
181
+ rescue Exception => ex
182
+ # Solaris, for one, does not like/support socket timeouts.
183
+ @logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
184
+ end
185
+ end
186
+ sock
187
+ end
188
+
189
+ def method_missing(*argv)
190
+ call_command(argv)
191
+ end
192
+
193
+ def call_command(argv)
194
+ @logger.debug { argv.inspect } if @logger
195
+
196
+ # this wrapper to raw_call_command handle reconnection on socket
197
+ # error. We try to reconnect just one time, otherwise let the error
198
+ # araise.
199
+ connect_to_server if !@sock
200
+
201
+ begin
202
+ raw_call_command(argv.dup)
203
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
204
+ @sock.close rescue nil
205
+ @sock = nil
206
+ connect_to_server
207
+ raw_call_command(argv.dup)
208
+ end
209
+ end
210
+
211
+ def raw_call_command(argvp)
212
+ if argvp[0].is_a?(Array)
213
+ argvv = argvp
214
+ pipeline = true
215
+ else
216
+ argvv = [argvp]
217
+ end
218
+
219
+ if @binary_keys or MULTI_BULK_COMMANDS[argvv[0][0].to_s]
220
+ command = ""
221
+ argvv.each do |argv|
222
+ command << "*#{argv.size}\r\n"
223
+ argv.each{|a|
224
+ a = a.to_s
225
+ command << "$#{get_size(a)}\r\n"
226
+ command << a
227
+ command << "\r\n"
228
+ }
229
+ end
230
+ else
231
+ command = ""
232
+ argvv.each do |argv|
233
+ bulk = nil
234
+ argv[0] = argv[0].to_s.downcase
235
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
236
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
237
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
238
+ bulk = argv[-1].to_s
239
+ argv[-1] = get_size(bulk)
240
+ end
241
+ command << "#{argv.join(' ')}\r\n"
242
+ command << "#{bulk}\r\n" if bulk
243
+ end
244
+ end
245
+ results = maybe_lock { process_command(command, argvv) }
246
+
247
+ return pipeline ? results : results[0]
248
+ end
249
+
250
+ def process_command(command, argvv)
251
+ @sock.write(command)
252
+ argvv.map do |argv|
253
+ processor = REPLY_PROCESSOR[argv[0].to_s]
254
+ processor ? processor.call(read_reply) : read_reply
255
+ end
256
+ end
257
+
258
+ def maybe_lock(&block)
259
+ if @thread_safe
260
+ @mutex.synchronize(&block)
261
+ else
262
+ block.call
263
+ end
264
+ end
265
+
266
+ def select(*args)
267
+ raise "SELECT not allowed, use the :db option when creating the object"
268
+ end
269
+
270
+ def [](key)
271
+ self.get(key)
272
+ end
273
+
274
+ def []=(key,value)
275
+ set(key,value)
276
+ end
277
+
278
+ def set(key, value, expiry=nil)
279
+ s = call_command([:set, key, value]) == OK
280
+ expire(key, expiry) if s && expiry
281
+ s
282
+ end
283
+
284
+ def mset(*args)
285
+ hsh = args.pop if Hash === args.last
286
+ if hsh
287
+ call_command(hsh.to_a.flatten.unshift(:mset))
288
+ else
289
+ call_command(args.unshift(:mset))
290
+ end
291
+ end
292
+
293
+ def msetnx(*args)
294
+ hsh = args.pop if Hash === args.last
295
+ if hsh
296
+ call_command(hsh.to_a.flatten.unshift(:msetnx))
297
+ else
298
+ call_command(args.unshift(:msetnx))
299
+ end
300
+ end
301
+
302
+ def sort(key, options = {})
303
+ cmd = ["SORT"]
304
+ cmd << key
305
+ cmd << "BY #{options[:by]}" if options[:by]
306
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
307
+ cmd << "#{options[:order]}" if options[:order]
308
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
309
+ cmd << "STORE #{options[:store]}" if options[:store]
310
+ call_command(cmd)
311
+ end
312
+
313
+ def incr(key, increment = nil)
314
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
315
+ end
316
+
317
+ def decr(key,decrement = nil)
318
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
319
+ end
320
+
321
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
322
+ # keys to values.
323
+ def mapped_mget(*keys)
324
+ result = {}
325
+ mget(*keys).each do |value|
326
+ key = keys.shift
327
+ result.merge!(key => value) unless value.nil?
328
+ end
329
+ result
330
+ end
331
+
332
+ # Ruby defines a now deprecated type method so we need to override it here
333
+ # since it will never hit method_missing
334
+ def type(key)
335
+ call_command(['type', key])
336
+ end
337
+
338
+ def quit
339
+ call_command(['quit'])
340
+ rescue Errno::ECONNRESET
341
+ end
342
+
343
+ def pipelined(&block)
344
+ pipeline = Pipeline.new self
345
+ yield pipeline
346
+ pipeline.execute
347
+ end
348
+
349
+ def read_reply
350
+ # We read the first byte using read() mainly because gets() is
351
+ # immune to raw socket timeouts.
352
+ begin
353
+ rtype = @sock.read(1)
354
+ rescue Errno::EAGAIN
355
+ # We want to make sure it reconnects on the next command after the
356
+ # timeout. Otherwise the server may reply in the meantime leaving
357
+ # the protocol in a desync status.
358
+ @sock = nil
359
+ raise Errno::EAGAIN, "Timeout reading from the socket"
360
+ end
361
+
362
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
363
+ line = @sock.gets
364
+ case rtype
365
+ when MINUS
366
+ raise MINUS + line.strip
367
+ when PLUS
368
+ line.strip
369
+ when COLON
370
+ line.to_i
371
+ when DOLLAR
372
+ bulklen = line.to_i
373
+ return nil if bulklen == -1
374
+ data = @sock.read(bulklen)
375
+ @sock.read(2) # CRLF
376
+ data
377
+ when ASTERISK
378
+ objects = line.to_i
379
+ return nil if bulklen == -1
380
+ res = []
381
+ objects.times {
382
+ res << read_reply
383
+ }
384
+ res
385
+ else
386
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
387
+ end
388
+ end
389
+
390
+ def exec
391
+ # Need to override Kernel#exec.
392
+ call_command([:exec])
393
+ end
394
+
395
+ def multi(&block)
396
+ result = call_command [:multi]
397
+
398
+ return result unless block_given?
399
+
400
+ begin
401
+ yield(self)
402
+ exec
403
+ rescue Exception => e
404
+ discard
405
+ raise e
406
+ end
407
+ end
408
+
409
+ private
410
+ def get_size(string)
411
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
412
+ end
413
+ end
414
+ end
@@ -0,0 +1,114 @@
1
+ require 'redis/hash_ring'
2
+
3
+ module RedisRb
4
+ class DistRedis
5
+ attr_reader :ring
6
+ def initialize(opts={})
7
+ hosts = []
8
+
9
+ db = opts[:db] || nil
10
+ timeout = opts[:timeout] || nil
11
+
12
+ raise "No hosts given" unless opts[:hosts]
13
+
14
+ opts[:hosts].each do |h|
15
+ host, port = h.split(':')
16
+ hosts << Client.new(:host => host, :port => port, :db => db, :timeout => timeout)
17
+ end
18
+
19
+ @ring = HashRing.new hosts
20
+ end
21
+
22
+ def node_for_key(key)
23
+ key = $1 if key =~ /\{(.*)?\}/
24
+ @ring.get_node(key)
25
+ end
26
+
27
+ def add_server(server)
28
+ server, port = server.split(':')
29
+ @ring.add_node Client.new(:host => server, :port => port)
30
+ end
31
+
32
+ def method_missing(sym, *args, &blk)
33
+ if redis = node_for_key(args.first.to_s)
34
+ redis.send sym, *args, &blk
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def node_keys(glob)
41
+ @ring.nodes.map do |red|
42
+ red.keys(glob)
43
+ end
44
+ end
45
+
46
+ def keys(glob)
47
+ node_keys(glob).flatten
48
+ end
49
+
50
+ def save
51
+ on_each_node :save
52
+ end
53
+
54
+ def bgsave
55
+ on_each_node :bgsave
56
+ end
57
+
58
+ def quit
59
+ on_each_node :quit
60
+ end
61
+
62
+ def flush_all
63
+ on_each_node :flush_all
64
+ end
65
+ alias_method :flushall, :flush_all
66
+
67
+ def flush_db
68
+ on_each_node :flush_db
69
+ end
70
+ alias_method :flushdb, :flush_db
71
+
72
+ def delete_cloud!
73
+ @ring.nodes.each do |red|
74
+ red.keys("*").each do |key|
75
+ red.delete key
76
+ end
77
+ end
78
+ end
79
+
80
+ def on_each_node(command, *args)
81
+ @ring.nodes.each do |red|
82
+ red.send(command, *args)
83
+ end
84
+ end
85
+
86
+ def mset()
87
+
88
+ end
89
+
90
+ def mget(*keyz)
91
+ results = {}
92
+ kbn = keys_by_node(keyz)
93
+ kbn.each do |node, node_keyz|
94
+ node.mapped_mget(*node_keyz).each do |k, v|
95
+ results[k] = v
96
+ end
97
+ end
98
+ keyz.flatten.map { |k| results[k] }
99
+ end
100
+
101
+ def keys_by_node(*keyz)
102
+ keyz.flatten.inject({}) do |kbn, k|
103
+ node = node_for_key(k)
104
+ next if kbn[node] && kbn[node].include?(k)
105
+ kbn[node] ||= []
106
+ kbn[node] << k
107
+ kbn
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # For backwards compatibility
114
+ DistRedis = RedisRb::DistRedis
@@ -0,0 +1,131 @@
1
+ require 'zlib'
2
+
3
+ module RedisRb
4
+ class HashRing
5
+
6
+ POINTS_PER_SERVER = 160 # this is the default in libmemcached
7
+
8
+ attr_reader :ring, :sorted_keys, :replicas, :nodes
9
+
10
+ # nodes is a list of objects that have a proper to_s representation.
11
+ # replicas indicates how many virtual points should be used pr. node,
12
+ # replicas are required to improve the distribution.
13
+ def initialize(nodes=[], replicas=POINTS_PER_SERVER)
14
+ @replicas = replicas
15
+ @ring = {}
16
+ @nodes = []
17
+ @sorted_keys = []
18
+ nodes.each do |node|
19
+ add_node(node)
20
+ end
21
+ end
22
+
23
+ # Adds a `node` to the hash ring (including a number of replicas).
24
+ def add_node(node)
25
+ @nodes << node
26
+ @replicas.times do |i|
27
+ key = Zlib.crc32("#{node}:#{i}")
28
+ @ring[key] = node
29
+ @sorted_keys << key
30
+ end
31
+ @sorted_keys.sort!
32
+ end
33
+
34
+ def remove_node(node)
35
+ @nodes.reject!{|n| n.to_s == node.to_s}
36
+ @replicas.times do |i|
37
+ key = Zlib.crc32("#{node}:#{i}")
38
+ @ring.delete(key)
39
+ @sorted_keys.reject! {|k| k == key}
40
+ end
41
+ end
42
+
43
+ # get the node in the hash ring for this key
44
+ def get_node(key)
45
+ get_node_pos(key)[0]
46
+ end
47
+
48
+ def get_node_pos(key)
49
+ return [nil,nil] if @ring.size == 0
50
+ crc = Zlib.crc32(key)
51
+ idx = HashRing.binary_search(@sorted_keys, crc)
52
+ return [@ring[@sorted_keys[idx]], idx]
53
+ end
54
+
55
+ def iter_nodes(key)
56
+ return [nil,nil] if @ring.size == 0
57
+ node, pos = get_node_pos(key)
58
+ @sorted_keys[pos..-1].each do |k|
59
+ yield @ring[k]
60
+ end
61
+ end
62
+
63
+ class << self
64
+
65
+ # gem install RubyInline to use this code
66
+ # Native extension to perform the binary search within the hashring.
67
+ # There's a pure ruby version below so this is purely optional
68
+ # for performance. In testing 20k gets and sets, the native
69
+ # binary search shaved about 12% off the runtime (9sec -> 8sec).
70
+ begin
71
+ require 'inline'
72
+ inline do |builder|
73
+ builder.c <<-EOM
74
+ int binary_search(VALUE ary, unsigned int r) {
75
+ int upper = RARRAY_LEN(ary) - 1;
76
+ int lower = 0;
77
+ int idx = 0;
78
+
79
+ while (lower <= upper) {
80
+ idx = (lower + upper) / 2;
81
+
82
+ VALUE continuumValue = RARRAY_PTR(ary)[idx];
83
+ unsigned int l = NUM2UINT(continuumValue);
84
+ if (l == r) {
85
+ return idx;
86
+ }
87
+ else if (l > r) {
88
+ upper = idx - 1;
89
+ }
90
+ else {
91
+ lower = idx + 1;
92
+ }
93
+ }
94
+ if (upper < 0) {
95
+ upper = RARRAY_LEN(ary) - 1;
96
+ }
97
+ return upper;
98
+ }
99
+ EOM
100
+ end
101
+ rescue Exception => e
102
+ # Find the closest index in HashRing with value <= the given value
103
+ def binary_search(ary, value, &block)
104
+ upper = ary.size - 1
105
+ lower = 0
106
+ idx = 0
107
+
108
+ while(lower <= upper) do
109
+ idx = (lower + upper) / 2
110
+ comp = ary[idx] <=> value
111
+
112
+ if comp == 0
113
+ return idx
114
+ elsif comp > 0
115
+ upper = idx - 1
116
+ else
117
+ lower = idx + 1
118
+ end
119
+ end
120
+
121
+ if upper < 0
122
+ upper = ary.size - 1
123
+ end
124
+ return upper
125
+ end
126
+
127
+ end
128
+ end
129
+
130
+ end
131
+ end
@@ -1,12 +1,12 @@
1
- class Redis
2
- class Pipeline < Redis
1
+ module RedisRb
2
+ class Pipeline < Client
3
3
  BUFFER_SIZE = 50_000
4
-
4
+
5
5
  def initialize(redis)
6
6
  @redis = redis
7
7
  @commands = []
8
8
  end
9
-
9
+
10
10
  def call_command(command)
11
11
  @commands << command
12
12
  end