redis 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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