ruster 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/bin/ruster +23 -283
- data/lib/ruster.rb +8 -0
- data/lib/ruster/cluster.rb +143 -0
- data/lib/ruster/node.rb +172 -0
- data/lib/ruster/util.rb +11 -0
- data/ruster.gemspec +3 -1
- data/test/cluster.rb +81 -0
- data/test/helper.rb +58 -0
- data/test/node.rb +255 -0
- data/test/util.rb +30 -0
- metadata +32 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96027b4bdf2c2795b11fd73f8810481dc3f8216b
|
4
|
+
data.tar.gz: 95eb3adb68b769c09c72413f6579815ab1e11cc9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04da50f39fdf89bb5030fe9044df8a67e60f8ca62471e83c6bed6d587a771d0b7659de17780dcd8f7e2161c9d6f6b6f7de47c51fbba59fa958a3d8eba75385ae
|
7
|
+
data.tar.gz: 6f71ed0fb738302ffb98c10bfb4929c68e6a72b52a2874fffc8f722c79e4ca0b39c95c24f4d8b2d198bf08a882816ad773036d70a532e5906770ae4c136d036c
|
data/README.md
CHANGED
@@ -107,6 +107,8 @@ Also, I'd like to thank to [Eruca Sativa][eruca] and [Cirse][cirse]
|
|
107
107
|
for the music that's currently blasting my speakers while I write
|
108
108
|
this.
|
109
109
|
|
110
|
+
Who said programming shouldn't be [fun][lovestory]? [Discuss on Hacker News][lovestoryhn].
|
111
|
+
|
110
112
|
[redis]: http://redis.io/
|
111
113
|
[redis-cluster]: http://redis.io/topics/cluster-tutorial
|
112
114
|
[redic]: https://github.com/amakawa/redic
|
@@ -120,3 +122,5 @@ this.
|
|
120
122
|
[@pote]: https://twitter.com/poteland
|
121
123
|
[@lucasefe]: https://twitter.com/lucasefe
|
122
124
|
[nameme]: https://twitter.com/inkel/status/444638064393326592
|
125
|
+
[lovestory]: https://github.com/inkel/ruster/blob/90f7da1c281bfc1a5fe01ccf8057f948278b3685/test/node.rb#L150-198
|
126
|
+
[lovestoryhn]: https://news.ycombinator.com/item?id=7406297
|
data/bin/ruster
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#! /usr/bin/env ruby
|
2
2
|
|
3
|
-
require "redic"
|
4
3
|
require "clap"
|
4
|
+
require_relative "../lib/ruster"
|
5
5
|
|
6
6
|
$verbose = 0
|
7
7
|
|
@@ -41,306 +41,46 @@ EOU
|
|
41
41
|
|
42
42
|
abort USAGE if action.nil? or args.nil? or args.empty?
|
43
43
|
|
44
|
-
module UI
|
45
|
-
def err msg
|
46
|
-
$stderr.puts msg
|
47
|
-
end
|
48
|
-
|
49
|
-
def abort msg, backtrace=[]
|
50
|
-
err msg
|
51
|
-
backtrace.each { |line| err line } if $verbose > 1
|
52
|
-
exit 1
|
53
|
-
end
|
54
|
-
|
55
|
-
def info *args
|
56
|
-
$stdout.puts args.join(" ")
|
57
|
-
end
|
58
|
-
|
59
|
-
def log *args
|
60
|
-
$stdout.puts args.join(" ") if $verbose > 0
|
61
|
-
end
|
62
|
-
|
63
|
-
def debug *args
|
64
|
-
$stdout.puts args.join(" ") if $verbose > 1
|
65
|
-
end
|
66
|
-
|
67
|
-
extend self
|
68
|
-
end
|
69
|
-
|
70
|
-
class Node
|
71
|
-
[:id, :ip_port, :flags, :master_id, :ping, :pong, :config, :state, :slots].each do |a|
|
72
|
-
attr a
|
73
|
-
end
|
74
|
-
|
75
|
-
def initialize(ip_port)
|
76
|
-
@ip_port = ip_port
|
77
|
-
load_info!
|
78
|
-
end
|
79
|
-
|
80
|
-
def cluster_enabled?
|
81
|
-
call("INFO", "cluster").include?("cluster_enabled:1")
|
82
|
-
end
|
83
|
-
|
84
|
-
def only_node?
|
85
|
-
call("CLUSTER", "INFO").include?("cluster_known_nodes:1")
|
86
|
-
end
|
87
|
-
|
88
|
-
def empty?
|
89
|
-
call("INFO", "keyspace").strip == "# Keyspace"
|
90
|
-
end
|
91
|
-
|
92
|
-
def load_info!
|
93
|
-
call("CLUSTER", "NODES").split("\n").each do |line|
|
94
|
-
parts = line.split
|
95
|
-
next unless parts[2].include?("myself")
|
96
|
-
set_info!(*parts)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def set_info!(id, ip_port, flags, master_id, ping, pong, config, state, *slots)
|
101
|
-
@id = id
|
102
|
-
@flags = flags.split(",")
|
103
|
-
@master_id = master_id
|
104
|
-
@ping = ping
|
105
|
-
@pong = pong
|
106
|
-
@config = config
|
107
|
-
@state = state
|
108
|
-
@slots = slots
|
109
|
-
@ip_port = ip_port unless flags.include?("myself")
|
110
|
-
end
|
111
|
-
|
112
|
-
def ip
|
113
|
-
@ip_port.split(":").first
|
114
|
-
end
|
115
|
-
|
116
|
-
def port
|
117
|
-
@ip_port.split(":").last
|
118
|
-
end
|
119
|
-
|
120
|
-
def client
|
121
|
-
@client ||= Redic.new("redis://#{@ip_port}")
|
122
|
-
end
|
123
|
-
|
124
|
-
def call(*args)
|
125
|
-
UI.debug ">", *args
|
126
|
-
client.call(*args)
|
127
|
-
end
|
128
|
-
|
129
|
-
def dead?
|
130
|
-
%w{ disconnected fail noaddr }.any? do |flag|
|
131
|
-
flags.include?(flag)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def alive?
|
136
|
-
p [ip_port, flags, state]
|
137
|
-
!dead?
|
138
|
-
end
|
139
|
-
|
140
|
-
def to_s
|
141
|
-
"#{@id} [#{@ip_port}]"
|
142
|
-
end
|
143
|
-
|
144
|
-
def slots
|
145
|
-
return @_slots if @_slots
|
146
|
-
|
147
|
-
slots = { slots: [], migrating: {}, importing: {} }
|
148
|
-
|
149
|
-
@slots.each do |data|
|
150
|
-
if data[0] == /\[(\d+)-([<>])-(\d+)\]/
|
151
|
-
if $2 == ">"
|
152
|
-
slots[:migrating][$1] = $2
|
153
|
-
else
|
154
|
-
slots[:importing][$1] = $2
|
155
|
-
end
|
156
|
-
elsif data =~ /(\d+)-(\d+)/
|
157
|
-
b, e = $1.to_i, $2.to_i
|
158
|
-
(b..e).each { |slot| slots[:slots] << slot }
|
159
|
-
else
|
160
|
-
slots[:slots] << data.to_i
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
@_slots = slots
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
class Cluster
|
169
|
-
SLOTS = 16384
|
170
|
-
|
171
|
-
def initialize(addrs)
|
172
|
-
@addrs = Array(addrs)
|
173
|
-
end
|
174
|
-
|
175
|
-
def nodes
|
176
|
-
@nodes ||= @addrs.map { |addr| Node.new(addr) }
|
177
|
-
end
|
178
|
-
|
179
|
-
def allocate_slots(node, slots)
|
180
|
-
UI.log "Allocating #{slots.size} slots (#{slots.first}..#{slots.last}) in node #{node}"
|
181
|
-
|
182
|
-
UI.debug "> CLUSTER ADDSLOTS #{slots.first}..#{slots.last}"
|
183
|
-
|
184
|
-
res = node.client.call("CLUSTER", "ADDSLOTS", *slots)
|
185
|
-
|
186
|
-
UI.abort res.message if res.is_a?(RuntimeError)
|
187
|
-
end
|
188
|
-
|
189
|
-
def add_node(node)
|
190
|
-
default = nodes.first
|
191
|
-
|
192
|
-
UI.log "Joining node #{node} to node #{default}"
|
193
|
-
|
194
|
-
ip, port = node.ip_port.split(":")
|
195
|
-
|
196
|
-
res = default.call("CLUSTER", "MEET", ip, port)
|
197
|
-
|
198
|
-
UI.abort res.message if res.is_a?(RuntimeError)
|
199
|
-
end
|
200
|
-
|
201
|
-
def create!
|
202
|
-
nodes.each do |node|
|
203
|
-
raise ArgumentError, "Redis Server at #{node.ip_port} not running in cluster mode" unless node.cluster_enabled?
|
204
|
-
raise ArgumentError, "Redis Server at #{node.ip_port} already exists in a cluster" unless node.only_node?
|
205
|
-
raise ArgumentError, "Redis Server at #{node.ip_port} is not empty" unless node.empty?
|
206
|
-
end
|
207
|
-
|
208
|
-
UI.log "Allocating #{SLOTS} slots in #{nodes.length} nodes"
|
209
|
-
|
210
|
-
available_slots = 0.upto(SLOTS - 1).each_slice((SLOTS.to_f / nodes.length).ceil)
|
211
|
-
|
212
|
-
nodes.each do |node|
|
213
|
-
slots = available_slots.next.to_a
|
214
|
-
|
215
|
-
allocate_slots(node, slots)
|
216
|
-
end
|
217
|
-
|
218
|
-
nodes.each { |node| add_node node }
|
219
|
-
end
|
220
|
-
|
221
|
-
def remove_node(node)
|
222
|
-
default = nodes.first
|
223
|
-
|
224
|
-
UI.log "Removing node #{node} from cluster"
|
225
|
-
|
226
|
-
res = default.call("CLUSTER", "FORGET", node.id)
|
227
|
-
|
228
|
-
UI.abort res.message if res.is_a?(RuntimeError)
|
229
|
-
end
|
230
|
-
|
231
|
-
def nodes!
|
232
|
-
node = nodes.sample
|
233
|
-
|
234
|
-
node.call("CLUSTER", "NODES").split("\n").map do |line|
|
235
|
-
_, ip_port, flags, _ = line.split
|
236
|
-
|
237
|
-
if flags.include?("myself")
|
238
|
-
node
|
239
|
-
else
|
240
|
-
Node.new(ip_port)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def each(*args)
|
246
|
-
nodes!.each do |node|
|
247
|
-
UI.info "#{node}: #{args.join(' ')}"
|
248
|
-
|
249
|
-
res = node.call(*args)
|
250
|
-
UI.info res
|
251
|
-
UI.info "--"
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def reshard(target_addr, slots, sources, opts={})
|
256
|
-
options = { timeout: 1_000, db: 0 }.merge(opts)
|
257
|
-
|
258
|
-
target = Node.new(target_addr)
|
259
|
-
|
260
|
-
from = sources.map{ |addr| Node.new(addr) } \
|
261
|
-
.sort{ |a, b| b.slots[:slots].size <=> a.slots[:slots].size }
|
262
|
-
|
263
|
-
total_slots = from.inject(0) do |sum, source|
|
264
|
-
sum + source.slots[:slots].size
|
265
|
-
end
|
266
|
-
|
267
|
-
UI.abort "No slots found to migrate" unless total_slots > 0
|
268
|
-
|
269
|
-
from.each do |source|
|
270
|
-
# Proportional number of slots, based on current assigned slots
|
271
|
-
node_slots = (slots.to_f / total_slots * source.slots[:slots].size).to_i
|
272
|
-
|
273
|
-
UI.info "Moving #{node_slots} slots from #{source} to #{target}"
|
274
|
-
|
275
|
-
source.slots[:slots].take(node_slots).each do |slot|
|
276
|
-
count = source.call("CLUSTER", "COUNTKEYSINSLOT", slot)
|
277
|
-
|
278
|
-
UI.log " Moving slot #{slot} (#{count} keys)"
|
279
|
-
|
280
|
-
target.call("CLUSTER", "SETSLOT", slot, "IMPORTING", source.id)
|
281
|
-
source.call("CLUSTER", "SETSLOT", slot, "MIGRATING", target.id)
|
282
|
-
|
283
|
-
done = false
|
284
|
-
|
285
|
-
until done
|
286
|
-
keys = source.call("CLUSTER", "GETKEYSINSLOT", slot, 10)
|
287
|
-
|
288
|
-
done = keys.empty?
|
289
|
-
|
290
|
-
keys.each do |key|
|
291
|
-
res = source.call("MIGRATE", target.ip, target.port, key, options[:db], options[:timeout])
|
292
|
-
|
293
|
-
UI.abort res.message if res.is_a?(RuntimeError)
|
294
|
-
|
295
|
-
$stdout.print '.' if $verbose > 2
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
nodes!.each do |node|
|
300
|
-
res = node.call("CLUSTER", "SETSLOT", slot, "NODE", target.id)
|
301
|
-
|
302
|
-
UI.err res.message if res.is_a?(RuntimeError)
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
end
|
308
|
-
|
309
44
|
begin
|
310
45
|
case action
|
311
46
|
when "create"
|
312
|
-
|
313
|
-
cluster.create!
|
47
|
+
Ruster::Cluster.create!(args)
|
314
48
|
when "add"
|
315
|
-
cluster = Cluster.new(args.shift)
|
49
|
+
cluster = Ruster::Cluster.new(Ruster::Node.new(args.shift))
|
316
50
|
|
317
51
|
args.each do |addr|
|
318
|
-
|
52
|
+
ip, port = addr.split(":")
|
53
|
+
cluster.add_node(ip, port)
|
319
54
|
end
|
320
55
|
when "remove"
|
321
|
-
cluster = Cluster.new(args.shift)
|
56
|
+
cluster = Ruster::Cluster.new(Ruster::Node.new(args.shift))
|
322
57
|
|
323
58
|
args.each do |addr|
|
324
|
-
|
59
|
+
node = Ruster::Node.new(addr)
|
60
|
+
node.load!
|
61
|
+
cluster.remove_node(node)
|
325
62
|
end
|
326
63
|
when "each"
|
327
|
-
cluster = Cluster.new(args.shift)
|
64
|
+
cluster = Ruster::Cluster.new(Ruster::Node.new(args.shift))
|
328
65
|
|
329
|
-
cluster.each(*args)
|
66
|
+
cluster.each(*args) do |node, res|
|
67
|
+
puts "> #{node}"
|
68
|
+
puts res
|
69
|
+
end
|
330
70
|
when "reshard"
|
331
|
-
|
71
|
+
cluster = Ruster::Cluster.new(Ruster::Node.new(args.shift))
|
332
72
|
|
333
|
-
|
334
|
-
"-t" => ->(ms) { options[:timeout] = Integer(ms) },
|
335
|
-
"-n" => ->(db) { options[:db] = db }
|
336
|
-
}
|
73
|
+
num_slots, target_addr, *sources_addr = args
|
337
74
|
|
338
|
-
|
75
|
+
target = Ruster::Node.new(target_addr)
|
76
|
+
sources = sources_addr.map{ |addr| Ruster::Node.new(addr) }
|
339
77
|
|
340
|
-
cluster.reshard(
|
78
|
+
cluster.reshard(target, num_slots.to_i, sources)
|
341
79
|
else
|
342
|
-
|
80
|
+
abort "Unrecognized action `#{action}'\n#{USAGE}"
|
343
81
|
end
|
344
82
|
rescue => ex
|
345
|
-
|
83
|
+
$stderr.puts ex.message
|
84
|
+
ex.backtrace.each{ |line| $stderr.puts line } if $verbose > 1
|
85
|
+
exit 2
|
346
86
|
end
|
data/lib/ruster.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
class Ruster::Cluster
|
2
|
+
include Ruster::Util
|
3
|
+
|
4
|
+
attr :entry
|
5
|
+
|
6
|
+
SLOTS = 16384
|
7
|
+
|
8
|
+
def initialize(entry)
|
9
|
+
@entry = entry
|
10
|
+
end
|
11
|
+
|
12
|
+
def info
|
13
|
+
@entry.cluster_info
|
14
|
+
end
|
15
|
+
|
16
|
+
def state
|
17
|
+
info[:cluster_state]
|
18
|
+
end
|
19
|
+
|
20
|
+
def ok?
|
21
|
+
state == "ok"
|
22
|
+
end
|
23
|
+
|
24
|
+
def fail?
|
25
|
+
state == "fail"
|
26
|
+
end
|
27
|
+
|
28
|
+
def slots_assigned
|
29
|
+
info[:cluster_slots_assigned].to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def slots_ok
|
33
|
+
info[:cluster_slots_ok].to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def slots_pfail
|
37
|
+
info[:cluster_slots_pfail].to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
def slots_fail
|
41
|
+
info[:cluster_slots_fail].to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def known_nodes
|
45
|
+
info[:cluster_known_nodes].to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
def size
|
49
|
+
info[:cluster_size].to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_epoch
|
53
|
+
info[:cluster_current_epoch].to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def stats_messages_sent
|
57
|
+
info[:cluster_stats_messages_sent].to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
def stats_messages_received
|
61
|
+
info[:stats_messages_received].to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
def nodes
|
65
|
+
@entry.load!
|
66
|
+
[@entry] + @entry.friends
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_node(ip, port)
|
70
|
+
@entry.meet(ip, port)
|
71
|
+
end
|
72
|
+
|
73
|
+
def remove_node(bye)
|
74
|
+
nodes.each do |node|
|
75
|
+
next if node.id == bye.id
|
76
|
+
node.forget(bye)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.create!(addrs)
|
81
|
+
# Check nodes
|
82
|
+
nodes = addrs.map do |addr|
|
83
|
+
node = ::Ruster::Node.new(addr)
|
84
|
+
|
85
|
+
raise ArgumentError, "Redis Server at #{addr} not running in cluster mode" unless node.enabled?
|
86
|
+
raise ArgumentError, "Redis Server at #{addr} already exists in a cluster" unless node.only_node?
|
87
|
+
raise ArgumentError, "Redis Server at #{addr} is not empty" unless node.empty?
|
88
|
+
|
89
|
+
node
|
90
|
+
end
|
91
|
+
|
92
|
+
# Allocate slots evenly among all nodes
|
93
|
+
slots_by_node = 0.upto(SLOTS - 1).each_slice((SLOTS.to_f / nodes.length).ceil)
|
94
|
+
|
95
|
+
nodes.each do |node|
|
96
|
+
slots = slots_by_node.next.to_a
|
97
|
+
|
98
|
+
node.add_slots(*slots)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create cluster by meeting nodes
|
102
|
+
entry = nodes.shift
|
103
|
+
|
104
|
+
nodes.each { |node| entry.meet node.ip, node.port }
|
105
|
+
|
106
|
+
new(entry)
|
107
|
+
end
|
108
|
+
|
109
|
+
def each(*args, &block)
|
110
|
+
nodes.each do |node|
|
111
|
+
yield node, node.call(*args)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def reshard(target, num_slots, sources)
|
116
|
+
raise ArgumentError, "Target node #{target} is not part of the cluster" unless in_cluster?(target)
|
117
|
+
target.load!
|
118
|
+
|
119
|
+
sources.each do |source|
|
120
|
+
raise ArgumentError, "Source node #{source} is not part of the cluster" unless in_cluster?(source)
|
121
|
+
source.load!
|
122
|
+
end
|
123
|
+
|
124
|
+
sources.sort_by!{ |node| -node.slots.size }
|
125
|
+
|
126
|
+
total_slots = sources.inject(0) do |sum, node|
|
127
|
+
sum + node.all_slots.size
|
128
|
+
end
|
129
|
+
|
130
|
+
sources.each do |node|
|
131
|
+
# Proportional number of slots based on node size
|
132
|
+
node_slots = (num_slots.to_f / total_slots * node.all_slots.size)
|
133
|
+
|
134
|
+
node.all_slots.take(node_slots).each do |slot|
|
135
|
+
node.move_slot!(slot, target)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def in_cluster?(node)
|
141
|
+
nodes.any?{ |n| n.addr == node.addr }
|
142
|
+
end
|
143
|
+
end
|
data/lib/ruster/node.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
class Ruster::Node
|
2
|
+
include Ruster::Util
|
3
|
+
|
4
|
+
attr :addr
|
5
|
+
attr :id
|
6
|
+
attr :flags
|
7
|
+
attr :master_id
|
8
|
+
attr :ping_epoch
|
9
|
+
attr :pong_epoch
|
10
|
+
attr :config_epoch
|
11
|
+
attr :state
|
12
|
+
attr :slots
|
13
|
+
attr :migrating
|
14
|
+
attr :importing
|
15
|
+
attr :friends
|
16
|
+
|
17
|
+
def initialize(addr)
|
18
|
+
@addr = addr
|
19
|
+
end
|
20
|
+
|
21
|
+
def client
|
22
|
+
@client ||= Redic.new("redis://#{addr}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(*args)
|
26
|
+
res = client.call(*args)
|
27
|
+
raise res if res.is_a?(RuntimeError)
|
28
|
+
res
|
29
|
+
end
|
30
|
+
|
31
|
+
def enabled?
|
32
|
+
parse_info(call("INFO", "cluster"))[:cluster_enabled] == "1"
|
33
|
+
end
|
34
|
+
|
35
|
+
def read_info_line!(info_line)
|
36
|
+
parts = info_line.split
|
37
|
+
|
38
|
+
@id = parts.shift
|
39
|
+
addr = parts.shift
|
40
|
+
@flags = parts.shift.split(",")
|
41
|
+
@addr = addr unless @flags.include?("myself")
|
42
|
+
@master_id = parts.shift
|
43
|
+
@ping_epoch = parts.shift.to_i
|
44
|
+
@pong_epoch = parts.shift.to_i
|
45
|
+
@config_epoch = parts.shift.to_i
|
46
|
+
@state = parts.shift
|
47
|
+
@slots = []
|
48
|
+
@migrating = {}
|
49
|
+
@importing = {}
|
50
|
+
|
51
|
+
parts.each do |slots|
|
52
|
+
case slots
|
53
|
+
when /^(\d+)-(\d+)$/ then @slots << ($1.to_i..$2.to_i)
|
54
|
+
when /^\d+$/ then @slots << (slots.to_i..slots.to_i)
|
55
|
+
when /^\[(\d+)-([<>])-([a-z0-9]+)\]$/
|
56
|
+
case $2
|
57
|
+
when ">" then @migrating[$1.to_i] = $3
|
58
|
+
when "<" then @importing[$1.to_i] = $3
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def all_slots
|
65
|
+
slots.map(&:to_a).flatten
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
"#{addr} [#{id}]"
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.from_info_line(info_line)
|
73
|
+
_, addr, _ = info_line.split
|
74
|
+
new(addr).tap { |node| node.read_info_line!(info_line) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def load!
|
78
|
+
@friends = []
|
79
|
+
|
80
|
+
call("CLUSTER", "NODES").split("\n").each do |line|
|
81
|
+
if line.include?("myself")
|
82
|
+
read_info_line!(line)
|
83
|
+
else
|
84
|
+
@friends << self.class.from_info_line(line)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def meet(ip, port)
|
90
|
+
call("CLUSTER", "MEET", ip, port)
|
91
|
+
end
|
92
|
+
|
93
|
+
def forget(node)
|
94
|
+
raise ArgumentError, "Node #{node} is not empty" unless node.slots.empty? and node.migrating.empty? and node.importing.empty?
|
95
|
+
call("CLUSTER", "FORGET", node.id)
|
96
|
+
end
|
97
|
+
|
98
|
+
def replicate(node)
|
99
|
+
call("CLUSTER", "REPLICATE", node.id)
|
100
|
+
end
|
101
|
+
|
102
|
+
def slaves
|
103
|
+
call("CLUSTER", "SLAVES", id).map do |line|
|
104
|
+
self.class.from_info_line(line)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_slots(*slots)
|
109
|
+
call("CLUSTER", "ADDSLOTS", *slots)
|
110
|
+
end
|
111
|
+
|
112
|
+
def del_slots(*slots)
|
113
|
+
call("CLUSTER", "DELSLOTS", *slots)
|
114
|
+
end
|
115
|
+
|
116
|
+
def flush_slots!
|
117
|
+
call("CLUSTER", "FLUSHSLOTS")
|
118
|
+
end
|
119
|
+
|
120
|
+
def cluster_info
|
121
|
+
parse_info(call("CLUSTER", "INFO"))
|
122
|
+
end
|
123
|
+
|
124
|
+
def ip
|
125
|
+
addr.split(":").first
|
126
|
+
end
|
127
|
+
|
128
|
+
def port
|
129
|
+
addr.split(":").last
|
130
|
+
end
|
131
|
+
|
132
|
+
# In Redis Cluster only DB 0 is enabled
|
133
|
+
DB0 = 0
|
134
|
+
|
135
|
+
def move_slot!(slot, target, options={})
|
136
|
+
options[:num_keys] ||= 10
|
137
|
+
options[:timeout] ||= call("CONFIG", "GET", "cluster-node-timeout")
|
138
|
+
|
139
|
+
# Tell the target node to import the slot
|
140
|
+
target.call("CLUSTER", "SETSLOT", slot, "IMPORTING", id)
|
141
|
+
|
142
|
+
# Tell the current node to export the slot
|
143
|
+
call("CLUSTER", "SETSLOT", slot, "MIGRATING", target.id)
|
144
|
+
|
145
|
+
# Export keys
|
146
|
+
done = false
|
147
|
+
until done
|
148
|
+
keys = call("CLUSTER", "GETKEYSINSLOT", slot, options[:num_keys])
|
149
|
+
|
150
|
+
done = keys.empty?
|
151
|
+
|
152
|
+
keys.each do |key|
|
153
|
+
call("MIGRATE", target.ip, target.port, key, DB0, options[:timeout])
|
154
|
+
end
|
155
|
+
|
156
|
+
# Tell cluster the location of the new slot
|
157
|
+
call("CLUSTER", "SETSLOT", slot, "NODE", target.id)
|
158
|
+
|
159
|
+
friends.each do |node|
|
160
|
+
node.call("CLUSTER", "SETSLOT", slot, "NODE", target.id)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def empty?
|
166
|
+
call("DBSIZE") == 0
|
167
|
+
end
|
168
|
+
|
169
|
+
def only_node?
|
170
|
+
parse_info(call("CLUSTER", "INFO"))[:cluster_known_nodes] == "1"
|
171
|
+
end
|
172
|
+
end
|
data/lib/ruster/util.rb
ADDED
data/ruster.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "ruster"
|
5
|
-
s.version = "0.0.
|
5
|
+
s.version = "0.0.4"
|
6
6
|
s.summary = "A simple Redis Cluster Administration tool"
|
7
7
|
s.description = "Control your Redis Cluster from the command line."
|
8
8
|
s.authors = ["Leandro López"]
|
@@ -15,5 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.add_dependency "redic"
|
16
16
|
s.add_dependency "clap"
|
17
17
|
|
18
|
+
s.add_development_dependency "protest"
|
19
|
+
|
18
20
|
s.files = `git ls-files`.split("\n")
|
19
21
|
end
|
data/test/cluster.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative "./helper"
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
Protest.describe "Ruster::Cluster" do
|
6
|
+
test "add node" do
|
7
|
+
with_nodes(n: 2) do |ports|
|
8
|
+
port_a, port_b = ports.to_a
|
9
|
+
|
10
|
+
bob = Ruster::Node.new("127.0.0.1:#{port_a}")
|
11
|
+
bob.add_slots(*0..16383)
|
12
|
+
cluster = Ruster::Cluster.new(bob)
|
13
|
+
|
14
|
+
Timeout.timeout(10) { sleep 0.05 until cluster.ok? }
|
15
|
+
|
16
|
+
assert_equal 1, cluster.nodes.size
|
17
|
+
assert_equal [0..16383], bob.slots
|
18
|
+
|
19
|
+
cluster.add_node("127.0.0.1", port_b)
|
20
|
+
|
21
|
+
Timeout.timeout(10) { sleep 0.05 until cluster.ok? }
|
22
|
+
|
23
|
+
assert_equal 2, cluster.nodes.size
|
24
|
+
|
25
|
+
slots = cluster.nodes.map do |node|
|
26
|
+
[node.addr, node.slots]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Do not realloce slots
|
30
|
+
assert_equal 2, slots.size
|
31
|
+
assert slots.include?(["127.0.0.1:#{port_a}", [0..16383]])
|
32
|
+
assert slots.include?(["127.0.0.1:#{port_b}", []])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "remove node" do
|
37
|
+
test "empty node" do
|
38
|
+
with_nodes(n: 3) do |ports|
|
39
|
+
port_a, port_b, port_c = ports.to_a
|
40
|
+
|
41
|
+
soveran = Ruster::Node.new("127.0.0.1:#{port_a}")
|
42
|
+
cuervo = Ruster::Node.new("127.0.0.1:#{port_b}")
|
43
|
+
inkel = Ruster::Node.new("127.0.0.1:#{port_c}")
|
44
|
+
|
45
|
+
soveran.add_slots(*0..8191)
|
46
|
+
cuervo.add_slots(*8192..16383)
|
47
|
+
|
48
|
+
cluster = Ruster::Cluster.new(soveran)
|
49
|
+
cluster.add_node(cuervo.ip, cuervo.port)
|
50
|
+
cluster.add_node(inkel.ip, inkel.port)
|
51
|
+
|
52
|
+
Timeout.timeout(10) { sleep 0.05 until cluster.ok? }
|
53
|
+
|
54
|
+
soveran.load!
|
55
|
+
cuervo.load!
|
56
|
+
inkel.load!
|
57
|
+
|
58
|
+
cluster.remove_node(inkel)
|
59
|
+
|
60
|
+
Timeout.timeout(10) { sleep 0.05 until cluster.ok? }
|
61
|
+
|
62
|
+
assert_equal 2, cluster.nodes.size
|
63
|
+
|
64
|
+
ids = cluster.nodes.map(&:id)
|
65
|
+
|
66
|
+
assert ids.include?(soveran.id)
|
67
|
+
assert ids.include?(cuervo.id)
|
68
|
+
assert !ids.include?(inkel.id)
|
69
|
+
|
70
|
+
slots = cluster.nodes.map do |node|
|
71
|
+
[node.addr, node.slots]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Do not realloce slots
|
75
|
+
assert_equal 2, slots.size
|
76
|
+
assert slots.include?([soveran.addr, [0..8191]])
|
77
|
+
assert slots.include?([cuervo.addr, [8192..16383]])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
$:.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
require "protest"
|
4
|
+
require "ruster"
|
5
|
+
require "tmpdir"
|
6
|
+
|
7
|
+
def with_nodes(opts={})
|
8
|
+
options = {
|
9
|
+
n: 3,
|
10
|
+
init_port: 12701,
|
11
|
+
enabled: "yes"
|
12
|
+
}
|
13
|
+
|
14
|
+
options.merge!(opts)
|
15
|
+
|
16
|
+
end_port = options[:init_port] + options[:n] - 1
|
17
|
+
|
18
|
+
tmp = Dir.mktmpdir
|
19
|
+
|
20
|
+
pids = []
|
21
|
+
ports = (options[:init_port]..end_port)
|
22
|
+
|
23
|
+
ports.each do |port|
|
24
|
+
pids << fork do
|
25
|
+
dir = File.join(tmp, port.to_s)
|
26
|
+
|
27
|
+
Dir.mkdir(dir)
|
28
|
+
|
29
|
+
args = [
|
30
|
+
"--port", port.to_s,
|
31
|
+
"--dir", dir,
|
32
|
+
"--save", "",
|
33
|
+
"--logfile", "./redis.log"
|
34
|
+
]
|
35
|
+
|
36
|
+
if options[:enabled] == "yes"
|
37
|
+
args.concat(["--cluster-enabled", "yes",
|
38
|
+
"--cluster-config-file", "redis.conf",
|
39
|
+
"--cluster-node-timeout", "5000"])
|
40
|
+
end
|
41
|
+
|
42
|
+
exec "redis-server", *args
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Wait for redis-server to start
|
47
|
+
sleep 0.125
|
48
|
+
|
49
|
+
yield ports
|
50
|
+
ensure
|
51
|
+
pids.each { |pid| Process.kill :TERM, pid }
|
52
|
+
|
53
|
+
Process.waitall
|
54
|
+
|
55
|
+
FileUtils.remove_entry_secure tmp
|
56
|
+
end
|
57
|
+
|
58
|
+
Protest.report_with((ENV["PROTEST_REPORT"] || "documentation").to_sym)
|
data/test/node.rb
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
require_relative "./helper"
|
2
|
+
|
3
|
+
Protest.describe "Node" do
|
4
|
+
test "is cluster enabled" do
|
5
|
+
with_nodes(n: 1) do |ports|
|
6
|
+
node = Ruster::Node.new("127.0.0.1:#{ports.first}")
|
7
|
+
|
8
|
+
assert node.enabled?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
test "is not cluster enabled" do
|
13
|
+
with_nodes(n: 1, enabled: "no") do |ports|
|
14
|
+
node = Ruster::Node.new("127.0.0.1:#{ports.first}")
|
15
|
+
|
16
|
+
assert !node.enabled?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "information" do
|
21
|
+
test "read and parses info line" do
|
22
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected"
|
23
|
+
|
24
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
25
|
+
|
26
|
+
node.read_info_line!(info_line)
|
27
|
+
|
28
|
+
assert_equal "9aee954a0b7d6b49d7e68c18d08873c56aaead6b", node.id
|
29
|
+
assert_equal "127.0.0.1:12701", node.addr
|
30
|
+
assert_equal ["myself", "master"], node.flags
|
31
|
+
assert_equal "-", node.master_id
|
32
|
+
assert_equal 0, node.ping_epoch
|
33
|
+
assert_equal 1, node.pong_epoch
|
34
|
+
assert_equal 2, node.config_epoch
|
35
|
+
assert_equal "connected", node.state
|
36
|
+
assert_equal [], node.slots
|
37
|
+
|
38
|
+
assert_equal "127.0.0.1:12701 [9aee954a0b7d6b49d7e68c18d08873c56aaead6b]", node.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
context "allocated slots" do
|
42
|
+
test "contiguous block" do
|
43
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected 0-16383"
|
44
|
+
|
45
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
46
|
+
|
47
|
+
node.read_info_line!(info_line)
|
48
|
+
|
49
|
+
assert_equal [(0..16383)], node.slots
|
50
|
+
assert node.migrating.empty?
|
51
|
+
assert node.importing.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
test "single" do
|
55
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected 4096"
|
56
|
+
|
57
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
58
|
+
|
59
|
+
node.read_info_line!(info_line)
|
60
|
+
|
61
|
+
assert_equal [(4096..4096)], node.slots
|
62
|
+
assert node.migrating.empty?
|
63
|
+
assert node.importing.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
test "migrating" do
|
67
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected [16383->-6daeaa65c37880d81c86e7d94b6d7b0a459eea9]"
|
68
|
+
|
69
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
70
|
+
|
71
|
+
node.read_info_line!(info_line)
|
72
|
+
|
73
|
+
assert_equal [], node.slots
|
74
|
+
assert_equal 1, node.migrating.size
|
75
|
+
assert_equal "6daeaa65c37880d81c86e7d94b6d7b0a459eea9", node.migrating[16383]
|
76
|
+
assert node.importing.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
test "importing" do
|
80
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected [16383-<-6daeaa65c37880d81c86e7d94b6d7b0a459eea9]"
|
81
|
+
|
82
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
83
|
+
|
84
|
+
node.read_info_line!(info_line)
|
85
|
+
|
86
|
+
assert_equal [], node.slots
|
87
|
+
assert_equal 1, node.importing.size
|
88
|
+
assert_equal "6daeaa65c37880d81c86e7d94b6d7b0a459eea9", node.importing[16383]
|
89
|
+
assert node.migrating.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
test "combined" do
|
93
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected 0-1024 2048 [3072->-6daeaa65c37880d81c86e7d94b6d7b0a459eea9] 4096 [6144-<-6daeaa65c37880d81c86e7d94b6d7b0a459eea9] 8192-16383"
|
94
|
+
|
95
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
96
|
+
|
97
|
+
node.read_info_line!(info_line)
|
98
|
+
|
99
|
+
assert_equal [(0..1024), (2048..2048), (4096..4096), (8192..16383)], node.slots
|
100
|
+
|
101
|
+
assert_equal 1, node.migrating.size
|
102
|
+
assert_equal "6daeaa65c37880d81c86e7d94b6d7b0a459eea9", node.migrating[3072]
|
103
|
+
|
104
|
+
assert_equal 1, node.importing.size
|
105
|
+
assert_equal "6daeaa65c37880d81c86e7d94b6d7b0a459eea9", node.importing[6144]
|
106
|
+
end
|
107
|
+
|
108
|
+
test "all allocated slots as an array" do
|
109
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b :0 myself,master - 0 1 2 connected 0-3 5 10-13 16383"
|
110
|
+
|
111
|
+
node = Ruster::Node.new("127.0.0.1:12701")
|
112
|
+
|
113
|
+
node.read_info_line!(info_line)
|
114
|
+
|
115
|
+
assert_equal [0, 1, 2, 3, 5, 10, 11, 12, 13, 16383], node.all_slots
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
test "create from info line" do
|
120
|
+
info_line = "9aee954a0b7d6b49d7e68c18d08873c56aaead6b 127.0.0.1:12701 master - 0 1 2 connected"
|
121
|
+
|
122
|
+
node = Ruster::Node.from_info_line(info_line)
|
123
|
+
|
124
|
+
assert_equal "9aee954a0b7d6b49d7e68c18d08873c56aaead6b", node.id
|
125
|
+
assert_equal "127.0.0.1:12701", node.addr
|
126
|
+
assert_equal ["master"], node.flags
|
127
|
+
assert_equal "-", node.master_id
|
128
|
+
assert_equal 0, node.ping_epoch
|
129
|
+
assert_equal 1, node.pong_epoch
|
130
|
+
assert_equal 2, node.config_epoch
|
131
|
+
assert_equal "connected", node.state
|
132
|
+
assert_equal [], node.slots
|
133
|
+
|
134
|
+
assert_equal "127.0.0.1:12701 [9aee954a0b7d6b49d7e68c18d08873c56aaead6b]", node.to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "in cluster" do
|
139
|
+
test "only node" do
|
140
|
+
with_nodes(n: 1) do |ports|
|
141
|
+
node = Ruster::Node.new("127.0.0.1:#{ports.first}")
|
142
|
+
|
143
|
+
node.load!
|
144
|
+
|
145
|
+
assert node.id
|
146
|
+
assert_equal [], node.friends
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
test "meet and forget node, a tragic love story" do
|
151
|
+
with_nodes(n: 2) do |ports|
|
152
|
+
port_a, port_b = ports.to_a
|
153
|
+
|
154
|
+
# This is the story of two nodes
|
155
|
+
joel = Ruster::Node.new("127.0.0.1:#{port_a}")
|
156
|
+
clem = Ruster::Node.new("127.0.0.1:#{port_b}")
|
157
|
+
|
158
|
+
# One day they met for the first time and fell for each other
|
159
|
+
joel.meet("127.0.0.1", port_b)
|
160
|
+
|
161
|
+
# Give the nodes some time to get to know each other
|
162
|
+
sleep 0.5
|
163
|
+
|
164
|
+
joel.load!
|
165
|
+
clem.load!
|
166
|
+
|
167
|
+
assert_equal 1, joel.friends.size
|
168
|
+
assert_equal 1, clem.friends.size
|
169
|
+
|
170
|
+
assert_equal clem.id, joel.friends.first.id
|
171
|
+
assert_equal joel.id, clem.friends.first.id
|
172
|
+
|
173
|
+
# But one tragic afternoon, clem took a terrible decision
|
174
|
+
clem.forget(joel)
|
175
|
+
|
176
|
+
# Give the nodes some time to process their breakup
|
177
|
+
sleep 0.5
|
178
|
+
|
179
|
+
joel.load!
|
180
|
+
clem.load!
|
181
|
+
|
182
|
+
# joel still remembers clem...
|
183
|
+
assert_equal 1, joel.friends.size
|
184
|
+
|
185
|
+
# ...but clem has already moved on
|
186
|
+
assert_equal 0, clem.friends.size
|
187
|
+
|
188
|
+
# joel now decides to use the machine from Eternal sunshine of the spotless mind...
|
189
|
+
joel.forget(clem)
|
190
|
+
|
191
|
+
# ...and after a while, this story ends
|
192
|
+
sleep 0.5
|
193
|
+
|
194
|
+
joel.load!
|
195
|
+
|
196
|
+
assert_equal 0, joel.friends.size
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
test "replicate/slaves" do
|
201
|
+
with_nodes(n: 2) do |ports|
|
202
|
+
port_a, port_b = ports.to_a
|
203
|
+
|
204
|
+
leo = Ruster::Node.new("127.0.0.1:#{port_a}")
|
205
|
+
django = Ruster::Node.new("127.0.0.1:#{port_b}")
|
206
|
+
|
207
|
+
leo.meet("127.0.0.1", port_b)
|
208
|
+
|
209
|
+
# Give the nodes some time to get to know each other
|
210
|
+
sleep 0.5
|
211
|
+
|
212
|
+
leo.load!
|
213
|
+
|
214
|
+
django.replicate(leo)
|
215
|
+
|
216
|
+
# Wait for configuration to update
|
217
|
+
sleep 0.5
|
218
|
+
|
219
|
+
assert_equal 1, leo.slaves.size
|
220
|
+
|
221
|
+
django.load!
|
222
|
+
|
223
|
+
assert_equal django.id, leo.slaves.first.id
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
test "allocate, deallocate and flush slots" do
|
228
|
+
with_nodes(n: 1) do |ports|
|
229
|
+
node = Ruster::Node.new("127.0.0.1:#{ports.first}")
|
230
|
+
|
231
|
+
# Single slot
|
232
|
+
node.add_slots(1024)
|
233
|
+
|
234
|
+
# Multiple slots
|
235
|
+
node.add_slots(2048, 4096)
|
236
|
+
|
237
|
+
node.load!
|
238
|
+
|
239
|
+
assert_equal [1024..1024, 2048..2048, 4096..4096], node.slots
|
240
|
+
|
241
|
+
node.del_slots(1024)
|
242
|
+
|
243
|
+
node.load!
|
244
|
+
|
245
|
+
assert_equal [2048..2048, 4096..4096], node.slots
|
246
|
+
|
247
|
+
node.flush_slots!
|
248
|
+
|
249
|
+
node.load!
|
250
|
+
|
251
|
+
assert_equal [], node.slots
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
data/test/util.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "./helper"
|
2
|
+
|
3
|
+
Protest.describe "Ruster::Util" do
|
4
|
+
U = Module.new { extend Ruster::Util }
|
5
|
+
|
6
|
+
test "parse INFO as hash"do
|
7
|
+
info = "cluster_state:fail\r\ncluster_slots_assigned:0\r\ncluster_slots_ok:0\r\ncluster_slots_pfail:0\r\ncluster_slots_fail:0\r\ncluster_known_nodes:1\r\ncluster_size:0\r\ncluster_current_epoch:0\r\ncluster_stats_messages_sent:0\r\ncluster_stats_messages_received:0\r\n"
|
8
|
+
|
9
|
+
data = U.parse_info(info)
|
10
|
+
|
11
|
+
assert data.is_a?(Hash)
|
12
|
+
|
13
|
+
assert_equal "fail", data[:cluster_state]
|
14
|
+
assert_equal "0", data[:cluster_slots_assigned]
|
15
|
+
assert_equal "0", data[:cluster_slots_ok]
|
16
|
+
assert_equal "0", data[:cluster_slots_pfail]
|
17
|
+
assert_equal "0", data[:cluster_slots_fail]
|
18
|
+
assert_equal "1", data[:cluster_known_nodes]
|
19
|
+
assert_equal "0", data[:cluster_size]
|
20
|
+
assert_equal "0", data[:cluster_current_epoch]
|
21
|
+
assert_equal "0", data[:cluster_stats_messages_sent]
|
22
|
+
assert_equal "0", data[:cluster_stats_messages_received]
|
23
|
+
end
|
24
|
+
|
25
|
+
test "ignore comments in INFO parsing" do
|
26
|
+
data = U.parse_info("# Cluster\r\ncluster_enabled:1\r\n")
|
27
|
+
|
28
|
+
assert_equal 1, data.size
|
29
|
+
end
|
30
|
+
end
|
metadata
CHANGED
@@ -1,41 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro López
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redic
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: clap
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: protest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
41
55
|
description: Control your Redis Cluster from the command line.
|
@@ -46,11 +60,19 @@ executables:
|
|
46
60
|
extensions: []
|
47
61
|
extra_rdoc_files: []
|
48
62
|
files:
|
49
|
-
- .gitignore
|
63
|
+
- ".gitignore"
|
50
64
|
- LICENSE
|
51
65
|
- README.md
|
52
66
|
- bin/ruster
|
67
|
+
- lib/ruster.rb
|
68
|
+
- lib/ruster/cluster.rb
|
69
|
+
- lib/ruster/node.rb
|
70
|
+
- lib/ruster/util.rb
|
53
71
|
- ruster.gemspec
|
72
|
+
- test/cluster.rb
|
73
|
+
- test/helper.rb
|
74
|
+
- test/node.rb
|
75
|
+
- test/util.rb
|
54
76
|
homepage: http://inkel.github.com/ruster
|
55
77
|
licenses:
|
56
78
|
- MIT
|
@@ -61,17 +83,17 @@ require_paths:
|
|
61
83
|
- lib
|
62
84
|
required_ruby_version: !ruby/object:Gem::Requirement
|
63
85
|
requirements:
|
64
|
-
- -
|
86
|
+
- - ">="
|
65
87
|
- !ruby/object:Gem::Version
|
66
88
|
version: '0'
|
67
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
90
|
requirements:
|
69
|
-
- -
|
91
|
+
- - ">="
|
70
92
|
- !ruby/object:Gem::Version
|
71
93
|
version: '0'
|
72
94
|
requirements: []
|
73
95
|
rubyforge_project:
|
74
|
-
rubygems_version: 2.
|
96
|
+
rubygems_version: 2.2.2
|
75
97
|
signing_key:
|
76
98
|
specification_version: 4
|
77
99
|
summary: A simple Redis Cluster Administration tool
|