redis 0.1.2 → 1.0.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.
@@ -0,0 +1,114 @@
1
+ require 'redis/hash_ring'
2
+
3
+ class Redis
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 = Redis::DistRedis
@@ -0,0 +1,131 @@
1
+ require 'zlib'
2
+
3
+ class Redis
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
1
  class Redis
2
- class Pipeline < Redis
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
@@ -16,6 +16,5 @@ class Redis
16
16
  @redis.call_command(@commands)
17
17
  @commands.clear
18
18
  end
19
-
20
19
  end
21
20
  end
data/lib/redis.rb CHANGED
@@ -1,373 +1,24 @@
1
1
  require 'socket'
2
- require File.join(File.dirname(__FILE__),'pipeline')
2
+
3
+ class Redis
4
+ VERSION = "1.0.0"
5
+
6
+ def self.new(*attrs)
7
+ Client.new(*attrs)
8
+ end
9
+ end
3
10
 
4
11
  begin
5
12
  if RUBY_VERSION >= '1.9'
6
13
  require 'timeout'
7
- RedisTimer = Timeout
14
+ Redis::Timer = Timeout
8
15
  else
9
16
  require 'system_timer'
10
- RedisTimer = SystemTimer
17
+ Redis::Timer = SystemTimer
11
18
  end
12
19
  rescue LoadError
13
- RedisTimer = nil
20
+ Redis::Timer = nil
14
21
  end
