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 +3 -1
- data/lib/edis.rb +3 -0
- data/lib/redis/client.rb +414 -0
- data/lib/redis/dist_redis.rb +114 -0
- data/lib/redis/hash_ring.rb +131 -0
- data/lib/{pipeline.rb → redis/pipeline.rb} +4 -4
- data/lib/redis.rb +11 -361
- data/spec/redis_spec.rb +90 -6
- metadata +23 -12
- data/lib/dist_redis.rb +0 -149
- data/lib/hash_ring.rb +0 -135
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 =
|
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
data/lib/redis/client.rb
ADDED
@@ -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
|