ruster 0.0.3 → 0.0.4
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.
- 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
|