+ClusterHashSlots = 4096
+
+def xputs(s)
+ printf s
+ STDOUT.flush
+end
+
+class ClusterNode
+ def initialize(addr)
+ s = addr.split(":")
+ if s.length != 2
+ puts "Invalid node name #{addr}"
+ exit 1
+ end
+ @r = nil
+ @info = {}
+ @info[:host] = s[0]
+ @info[:port] = s[1]
+ @info[:slots] = {}
+ @dirty = false # True if we need to flush slots info into node.
+ @friends = []
+ end
+
+ def friends
+ @friends
+ end
+
+ def slots
+ @info[:slots]
+ end
+
+ def to_s
+ "#{@info[:host]}:#{@info[:port]}"
+ end
+
+ def connect(o={})
+ return if @r
+ xputs "Connecting to node #{self}: "
+ begin
+ @r = Redis.new(:host => @info[:host], :port => @info[:port])
+ @r.ping
+ rescue
+ puts "ERROR"
+ puts "Sorry, can't connect to node #{self}"
+ exit 1 if o[:abort]
+ @r = nil
+ end
+ puts "OK"
+ end
+
+ def assert_cluster
+ info = @r.info
+ if !info["cluster_enabled"] || info["cluster_enabled"].to_i == 0
+ puts "Error: Node #{self} is not configured as a cluster node."
+ exit 1
+ end
+ end
+
+ def assert_empty
+ if !(@r.cluster("info").split("\r\n").index("cluster_known_nodes:1")) ||
+ (@r.info['db0'])
+ puts "Error: Node #{self} is not empty. Either the node already knows other nodes (check with nodes-info) or contains some key in database 0."
+ exit 1
+ end
+ end
+
+ def load_info(o={})
+ self.connect
+ nodes = @r.cluster("nodes").split("\n")
+ nodes.each{|n|
+ # name addr flags role ping_sent ping_recv link_status slots
+ split = n.split
+ name,addr,flags,role,ping_sent,ping_recv,link_status = split[0..6]
+ slots = split[7..-1]
+ info = {
+ :name => name,
+ :addr => addr,
+ :flags => flags.split(","),
+ :role => role,
+ :ping_sent => ping_sent.to_i,
+ :ping_recv => ping_recv.to_i,
+ :link_status => link_status
+ }
+ if info[:flags].index("myself")
+ @info = @info.merge(info)
+ @info[:slots] = {}
+ slots.each{|s|
+ if s[0..0] == '['
+ # Fixme: for now skipping migration entries
+ elsif s.index("-")
+ start,stop = s.split("-")
+ self.add_slots((start.to_i)..(stop.to_i))
+ else
+ self.add_slots((s.to_i)..(s.to_i))
+ end
+ } if slots
+ @dirty = false
+ @r.cluster("info").split("\n").each{|e|
+ k,v=e.split(":")
+ k = k.to_sym
+ v.chop!
+ if k != :cluster_state
+ @info[k] = v.to_i
+ else
+ @info[k] = v
+ end
+ }
+ elsif o[:getfriends]
+ @friends << info
+ end
+ }
+ end
+
+ def add_slots(slots)
+ slots.each{|s|
+ @info[:slots][s] = :new
+ }
+ @dirty = true
+ end
+
+ def flush_node_config
+ return if !@dirty
+ new = []
+ @info[:slots].each{|s,val|
+ if val == :new
+ new << s
+ @info[:slots][s] = true
+ end
+ }
+ @r.cluster("addslots",*new)
+ @dirty = false
+ end
+
+ def info_string
+ # We want to display the hash slots assigned to this node
+ # as ranges, like in: "1-5,8-9,20-25,30"
+ #
+ # Note: this could be easily written without side effects,
+ # we use 'slots' just to split the computation into steps.
+
+ # First step: we want an increasing array of integers
+ # for instance: [1,2,3,4,5,8,9,20,21,22,23,24,25,30]
+ slots = @info[:slots].keys.sort
+
+ # As we want to aggregate adiacent slots we convert all the
+ # slot integers into ranges (with just one element)
+ # So we have something like [1..1,2..2, ... and so forth.
+ slots.map!{|x| x..x}
+
+ # Finally we group ranges with adiacent elements.
+ slots = slots.reduce([]) {|a,b|
+ if !a.empty? && b.first == (a[-1].last)+1
+ a[0..-2] + [(a[-1].first)..(b.last)]
+ else
+ a + [b]
+ end
+ }
+
+ # Now our task is easy, we just convert ranges with just one
+ # element into a number, and a real range into a start-end format.
+ # Finally we join the array using the comma as separator.
+ slots = slots.map{|x|
+ x.count == 1 ? x.first.to_s : "#{x.first}-#{x.last}"
+ }.join(",")
+
+ "[#{@info[:cluster_state].upcase}] #{self.info[:name]} #{self.to_s} slots:#{slots} (#{self.slots.length} slots)"
+ end
+
+ def info
+ @info
+ end
+
+ def is_dirty?
+ @dirty
+ end
+
+ def r
+ @r
+ end
+end
+