15
22
 
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
- "zadd" => true,
38
- "zincrby" => true,
39
- "zrem" => true,
40
- "zscore" => true
41
- }
42
-
43
- MULTI_BULK_COMMANDS = {
44
- "mset" => true,
45
- "msetnx" => true
46
- }
47
-
48
- BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
49
-
50
- REPLY_PROCESSOR = {
51
- "exists" => BOOLEAN_PROCESSOR,
52
- "sismember" => BOOLEAN_PROCESSOR,
53
- "sadd" => BOOLEAN_PROCESSOR,
54
- "srem" => BOOLEAN_PROCESSOR,
55
- "smove" => BOOLEAN_PROCESSOR,
56
- "zadd" => BOOLEAN_PROCESSOR,
57
- "zrem" => BOOLEAN_PROCESSOR,
58
- "move" => BOOLEAN_PROCESSOR,
59
- "setnx" => BOOLEAN_PROCESSOR,
60
- "del" => BOOLEAN_PROCESSOR,
61
- "renamenx" => BOOLEAN_PROCESSOR,
62
- "expire" => BOOLEAN_PROCESSOR,
63
- "keys" => lambda{|r| r.split(" ")},
64
- "info" => lambda{|r|
65
- info = {}
66
- r.each_line {|kv|
67
- k,v = kv.split(":",2).map{|x| x.chomp}
68
- info[k.to_sym] = v
69
- }
70
- info
71
- }
72
- }
73
-
74
- ALIASES = {
75
- "flush_db" => "flushdb",
76
- "flush_all" => "flushall",
77
- "last_save" => "lastsave",
78
- "key?" => "exists",
79
- "delete" => "del",
80
- "randkey" => "randomkey",
81
- "list_length" => "llen",
82
- "push_tail" => "rpush",
83
- "push_head" => "lpush",
84
- "pop_tail" => "rpop",
85
- "pop_head" => "lpop",
86
- "list_set" => "lset",
87
- "list_range" => "lrange",
88
- "list_trim" => "ltrim",
89
- "list_index" => "lindex",
90
- "list_rm" => "lrem",
91
- "set_add" => "sadd",
92
- "set_delete" => "srem",
93
- "set_count" => "scard",
94
- "set_member?" => "sismember",
95
- "set_members" => "smembers",
96
- "set_intersect" => "sinter",
97
- "set_intersect_store" => "sinterstore",
98
- "set_inter_store" => "sinterstore",
99
- "set_union" => "sunion",
100
- "set_union_store" => "sunionstore",
101
- "set_diff" => "sdiff",
102
- "set_diff_store" => "sdiffstore",
103
- "set_move" => "smove",
104
- "set_unless_exists" => "setnx",
105
- "rename_unless_exists" => "renamenx",
106
- "type?" => "type",
107
- "zset_add" => "zadd",
108
- "zset_count" => "zcard",
109
- "zset_range_by_score" => "zrangebyscore",
110
- "zset_reverse_range" => "zrevrange",
111
- "zset_range" => "zrange",
112
- "zset_delete" => "zrem",
113
- "zset_score" => "zscore",
114
- "zset_incr_by" => "zincrby",
115
- "zset_increment_by" => "zincrby"
116
- }
117
-
118
- DISABLED_COMMANDS = {
119
- "monitor" => true,
120
- "sync" => true
121
- }
122
-
123
- def initialize(options = {})
124
- @host = options[:host] || '127.0.0.1'
125
- @port = (options[:port] || 6379).to_i
126
- @db = (options[:db] || 0).to_i
127
- @timeout = (options[:timeout] || 5).to_i
128
- @password = options[:password]
129
- @logger = options[:logger]
130
- @thread_safe = options[:thread_safe]
131
- @mutex = Mutex.new if @thread_safe
132
- @sock = nil
133
-
134
- @logger.info { self.to_s } if @logger
135
- end
136
-
137
- def to_s
138
- "Redis Client connected to #{server} against DB #{@db}"
139
- end
140
-
141
- def server
142
- "#{@host}:#{@port}"
143
- end
144
-
145
- def connect_to_server
146
- @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
147
- call_command(["auth",@password]) if @password
148
- call_command(["select",@db]) unless @db == 0
149
- end
150
-
151
- def connect_to(host, port, timeout=nil)
152
- # We support connect() timeout only if system_timer is availabe
153
- # or if we are running against Ruby >= 1.9
154
- # Timeout reading from the socket instead will be supported anyway.
155
- if @timeout != 0 and RedisTimer
156
- begin
157
- sock = TCPSocket.new(host, port)
158
- rescue Timeout::Error
159
- @sock = nil
160
- raise Timeout::Error, "Timeout connecting to the server"
161
- end
162
- else
163
- sock = TCPSocket.new(host, port)
164
- end
165
- sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
166
-
167
- # If the timeout is set we set the low level socket options in order
168
- # to make sure a blocking read will return after the specified number
169
- # of seconds. This hack is from memcached ruby client.
170
- if timeout
171
- secs = Integer(timeout)
172
- usecs = Integer((timeout - secs) * 1_000_000)
173
- optval = [secs, usecs].pack("l_2")
174
- begin
175
- sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
176
- sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
177
- rescue Exception => ex
178
- # Solaris, for one, does not like/support socket timeouts.
179
- @logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
180
- end
181
- end
182
- sock
183
- end
184
-
185
- def method_missing(*argv)
186
- call_command(argv)
187
- end
188
-
189
- def call_command(argv)
190
- @logger.debug { argv.inspect } if @logger
191
-
192
- # this wrapper to raw_call_command handle reconnection on socket
193
- # error. We try to reconnect just one time, otherwise let the error
194
- # araise.
195
- connect_to_server if !@sock
196
-
197
- begin
198
- raw_call_command(argv.dup)
199
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
200
- @sock.close rescue nil
201
- @sock = nil
202
- connect_to_server
203
- raw_call_command(argv.dup)
204
- end
205
- end
206
-
207
- def raw_call_command(argvp)
208
- pipeline = argvp[0].is_a?(Array)
209
-
210
- unless pipeline
211
- argvv = [argvp]
212
- else
213
- argvv = argvp
214
- end
215
-
216
- if MULTI_BULK_COMMANDS[argvv.flatten[0].to_s]
217
- # TODO improve this code
218
- argvp = argvv.flatten
219
- values = argvp.pop.to_a.flatten
220
- argvp = values.unshift(argvp[0])
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
23
+ require 'redis/client'
24
+ require 'redis/pipeline'