1#!/usr/bin/env ruby 2 3# TODO (temporary here, we'll move this into the Github issues once 4# redis-trib initial implementation is completed). 5# 6# - Make sure that if the rehashing fails in the middle redis-trib will try 7# to recover. 8# - When redis-trib performs a cluster check, if it detects a slot move in 9# progress it should prompt the user to continue the move from where it 10# stopped. 11# - Gracefully handle Ctrl+C in move_slot to prompt the user if really stop 12# while rehashing, and performing the best cleanup possible if the user 13# forces the quit. 14# - When doing "fix" set a global Fix to true, and prompt the user to 15# fix the problem if automatically fixable every time there is something 16# to fix. For instance: 17# 1) If there is a node that pretend to receive a slot, or to migrate a 18# slot, but has no entries in that slot, fix it. 19# 2) If there is a node having keys in slots that are not owned by it 20# fix this condition moving the entries in the same node. 21# 3) Perform more possibly slow tests about the state of the cluster. 22# 4) When aborted slot migration is detected, fix it. 23 24require 'rubygems' 25require 'redis' 26 27ClusterHashSlots = 16384 28MigrateDefaultTimeout = 60000 29MigrateDefaultPipeline = 10 30RebalanceDefaultThreshold = 2 31 32$verbose = false 33 34def xputs(s) 35 case s[0..2] 36 when ">>>" 37 color="29;1" 38 when "[ER" 39 color="31;1" 40 when "[WA" 41 color="31;1" 42 when "[OK" 43 color="32" 44 when "[FA","***" 45 color="33" 46 else 47 color=nil 48 end 49 50 color = nil if ENV['TERM'] != "xterm" 51 print "\033[#{color}m" if color 52 print s 53 print "\033[0m" if color 54 print "\n" 55end 56 57class ClusterNode 58 def initialize(addr) 59 s = addr.split(":") 60 if s.length < 2 61 puts "Invalid IP or Port (given as #{addr}) - use IP:Port format" 62 exit 1 63 end 64 port = s.pop # removes port from split array 65 ip = s.join(":") # if s.length > 1 here, it's IPv6, so restore address 66 @r = nil 67 @info = {} 68 @info[:host] = ip 69 @info[:port] = port 70 @info[:slots] = {} 71 @info[:migrating] = {} 72 @info[:importing] = {} 73 @info[:replicate] = false 74 @dirty = false # True if we need to flush slots info into node. 75 @friends = [] 76 end 77 78 def friends 79 @friends 80 end 81 82 def slots 83 @info[:slots] 84 end 85 86 def has_flag?(flag) 87 @info[:flags].index(flag) 88 end 89 90 def to_s 91 "#{@info[:host]}:#{@info[:port]}" 92 end 93 94 def connect(o={}) 95 return if @r 96 print "Connecting to node #{self}: " if $verbose 97 STDOUT.flush 98 begin 99 @r = Redis.new(:host => @info[:host], :port => @info[:port], :timeout => 60) 100 @r.ping 101 rescue 102 xputs "[ERR] Sorry, can't connect to node #{self}" 103 exit 1 if o[:abort] 104 @r = nil 105 end 106 xputs "OK" if $verbose 107 end 108 109 def assert_cluster 110 info = @r.info 111 if !info["cluster_enabled"] || info["cluster_enabled"].to_i == 0 112 xputs "[ERR] Node #{self} is not configured as a cluster node." 113 exit 1 114 end 115 end 116 117 def assert_empty 118 if !(@r.cluster("info").split("\r\n").index("cluster_known_nodes:1")) || 119 (@r.info['db0']) 120 xputs "[ERR] Node #{self} is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0." 121 exit 1 122 end 123 end 124 125 def load_info(o={}) 126 self.connect 127 nodes = @r.cluster("nodes").split("\n") 128 nodes.each{|n| 129 # name addr flags role ping_sent ping_recv link_status slots 130 split = n.split 131 name,addr,flags,master_id,ping_sent,ping_recv,config_epoch,link_status = split[0..6] 132 slots = split[8..-1] 133 info = { 134 :name => name, 135 :addr => addr, 136 :flags => flags.split(","), 137 :replicate => master_id, 138 :ping_sent => ping_sent.to_i, 139 :ping_recv => ping_recv.to_i, 140 :link_status => link_status 141 } 142 info[:replicate] = false if master_id == "-" 143 144 if info[:flags].index("myself") 145 @info = @info.merge(info) 146 @info[:slots] = {} 147 slots.each{|s| 148 if s[0..0] == '[' 149 if s.index("->-") # Migrating 150 slot,dst = s[1..-1].split("->-") 151 @info[:migrating][slot.to_i] = dst 152 elsif s.index("-<-") # Importing 153 slot,src = s[1..-1].split("-<-") 154 @info[:importing][slot.to_i] = src 155 end 156 elsif s.index("-") 157 start,stop = s.split("-") 158 self.add_slots((start.to_i)..(stop.to_i)) 159 else 160 self.add_slots((s.to_i)..(s.to_i)) 161 end 162 } if slots 163 @dirty = false 164 @r.cluster("info").split("\n").each{|e| 165 k,v=e.split(":") 166 k = k.to_sym 167 v.chop! 168 if k != :cluster_state 169 @info[k] = v.to_i 170 else 171 @info[k] = v 172 end 173 } 174 elsif o[:getfriends] 175 @friends << info 176 end 177 } 178 end 179 180 def add_slots(slots) 181 slots.each{|s| 182 @info[:slots][s] = :new 183 } 184 @dirty = true 185 end 186 187 def set_as_replica(node_id) 188 @info[:replicate] = node_id 189 @dirty = true 190 end 191 192 def flush_node_config 193 return if !@dirty 194 if @info[:replicate] 195 begin 196 @r.cluster("replicate",@info[:replicate]) 197 rescue 198 # If the cluster did not already joined it is possible that 199 # the slave does not know the master node yet. So on errors 200 # we return ASAP leaving the dirty flag set, to flush the 201 # config later. 202 return 203 end 204 else 205 new = [] 206 @info[:slots].each{|s,val| 207 if val == :new 208 new << s 209 @info[:slots][s] = true 210 end 211 } 212 @r.cluster("addslots",*new) 213 end 214 @dirty = false 215 end 216 217 def info_string 218 # We want to display the hash slots assigned to this node 219 # as ranges, like in: "1-5,8-9,20-25,30" 220 # 221 # Note: this could be easily written without side effects, 222 # we use 'slots' just to split the computation into steps. 223 224 # First step: we want an increasing array of integers 225 # for instance: [1,2,3,4,5,8,9,20,21,22,23,24,25,30] 226 slots = @info[:slots].keys.sort 227 228 # As we want to aggregate adjacent slots we convert all the 229 # slot integers into ranges (with just one element) 230 # So we have something like [1..1,2..2, ... and so forth. 231 slots.map!{|x| x..x} 232 233 # Finally we group ranges with adjacent elements. 234 slots = slots.reduce([]) {|a,b| 235 if !a.empty? && b.first == (a[-1].last)+1 236 a[0..-2] + [(a[-1].first)..(b.last)] 237 else 238 a + [b] 239 end 240 } 241 242 # Now our task is easy, we just convert ranges with just one 243 # element into a number, and a real range into a start-end format. 244 # Finally we join the array using the comma as separator. 245 slots = slots.map{|x| 246 x.count == 1 ? x.first.to_s : "#{x.first}-#{x.last}" 247 }.join(",") 248 249 role = self.has_flag?("master") ? "M" : "S" 250 251 if self.info[:replicate] and @dirty 252 is = "S: #{self.info[:name]} #{self.to_s}" 253 else 254 is = "#{role}: #{self.info[:name]} #{self.to_s}\n"+ 255 " slots:#{slots} (#{self.slots.length} slots) "+ 256 "#{(self.info[:flags]-["myself"]).join(",")}" 257 end 258 if self.info[:replicate] 259 is += "\n replicates #{info[:replicate]}" 260 elsif self.has_flag?("master") && self.info[:replicas] 261 is += "\n #{info[:replicas].length} additional replica(s)" 262 end 263 is 264 end 265 266 # Return a single string representing nodes and associated slots. 267 # TODO: remove slaves from config when slaves will be handled 268 # by Redis Cluster. 269 def get_config_signature 270 config = [] 271 @r.cluster("nodes").each_line{|l| 272 s = l.split 273 slots = s[8..-1].select {|x| x[0..0] != "["} 274 next if slots.length == 0 275 config << s[0]+":"+(slots.sort.join(",")) 276 } 277 config.sort.join("|") 278 end 279 280 def info 281 @info 282 end 283 284 def is_dirty? 285 @dirty 286 end 287 288 def r 289 @r 290 end 291end 292 293class RedisTrib 294 def initialize 295 @nodes = [] 296 @fix = false 297 @errors = [] 298 @timeout = MigrateDefaultTimeout 299 end 300 301 def check_arity(req_args, num_args) 302 if ((req_args > 0 and num_args != req_args) || 303 (req_args < 0 and num_args < req_args.abs)) 304 xputs "[ERR] Wrong number of arguments for specified sub command" 305 exit 1 306 end 307 end 308 309 def add_node(node) 310 @nodes << node 311 end 312 313 def reset_nodes 314 @nodes = [] 315 end 316 317 def cluster_error(msg) 318 @errors << msg 319 xputs msg 320 end 321 322 # Return the node with the specified ID or Nil. 323 def get_node_by_name(name) 324 @nodes.each{|n| 325 return n if n.info[:name] == name.downcase 326 } 327 return nil 328 end 329 330 # Like get_node_by_name but the specified name can be just the first 331 # part of the node ID as long as the prefix in unique across the 332 # cluster. 333 def get_node_by_abbreviated_name(name) 334 l = name.length 335 candidates = [] 336 @nodes.each{|n| 337 if n.info[:name][0...l] == name.downcase 338 candidates << n 339 end 340 } 341 return nil if candidates.length != 1 342 candidates[0] 343 end 344 345 # This function returns the master that has the least number of replicas 346 # in the cluster. If there are multiple masters with the same smaller 347 # number of replicas, one at random is returned. 348 def get_master_with_least_replicas 349 masters = @nodes.select{|n| n.has_flag? "master"} 350 sorted = masters.sort{|a,b| 351 a.info[:replicas].length <=> b.info[:replicas].length 352 } 353 sorted[0] 354 end 355 356 def check_cluster(opt={}) 357 xputs ">>> Performing Cluster Check (using node #{@nodes[0]})" 358 show_nodes if !opt[:quiet] 359 check_config_consistency 360 check_open_slots 361 check_slots_coverage 362 end 363 364 def show_cluster_info 365 masters = 0 366 keys = 0 367 @nodes.each{|n| 368 if n.has_flag?("master") 369 puts "#{n} (#{n.info[:name][0...8]}...) -> #{n.r.dbsize} keys | #{n.slots.length} slots | "+ 370 "#{n.info[:replicas].length} slaves." 371 masters += 1 372 keys += n.r.dbsize 373 end 374 } 375 xputs "[OK] #{keys} keys in #{masters} masters." 376 keys_per_slot = sprintf("%.2f",keys/16384.0) 377 puts "#{keys_per_slot} keys per slot on average." 378 end 379 380 # Merge slots of every known node. If the resulting slots are equal 381 # to ClusterHashSlots, then all slots are served. 382 def covered_slots 383 slots = {} 384 @nodes.each{|n| 385 slots = slots.merge(n.slots) 386 } 387 slots 388 end 389 390 def check_slots_coverage 391 xputs ">>> Check slots coverage..." 392 slots = covered_slots 393 if slots.length == ClusterHashSlots 394 xputs "[OK] All #{ClusterHashSlots} slots covered." 395 else 396 cluster_error \ 397 "[ERR] Not all #{ClusterHashSlots} slots are covered by nodes." 398 fix_slots_coverage if @fix 399 end 400 end 401 402 def check_open_slots 403 xputs ">>> Check for open slots..." 404 open_slots = [] 405 @nodes.each{|n| 406 if n.info[:migrating].size > 0 407 cluster_error \ 408 "[WARNING] Node #{n} has slots in migrating state (#{n.info[:migrating].keys.join(",")})." 409 open_slots += n.info[:migrating].keys 410 elsif n.info[:importing].size > 0 411 cluster_error \ 412 "[WARNING] Node #{n} has slots in importing state (#{n.info[:importing].keys.join(",")})." 413 open_slots += n.info[:importing].keys 414 end 415 } 416 open_slots.uniq! 417 if open_slots.length > 0 418 xputs "[WARNING] The following slots are open: #{open_slots.join(",")}" 419 end 420 if @fix 421 open_slots.each{|slot| fix_open_slot slot} 422 end 423 end 424 425 def nodes_with_keys_in_slot(slot) 426 nodes = [] 427 @nodes.each{|n| 428 next if n.has_flag?("slave") 429 nodes << n if n.r.cluster("getkeysinslot",slot,1).length > 0 430 } 431 nodes 432 end 433 434 def fix_slots_coverage 435 not_covered = (0...ClusterHashSlots).to_a - covered_slots.keys 436 xputs ">>> Fixing slots coverage..." 437 xputs "List of not covered slots: " + not_covered.join(",") 438 439 # For every slot, take action depending on the actual condition: 440 # 1) No node has keys for this slot. 441 # 2) A single node has keys for this slot. 442 # 3) Multiple nodes have keys for this slot. 443 slots = {} 444 not_covered.each{|slot| 445 nodes = nodes_with_keys_in_slot(slot) 446 slots[slot] = nodes 447 xputs "Slot #{slot} has keys in #{nodes.length} nodes: #{nodes.join(", ")}" 448 } 449 450 none = slots.select {|k,v| v.length == 0} 451 single = slots.select {|k,v| v.length == 1} 452 multi = slots.select {|k,v| v.length > 1} 453 454 # Handle case "1": keys in no node. 455 if none.length > 0 456 xputs "The folowing uncovered slots have no keys across the cluster:" 457 xputs none.keys.join(",") 458 yes_or_die "Fix these slots by covering with a random node?" 459 none.each{|slot,nodes| 460 node = @nodes.sample 461 xputs ">>> Covering slot #{slot} with #{node}" 462 node.r.cluster("addslots",slot) 463 } 464 end 465 466 # Handle case "2": keys only in one node. 467 if single.length > 0 468 xputs "The folowing uncovered slots have keys in just one node:" 469 puts single.keys.join(",") 470 yes_or_die "Fix these slots by covering with those nodes?" 471 single.each{|slot,nodes| 472 xputs ">>> Covering slot #{slot} with #{nodes[0]}" 473 nodes[0].r.cluster("addslots",slot) 474 } 475 end 476 477 # Handle case "3": keys in multiple nodes. 478 if multi.length > 0 479 xputs "The folowing uncovered slots have keys in multiple nodes:" 480 xputs multi.keys.join(",") 481 yes_or_die "Fix these slots by moving keys into a single node?" 482 multi.each{|slot,nodes| 483 target = get_node_with_most_keys_in_slot(nodes,slot) 484 xputs ">>> Covering slot #{slot} moving keys to #{target}" 485 486 target.r.cluster('addslots',slot) 487 target.r.cluster('setslot',slot,'stable') 488 nodes.each{|src| 489 next if src == target 490 # Set the source node in 'importing' state (even if we will 491 # actually migrate keys away) in order to avoid receiving 492 # redirections for MIGRATE. 493 src.r.cluster('setslot',slot,'importing',target.info[:name]) 494 move_slot(src,target,slot,:dots=>true,:fix=>true,:cold=>true) 495 src.r.cluster('setslot',slot,'stable') 496 } 497 } 498 end 499 end 500 501 # Return the owner of the specified slot 502 def get_slot_owners(slot) 503 owners = [] 504 @nodes.each{|n| 505 next if n.has_flag?("slave") 506 n.slots.each{|s,_| 507 owners << n if s == slot 508 } 509 } 510 owners 511 end 512 513 # Return the node, among 'nodes' with the greatest number of keys 514 # in the specified slot. 515 def get_node_with_most_keys_in_slot(nodes,slot) 516 best = nil 517 best_numkeys = 0 518 @nodes.each{|n| 519 next if n.has_flag?("slave") 520 numkeys = n.r.cluster("countkeysinslot",slot) 521 if numkeys > best_numkeys || best == nil 522 best = n 523 best_numkeys = numkeys 524 end 525 } 526 return best 527 end 528 529 # Slot 'slot' was found to be in importing or migrating state in one or 530 # more nodes. This function fixes this condition by migrating keys where 531 # it seems more sensible. 532 def fix_open_slot(slot) 533 puts ">>> Fixing open slot #{slot}" 534 535 # Try to obtain the current slot owner, according to the current 536 # nodes configuration. 537 owners = get_slot_owners(slot) 538 owner = owners[0] if owners.length == 1 539 540 migrating = [] 541 importing = [] 542 @nodes.each{|n| 543 next if n.has_flag? "slave" 544 if n.info[:migrating][slot] 545 migrating << n 546 elsif n.info[:importing][slot] 547 importing << n 548 elsif n.r.cluster("countkeysinslot",slot) > 0 && n != owner 549 xputs "*** Found keys about slot #{slot} in node #{n}!" 550 importing << n 551 end 552 } 553 puts "Set as migrating in: #{migrating.join(",")}" 554 puts "Set as importing in: #{importing.join(",")}" 555 556 # If there is no slot owner, set as owner the slot with the biggest 557 # number of keys, among the set of migrating / importing nodes. 558 if !owner 559 xputs ">>> Nobody claims ownership, selecting an owner..." 560 owner = get_node_with_most_keys_in_slot(@nodes,slot) 561 562 # If we still don't have an owner, we can't fix it. 563 if !owner 564 xputs "[ERR] Can't select a slot owner. Impossible to fix." 565 exit 1 566 end 567 568 # Use ADDSLOTS to assign the slot. 569 puts "*** Configuring #{owner} as the slot owner" 570 n.r.cluster("setslot",slot,"stable") 571 n.r.cluster("addslot",slot) 572 # Make sure this information will propagate. Not strictly needed 573 # since there is no past owner, so all the other nodes will accept 574 # whatever epoch this node will claim the slot with. 575 n.r.cluster("bumpepoch") 576 577 # Remove the owner from the list of migrating/importing 578 # nodes. 579 migrating.delete(n) 580 importing.delete(n) 581 end 582 583 # If there are multiple owners of the slot, we need to fix it 584 # so that a single node is the owner and all the other nodes 585 # are in importing state. Later the fix can be handled by one 586 # of the base cases above. 587 # 588 # Note that this case also covers multiple nodes having the slot 589 # in migrating state, since migrating is a valid state only for 590 # slot owners. 591 if owners.length > 1 592 owner = get_node_with_most_keys_in_slot(owners,slot) 593 owners.each{|n| 594 next if n == owner 595 n.r.cluster('delslots',slot) 596 n.r.cluster('setslot',slot,'importing',owner.info[:name]) 597 importing.delete(n) # Avoid duplciates 598 importing << n 599 } 600 owner.r.cluster('bumpepoch') 601 end 602 603 # Case 1: The slot is in migrating state in one slot, and in 604 # importing state in 1 slot. That's trivial to address. 605 if migrating.length == 1 && importing.length == 1 606 move_slot(migrating[0],importing[0],slot,:dots=>true,:fix=>true) 607 # Case 2: There are multiple nodes that claim the slot as importing, 608 # they probably got keys about the slot after a restart so opened 609 # the slot. In this case we just move all the keys to the owner 610 # according to the configuration. 611 elsif migrating.length == 0 && importing.length > 0 612 xputs ">>> Moving all the #{slot} slot keys to its owner #{owner}" 613 importing.each {|node| 614 next if node == owner 615 move_slot(node,owner,slot,:dots=>true,:fix=>true,:cold=>true) 616 xputs ">>> Setting #{slot} as STABLE in #{node}" 617 node.r.cluster("setslot",slot,"stable") 618 } 619 # Case 3: There are no slots claiming to be in importing state, but 620 # there is a migrating node that actually don't have any key. We 621 # can just close the slot, probably a reshard interrupted in the middle. 622 elsif importing.length == 0 && migrating.length == 1 && 623 migrating[0].r.cluster("getkeysinslot",slot,10).length == 0 624 migrating[0].r.cluster("setslot",slot,"stable") 625 else 626 xputs "[ERR] Sorry, Redis-trib can't fix this slot yet (work in progress). Slot is set as migrating in #{migrating.join(",")}, as importing in #{importing.join(",")}, owner is #{owner}" 627 end 628 end 629 630 # Check if all the nodes agree about the cluster configuration 631 def check_config_consistency 632 if !is_config_consistent? 633 cluster_error "[ERR] Nodes don't agree about configuration!" 634 else 635 xputs "[OK] All nodes agree about slots configuration." 636 end 637 end 638 639 def is_config_consistent? 640 signatures=[] 641 @nodes.each{|n| 642 signatures << n.get_config_signature 643 } 644 return signatures.uniq.length == 1 645 end 646 647 def wait_cluster_join 648 print "Waiting for the cluster to join" 649 while !is_config_consistent? 650 print "." 651 STDOUT.flush 652 sleep 1 653 end 654 print "\n" 655 end 656 657 def alloc_slots 658 nodes_count = @nodes.length 659 masters_count = @nodes.length / (@replicas+1) 660 masters = [] 661 662 # The first step is to split instances by IP. This is useful as 663 # we'll try to allocate master nodes in different physical machines 664 # (as much as possible) and to allocate slaves of a given master in 665 # different physical machines as well. 666 # 667 # This code assumes just that if the IP is different, than it is more 668 # likely that the instance is running in a different physical host 669 # or at least a different virtual machine. 670 ips = {} 671 @nodes.each{|n| 672 ips[n.info[:host]] = [] if !ips[n.info[:host]] 673 ips[n.info[:host]] << n 674 } 675 676 # Select master instances 677 puts "Using #{masters_count} masters:" 678 interleaved = [] 679 stop = false 680 while not stop do 681 # Take one node from each IP until we run out of nodes 682 # across every IP. 683 ips.each do |ip,nodes| 684 if nodes.empty? 685 # if this IP has no remaining nodes, check for termination 686 if interleaved.length == nodes_count 687 # stop when 'interleaved' has accumulated all nodes 688 stop = true 689 next 690 end 691 else 692 # else, move one node from this IP to 'interleaved' 693 interleaved.push nodes.shift 694 end 695 end 696 end 697 698 masters = interleaved.slice!(0, masters_count) 699 nodes_count -= masters.length 700 701 masters.each{|m| puts m} 702 703 # Alloc slots on masters 704 slots_per_node = ClusterHashSlots.to_f / masters_count 705 first = 0 706 cursor = 0.0 707 masters.each_with_index{|n,masternum| 708 last = (cursor+slots_per_node-1).round 709 if last > ClusterHashSlots || masternum == masters.length-1 710 last = ClusterHashSlots-1 711 end 712 last = first if last < first # Min step is 1. 713 n.add_slots first..last 714 first = last+1 715 cursor += slots_per_node 716 } 717 718 # Select N replicas for every master. 719 # We try to split the replicas among all the IPs with spare nodes 720 # trying to avoid the host where the master is running, if possible. 721 # 722 # Note we loop two times. The first loop assigns the requested 723 # number of replicas to each master. The second loop assigns any 724 # remaining instances as extra replicas to masters. Some masters 725 # may end up with more than their requested number of replicas, but 726 # all nodes will be used. 727 assignment_verbose = false 728 729 [:requested,:unused].each do |assign| 730 masters.each do |m| 731 assigned_replicas = 0 732 while assigned_replicas < @replicas 733 break if nodes_count == 0 734 if assignment_verbose 735 if assign == :requested 736 puts "Requesting total of #{@replicas} replicas " \ 737 "(#{assigned_replicas} replicas assigned " \ 738 "so far with #{nodes_count} total remaining)." 739 elsif assign == :unused 740 puts "Assigning extra instance to replication " \ 741 "role too (#{nodes_count} remaining)." 742 end 743 end 744 745 # Return the first node not matching our current master 746 node = interleaved.find{|n| n.info[:host] != m.info[:host]} 747 748 # If we found a node, use it as a best-first match. 749 # Otherwise, we didn't find a node on a different IP, so we 750 # go ahead and use a same-IP replica. 751 if node 752 slave = node 753 interleaved.delete node 754 else 755 slave = interleaved.shift 756 end 757 slave.set_as_replica(m.info[:name]) 758 nodes_count -= 1 759 assigned_replicas += 1 760 puts "Adding replica #{slave} to #{m}" 761 762 # If we are in the "assign extra nodes" loop, 763 # we want to assign one extra replica to each 764 # master before repeating masters. 765 # This break lets us assign extra replicas to masters 766 # in a round-robin way. 767 break if assign == :unused 768 end 769 end 770 end 771 end 772 773 def flush_nodes_config 774 @nodes.each{|n| 775 n.flush_node_config 776 } 777 end 778 779 def show_nodes 780 @nodes.each{|n| 781 xputs n.info_string 782 } 783 end 784 785 # Redis Cluster config epoch collision resolution code is able to eventually 786 # set a different epoch to each node after a new cluster is created, but 787 # it is slow compared to assign a progressive config epoch to each node 788 # before joining the cluster. However we do just a best-effort try here 789 # since if we fail is not a problem. 790 def assign_config_epoch 791 config_epoch = 1 792 @nodes.each{|n| 793 begin 794 n.r.cluster("set-config-epoch",config_epoch) 795 rescue 796 end 797 config_epoch += 1 798 } 799 end 800 801 def join_cluster 802 # We use a brute force approach to make sure the node will meet 803 # each other, that is, sending CLUSTER MEET messages to all the nodes 804 # about the very same node. 805 # Thanks to gossip this information should propagate across all the 806 # cluster in a matter of seconds. 807 first = false 808 @nodes.each{|n| 809 if !first then first = n.info; next; end # Skip the first node 810 n.r.cluster("meet",first[:host],first[:port]) 811 } 812 end 813 814 def yes_or_die(msg) 815 print "#{msg} (type 'yes' to accept): " 816 STDOUT.flush 817 if !(STDIN.gets.chomp.downcase == "yes") 818 xputs "*** Aborting..." 819 exit 1 820 end 821 end 822 823 def load_cluster_info_from_node(nodeaddr) 824 node = ClusterNode.new(nodeaddr) 825 node.connect(:abort => true) 826 node.assert_cluster 827 node.load_info(:getfriends => true) 828 add_node(node) 829 node.friends.each{|f| 830 next if f[:flags].index("noaddr") || 831 f[:flags].index("disconnected") || 832 f[:flags].index("fail") 833 fnode = ClusterNode.new(f[:addr]) 834 fnode.connect() 835 next if !fnode.r 836 begin 837 fnode.load_info() 838 add_node(fnode) 839 rescue => e 840 xputs "[ERR] Unable to load info for node #{fnode}" 841 end 842 } 843 populate_nodes_replicas_info 844 end 845 846 # This function is called by load_cluster_info_from_node in order to 847 # add additional information to every node as a list of replicas. 848 def populate_nodes_replicas_info 849 # Start adding the new field to every node. 850 @nodes.each{|n| 851 n.info[:replicas] = [] 852 } 853 854 # Populate the replicas field using the replicate field of slave 855 # nodes. 856 @nodes.each{|n| 857 if n.info[:replicate] 858 master = get_node_by_name(n.info[:replicate]) 859 if !master 860 xputs "*** WARNING: #{n} claims to be slave of unknown node ID #{n.info[:replicate]}." 861 else 862 master.info[:replicas] << n 863 end 864 end 865 } 866 end 867 868 # Given a list of source nodes return a "resharding plan" 869 # with what slots to move in order to move "numslots" slots to another 870 # instance. 871 def compute_reshard_table(sources,numslots) 872 moved = [] 873 # Sort from bigger to smaller instance, for two reasons: 874 # 1) If we take less slots than instances it is better to start 875 # getting from the biggest instances. 876 # 2) We take one slot more from the first instance in the case of not 877 # perfect divisibility. Like we have 3 nodes and need to get 10 878 # slots, we take 4 from the first, and 3 from the rest. So the 879 # biggest is always the first. 880 sources = sources.sort{|a,b| b.slots.length <=> a.slots.length} 881 source_tot_slots = sources.inject(0) {|sum,source| 882 sum+source.slots.length 883 } 884 sources.each_with_index{|s,i| 885 # Every node will provide a number of slots proportional to the 886 # slots it has assigned. 887 n = (numslots.to_f/source_tot_slots*s.slots.length) 888 if i == 0 889 n = n.ceil 890 else 891 n = n.floor 892 end 893 s.slots.keys.sort[(0...n)].each{|slot| 894 if moved.length < numslots 895 moved << {:source => s, :slot => slot} 896 end 897 } 898 } 899 return moved 900 end 901 902 def show_reshard_table(table) 903 table.each{|e| 904 puts " Moving slot #{e[:slot]} from #{e[:source].info[:name]}" 905 } 906 end 907 908 # Move slots between source and target nodes using MIGRATE. 909 # 910 # Options: 911 # :verbose -- Print a dot for every moved key. 912 # :fix -- We are moving in the context of a fix. Use REPLACE. 913 # :cold -- Move keys without opening slots / reconfiguring the nodes. 914 # :update -- Update nodes.info[:slots] for source/target nodes. 915 # :quiet -- Don't print info messages. 916 def move_slot(source,target,slot,o={}) 917 o = {:pipeline => MigrateDefaultPipeline}.merge(o) 918 919 # We start marking the slot as importing in the destination node, 920 # and the slot as migrating in the target host. Note that the order of 921 # the operations is important, as otherwise a client may be redirected 922 # to the target node that does not yet know it is importing this slot. 923 if !o[:quiet] 924 print "Moving slot #{slot} from #{source} to #{target}: " 925 STDOUT.flush 926 end 927 928 if !o[:cold] 929 target.r.cluster("setslot",slot,"importing",source.info[:name]) 930 source.r.cluster("setslot",slot,"migrating",target.info[:name]) 931 end 932 # Migrate all the keys from source to target using the MIGRATE command 933 while true 934 keys = source.r.cluster("getkeysinslot",slot,o[:pipeline]) 935 break if keys.length == 0 936 begin 937 source.r.client.call(["migrate",target.info[:host],target.info[:port],"",0,@timeout,:keys,*keys]) 938 rescue => e 939 if o[:fix] && e.to_s =~ /BUSYKEY/ 940 xputs "*** Target key exists. Replacing it for FIX." 941 source.r.client.call(["migrate",target.info[:host],target.info[:port],"",0,@timeout,:replace,:keys,*keys]) 942 else 943 puts "" 944 xputs "[ERR] Calling MIGRATE: #{e}" 945 exit 1 946 end 947 end 948 print "."*keys.length if o[:dots] 949 STDOUT.flush 950 end 951 952 puts if !o[:quiet] 953 # Set the new node as the owner of the slot in all the known nodes. 954 if !o[:cold] 955 @nodes.each{|n| 956 next if n.has_flag?("slave") 957 n.r.cluster("setslot",slot,"node",target.info[:name]) 958 } 959 end 960 961 # Update the node logical config 962 if o[:update] then 963 source.info[:slots].delete(slot) 964 target.info[:slots][slot] = true 965 end 966 end 967 968 # redis-trib subcommands implementations. 969 970 def check_cluster_cmd(argv,opt) 971 load_cluster_info_from_node(argv[0]) 972 check_cluster 973 end 974 975 def info_cluster_cmd(argv,opt) 976 load_cluster_info_from_node(argv[0]) 977 show_cluster_info 978 end 979 980 def rebalance_cluster_cmd(argv,opt) 981 opt = { 982 'pipeline' => MigrateDefaultPipeline, 983 'threshold' => RebalanceDefaultThreshold 984 }.merge(opt) 985 986 # Load nodes info before parsing options, otherwise we can't 987 # handle --weight. 988 load_cluster_info_from_node(argv[0]) 989 990 # Options parsing 991 threshold = opt['threshold'].to_i 992 autoweights = opt['auto-weights'] 993 weights = {} 994 opt['weight'].each{|w| 995 fields = w.split("=") 996 node = get_node_by_abbreviated_name(fields[0]) 997 if !node || !node.has_flag?("master") 998 puts "*** No such master node #{fields[0]}" 999 exit 1 1000 end 1001 weights[node.info[:name]] = fields[1].to_f 1002 } if opt['weight'] 1003 useempty = opt['use-empty-masters'] 1004 1005 # Assign a weight to each node, and compute the total cluster weight. 1006 total_weight = 0 1007 nodes_involved = 0 1008 @nodes.each{|n| 1009 if n.has_flag?("master") 1010 next if !useempty && n.slots.length == 0 1011 n.info[:w] = weights[n.info[:name]] ? weights[n.info[:name]] : 1 1012 total_weight += n.info[:w] 1013 nodes_involved += 1 1014 end 1015 } 1016 1017 # Check cluster, only proceed if it looks sane. 1018 check_cluster(:quiet => true) 1019 if @errors.length != 0 1020 puts "*** Please fix your cluster problems before rebalancing" 1021 exit 1 1022 end 1023 1024 # Calculate the slots balance for each node. It's the number of 1025 # slots the node should lose (if positive) or gain (if negative) 1026 # in order to be balanced. 1027 threshold = opt['threshold'].to_f 1028 threshold_reached = false 1029 @nodes.each{|n| 1030 if n.has_flag?("master") 1031 next if !n.info[:w] 1032 expected = ((ClusterHashSlots.to_f / total_weight) * 1033 n.info[:w]).to_i 1034 n.info[:balance] = n.slots.length - expected 1035 # Compute the percentage of difference between the 1036 # expected number of slots and the real one, to see 1037 # if it's over the threshold specified by the user. 1038 over_threshold = false 1039 if threshold > 0 1040 if n.slots.length > 0 1041 err_perc = (100-(100.0*expected/n.slots.length)).abs 1042 over_threshold = true if err_perc > threshold 1043 elsif expected > 0 1044 over_threshold = true 1045 end 1046 end 1047 threshold_reached = true if over_threshold 1048 end 1049 } 1050 if !threshold_reached 1051 xputs "*** No rebalancing needed! All nodes are within the #{threshold}% threshold." 1052 return 1053 end 1054 1055 # Only consider nodes we want to change 1056 sn = @nodes.select{|n| 1057 n.has_flag?("master") && n.info[:w] 1058 } 1059 1060 # Because of rounding, it is possible that the balance of all nodes 1061 # summed does not give 0. Make sure that nodes that have to provide 1062 # slots are always matched by nodes receiving slots. 1063 total_balance = sn.map{|x| x.info[:balance]}.reduce{|a,b| a+b} 1064 while total_balance > 0 1065 sn.each{|n| 1066 if n.info[:balance] < 0 && total_balance > 0 1067 n.info[:balance] -= 1 1068 total_balance -= 1 1069 end 1070 } 1071 end 1072 1073 # Sort nodes by their slots balance. 1074 sn = sn.sort{|a,b| 1075 a.info[:balance] <=> b.info[:balance] 1076 } 1077 1078 xputs ">>> Rebalancing across #{nodes_involved} nodes. Total weight = #{total_weight}" 1079 1080 if $verbose 1081 sn.each{|n| 1082 puts "#{n} balance is #{n.info[:balance]} slots" 1083 } 1084 end 1085 1086 # Now we have at the start of the 'sn' array nodes that should get 1087 # slots, at the end nodes that must give slots. 1088 # We take two indexes, one at the start, and one at the end, 1089 # incrementing or decrementing the indexes accordingly til we 1090 # find nodes that need to get/provide slots. 1091 dst_idx = 0 1092 src_idx = sn.length - 1 1093 1094 while dst_idx < src_idx 1095 dst = sn[dst_idx] 1096 src = sn[src_idx] 1097 numslots = [dst.info[:balance],src.info[:balance]].map{|n| 1098 n.abs 1099 }.min 1100 1101 if numslots > 0 1102 puts "Moving #{numslots} slots from #{src} to #{dst}" 1103 1104 # Actaully move the slots. 1105 reshard_table = compute_reshard_table([src],numslots) 1106 if reshard_table.length != numslots 1107 xputs "*** Assertio failed: Reshard table != number of slots" 1108 exit 1 1109 end 1110 if opt['simulate'] 1111 print "#"*reshard_table.length 1112 else 1113 reshard_table.each{|e| 1114 move_slot(e[:source],dst,e[:slot], 1115 :quiet=>true, 1116 :dots=>false, 1117 :update=>true, 1118 :pipeline=>opt['pipeline']) 1119 print "#" 1120 STDOUT.flush 1121 } 1122 end 1123 puts 1124 end 1125 1126 # Update nodes balance. 1127 dst.info[:balance] += numslots 1128 src.info[:balance] -= numslots 1129 dst_idx += 1 if dst.info[:balance] == 0 1130 src_idx -= 1 if src.info[:balance] == 0 1131 end 1132 end 1133 1134 def fix_cluster_cmd(argv,opt) 1135 @fix = true 1136 @timeout = opt['timeout'].to_i if opt['timeout'] 1137 1138 load_cluster_info_from_node(argv[0]) 1139 check_cluster 1140 end 1141 1142 def reshard_cluster_cmd(argv,opt) 1143 opt = {'pipeline' => MigrateDefaultPipeline}.merge(opt) 1144 1145 load_cluster_info_from_node(argv[0]) 1146 check_cluster 1147 if @errors.length != 0 1148 puts "*** Please fix your cluster problems before resharding" 1149 exit 1 1150 end 1151 1152 @timeout = opt['timeout'].to_i if opt['timeout'].to_i 1153 1154 # Get number of slots 1155 if opt['slots'] 1156 numslots = opt['slots'].to_i 1157 else 1158 numslots = 0 1159 while numslots <= 0 or numslots > ClusterHashSlots 1160 print "How many slots do you want to move (from 1 to #{ClusterHashSlots})? " 1161 numslots = STDIN.gets.to_i 1162 end 1163 end 1164 1165 # Get the target instance 1166 if opt['to'] 1167 target = get_node_by_name(opt['to']) 1168 if !target || target.has_flag?("slave") 1169 xputs "*** The specified node is not known or not a master, please retry." 1170 exit 1 1171 end 1172 else 1173 target = nil 1174 while not target 1175 print "What is the receiving node ID? " 1176 target = get_node_by_name(STDIN.gets.chop) 1177 if !target || target.has_flag?("slave") 1178 xputs "*** The specified node is not known or not a master, please retry." 1179 target = nil 1180 end 1181 end 1182 end 1183 1184 # Get the source instances 1185 sources = [] 1186 if opt['from'] 1187 opt['from'].split(',').each{|node_id| 1188 if node_id == "all" 1189 sources = "all" 1190 break 1191 end 1192 src = get_node_by_name(node_id) 1193 if !src || src.has_flag?("slave") 1194 xputs "*** The specified node is not known or is not a master, please retry." 1195 exit 1 1196 end 1197 sources << src 1198 } 1199 else 1200 xputs "Please enter all the source node IDs." 1201 xputs " Type 'all' to use all the nodes as source nodes for the hash slots." 1202 xputs " Type 'done' once you entered all the source nodes IDs." 1203 while true 1204 print "Source node ##{sources.length+1}:" 1205 line = STDIN.gets.chop 1206 src = get_node_by_name(line) 1207 if line == "done" 1208 break 1209 elsif line == "all" 1210 sources = "all" 1211 break 1212 elsif !src || src.has_flag?("slave") 1213 xputs "*** The specified node is not known or is not a master, please retry." 1214 elsif src.info[:name] == target.info[:name] 1215 xputs "*** It is not possible to use the target node as source node." 1216 else 1217 sources << src 1218 end 1219 end 1220 end 1221 1222 if sources.length == 0 1223 puts "*** No source nodes given, operation aborted" 1224 exit 1 1225 end 1226 1227 # Handle soures == all. 1228 if sources == "all" 1229 sources = [] 1230 @nodes.each{|n| 1231 next if n.info[:name] == target.info[:name] 1232 next if n.has_flag?("slave") 1233 sources << n 1234 } 1235 end 1236 1237 # Check if the destination node is the same of any source nodes. 1238 if sources.index(target) 1239 xputs "*** Target node is also listed among the source nodes!" 1240 exit 1 1241 end 1242 1243 puts "\nReady to move #{numslots} slots." 1244 puts " Source nodes:" 1245 sources.each{|s| puts " "+s.info_string} 1246 puts " Destination node:" 1247 puts " #{target.info_string}" 1248 reshard_table = compute_reshard_table(sources,numslots) 1249 puts " Resharding plan:" 1250 show_reshard_table(reshard_table) 1251 if !opt['yes'] 1252 print "Do you want to proceed with the proposed reshard plan (yes/no)? " 1253 yesno = STDIN.gets.chop 1254 exit(1) if (yesno != "yes") 1255 end 1256 reshard_table.each{|e| 1257 move_slot(e[:source],target,e[:slot], 1258 :dots=>true, 1259 :pipeline=>opt['pipeline']) 1260 } 1261 end 1262 1263 # This is an helper function for create_cluster_cmd that verifies if 1264 # the number of nodes and the specified replicas have a valid configuration 1265 # where there are at least three master nodes and enough replicas per node. 1266 def check_create_parameters 1267 masters = @nodes.length/(@replicas+1) 1268 if masters < 3 1269 puts "*** ERROR: Invalid configuration for cluster creation." 1270 puts "*** Redis Cluster requires at least 3 master nodes." 1271 puts "*** This is not possible with #{@nodes.length} nodes and #{@replicas} replicas per node." 1272 puts "*** At least #{3*(@replicas+1)} nodes are required." 1273 exit 1 1274 end 1275 end 1276 1277 def create_cluster_cmd(argv,opt) 1278 opt = {'replicas' => 0}.merge(opt) 1279 @replicas = opt['replicas'].to_i 1280 1281 xputs ">>> Creating cluster" 1282 argv[0..-1].each{|n| 1283 node = ClusterNode.new(n) 1284 node.connect(:abort => true) 1285 node.assert_cluster 1286 node.load_info 1287 node.assert_empty 1288 add_node(node) 1289 } 1290 check_create_parameters 1291 xputs ">>> Performing hash slots allocation on #{@nodes.length} nodes..." 1292 alloc_slots 1293 show_nodes 1294 yes_or_die "Can I set the above configuration?" 1295 flush_nodes_config 1296 xputs ">>> Nodes configuration updated" 1297 xputs ">>> Assign a different config epoch to each node" 1298 assign_config_epoch 1299 xputs ">>> Sending CLUSTER MEET messages to join the cluster" 1300 join_cluster 1301 # Give one second for the join to start, in order to avoid that 1302 # wait_cluster_join will find all the nodes agree about the config as 1303 # they are still empty with unassigned slots. 1304 sleep 1 1305 wait_cluster_join 1306 flush_nodes_config # Useful for the replicas 1307 check_cluster 1308 end 1309 1310 def addnode_cluster_cmd(argv,opt) 1311 xputs ">>> Adding node #{argv[0]} to cluster #{argv[1]}" 1312 1313 # Check the existing cluster 1314 load_cluster_info_from_node(argv[1]) 1315 check_cluster 1316 1317 # If --master-id was specified, try to resolve it now so that we 1318 # abort before starting with the node configuration. 1319 if opt['slave'] 1320 if opt['master-id'] 1321 master = get_node_by_name(opt['master-id']) 1322 if !master 1323 xputs "[ERR] No such master ID #{opt['master-id']}" 1324 end 1325 else 1326 master = get_master_with_least_replicas 1327 xputs "Automatically selected master #{master}" 1328 end 1329 end 1330 1331 # Add the new node 1332 new = ClusterNode.new(argv[0]) 1333 new.connect(:abort => true) 1334 new.assert_cluster 1335 new.load_info 1336 new.assert_empty 1337 first = @nodes.first.info 1338 add_node(new) 1339 1340 # Send CLUSTER MEET command to the new node 1341 xputs ">>> Send CLUSTER MEET to node #{new} to make it join the cluster." 1342 new.r.cluster("meet",first[:host],first[:port]) 1343 1344 # Additional configuration is needed if the node is added as 1345 # a slave. 1346 if opt['slave'] 1347 wait_cluster_join 1348 xputs ">>> Configure node as replica of #{master}." 1349 new.r.cluster("replicate",master.info[:name]) 1350 end 1351 xputs "[OK] New node added correctly." 1352 end 1353 1354 def delnode_cluster_cmd(argv,opt) 1355 id = argv[1].downcase 1356 xputs ">>> Removing node #{id} from cluster #{argv[0]}" 1357 1358 # Load cluster information 1359 load_cluster_info_from_node(argv[0]) 1360 1361 # Check if the node exists and is not empty 1362 node = get_node_by_name(id) 1363 1364 if !node 1365 xputs "[ERR] No such node ID #{id}" 1366 exit 1 1367 end 1368 1369 if node.slots.length != 0 1370 xputs "[ERR] Node #{node} is not empty! Reshard data away and try again." 1371 exit 1 1372 end 1373 1374 # Send CLUSTER FORGET to all the nodes but the node to remove 1375 xputs ">>> Sending CLUSTER FORGET messages to the cluster..." 1376 @nodes.each{|n| 1377 next if n == node 1378 if n.info[:replicate] && n.info[:replicate].downcase == id 1379 # Reconfigure the slave to replicate with some other node 1380 master = get_master_with_least_replicas 1381 xputs ">>> #{n} as replica of #{master}" 1382 n.r.cluster("replicate",master.info[:name]) 1383 end 1384 n.r.cluster("forget",argv[1]) 1385 } 1386 1387 # Finally shutdown the node 1388 xputs ">>> SHUTDOWN the node." 1389 node.r.shutdown 1390 end 1391 1392 def set_timeout_cluster_cmd(argv,opt) 1393 timeout = argv[1].to_i 1394 if timeout < 100 1395 puts "Setting a node timeout of less than 100 milliseconds is a bad idea." 1396 exit 1 1397 end 1398 1399 # Load cluster information 1400 load_cluster_info_from_node(argv[0]) 1401 ok_count = 0 1402 err_count = 0 1403 1404 # Send CLUSTER FORGET to all the nodes but the node to remove 1405 xputs ">>> Reconfiguring node timeout in every cluster node..." 1406 @nodes.each{|n| 1407 begin 1408 n.r.config("set","cluster-node-timeout",timeout) 1409 n.r.config("rewrite") 1410 ok_count += 1 1411 xputs "*** New timeout set for #{n}" 1412 rescue => e 1413 puts "ERR setting node-timeot for #{n}: #{e}" 1414 err_count += 1 1415 end 1416 } 1417 xputs ">>> New node timeout set. #{ok_count} OK, #{err_count} ERR." 1418 end 1419 1420 def call_cluster_cmd(argv,opt) 1421 cmd = argv[1..-1] 1422 cmd[0] = cmd[0].upcase 1423 1424 # Load cluster information 1425 load_cluster_info_from_node(argv[0]) 1426 xputs ">>> Calling #{cmd.join(" ")}" 1427 @nodes.each{|n| 1428 begin 1429 res = n.r.send(*cmd) 1430 puts "#{n}: #{res}" 1431 rescue => e 1432 puts "#{n}: #{e}" 1433 end 1434 } 1435 end 1436 1437 def import_cluster_cmd(argv,opt) 1438 source_addr = opt['from'] 1439 xputs ">>> Importing data from #{source_addr} to cluster #{argv[1]}" 1440 use_copy = opt['copy'] 1441 use_replace = opt['replace'] 1442 1443 # Check the existing cluster. 1444 load_cluster_info_from_node(argv[0]) 1445 check_cluster 1446 1447 # Connect to the source node. 1448 xputs ">>> Connecting to the source Redis instance" 1449 src_host,src_port = source_addr.split(":") 1450 source = Redis.new(:host =>src_host, :port =>src_port) 1451 if source.info['cluster_enabled'].to_i == 1 1452 xputs "[ERR] The source node should not be a cluster node." 1453 end 1454 xputs "*** Importing #{source.dbsize} keys from DB 0" 1455 1456 # Build a slot -> node map 1457 slots = {} 1458 @nodes.each{|n| 1459 n.slots.each{|s,_| 1460 slots[s] = n 1461 } 1462 } 1463 1464 # Use SCAN to iterate over the keys, migrating to the 1465 # right node as needed. 1466 cursor = nil 1467 while cursor != 0 1468 cursor,keys = source.scan(cursor, :count => 1000) 1469 cursor = cursor.to_i 1470 keys.each{|k| 1471 # Migrate keys using the MIGRATE command. 1472 slot = key_to_slot(k) 1473 target = slots[slot] 1474 print "Migrating #{k} to #{target}: " 1475 STDOUT.flush 1476 begin 1477 cmd = ["migrate",target.info[:host],target.info[:port],k,0,@timeout] 1478 cmd << :copy if use_copy 1479 cmd << :replace if use_replace 1480 source.client.call(cmd) 1481 rescue => e 1482 puts e 1483 else 1484 puts "OK" 1485 end 1486 } 1487 end 1488 end 1489 1490 def help_cluster_cmd(argv,opt) 1491 show_help 1492 exit 0 1493 end 1494 1495 # Parse the options for the specific command "cmd". 1496 # Returns an hash populate with option => value pairs, and the index of 1497 # the first non-option argument in ARGV. 1498 def parse_options(cmd) 1499 idx = 1 ; # Current index into ARGV 1500 options={} 1501 while idx < ARGV.length && ARGV[idx][0..1] == '--' 1502 if ARGV[idx][0..1] == "--" 1503 option = ARGV[idx][2..-1] 1504 idx += 1 1505 1506 # --verbose is a global option 1507 if option == "verbose" 1508 $verbose = true 1509 next 1510 end 1511 1512 if ALLOWED_OPTIONS[cmd] == nil || ALLOWED_OPTIONS[cmd][option] == nil 1513 puts "Unknown option '#{option}' for command '#{cmd}'" 1514 exit 1 1515 end 1516 if ALLOWED_OPTIONS[cmd][option] != false 1517 value = ARGV[idx] 1518 idx += 1 1519 else 1520 value = true 1521 end 1522 1523 # If the option is set to [], it's a multiple arguments 1524 # option. We just queue every new value into an array. 1525 if ALLOWED_OPTIONS[cmd][option] == [] 1526 options[option] = [] if !options[option] 1527 options[option] << value 1528 else 1529 options[option] = value 1530 end 1531 else 1532 # Remaining arguments are not options. 1533 break 1534 end 1535 end 1536 1537 # Enforce mandatory options 1538 if ALLOWED_OPTIONS[cmd] 1539 ALLOWED_OPTIONS[cmd].each {|option,val| 1540 if !options[option] && val == :required 1541 puts "Option '--#{option}' is required "+ \ 1542 "for subcommand '#{cmd}'" 1543 exit 1 1544 end 1545 } 1546 end 1547 return options,idx 1548 end 1549end 1550 1551################################################################################# 1552# Libraries 1553# 1554# We try to don't depend on external libs since this is a critical part 1555# of Redis Cluster. 1556################################################################################# 1557 1558# This is the CRC16 algorithm used by Redis Cluster to hash keys. 1559# Implementation according to CCITT standards. 1560# 1561# This is actually the XMODEM CRC 16 algorithm, using the 1562# following parameters: 1563# 1564# Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN" 1565# Width : 16 bit 1566# Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1) 1567# Initialization : 0000 1568# Reflect Input byte : False 1569# Reflect Output CRC : False 1570# Xor constant to output CRC : 0000 1571# Output for "123456789" : 31C3 1572 1573module RedisClusterCRC16 1574 def RedisClusterCRC16.crc16(bytes) 1575 crc = 0 1576 bytes.each_byte{|b| 1577 crc = ((crc<<8) & 0xffff) ^ XMODEMCRC16Lookup[((crc>>8)^b) & 0xff] 1578 } 1579 crc 1580 end 1581 1582private 1583 XMODEMCRC16Lookup = [ 1584 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 1585 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, 1586 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, 1587 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, 1588 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, 1589 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, 1590 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, 1591 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, 1592 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, 1593 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, 1594 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, 1595 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, 1596 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, 1597 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, 1598 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, 1599 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, 1600 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, 1601 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, 1602 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, 1603 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, 1604 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, 1605 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, 1606 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, 1607 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, 1608 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, 1609 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, 1610 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, 1611 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, 1612 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, 1613 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, 1614 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, 1615 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 1616 ] 1617end 1618 1619# Turn a key name into the corrisponding Redis Cluster slot. 1620def key_to_slot(key) 1621 # Only hash what is inside {...} if there is such a pattern in the key. 1622 # Note that the specification requires the content that is between 1623 # the first { and the first } after the first {. If we found {} without 1624 # nothing in the middle, the whole key is hashed as usually. 1625 s = key.index "{" 1626 if s 1627 e = key.index "}",s+1 1628 if e && e != s+1 1629 key = key[s+1..e-1] 1630 end 1631 end 1632 RedisClusterCRC16.crc16(key) % 16384 1633end 1634 1635################################################################################# 1636# Definition of commands 1637################################################################################# 1638 1639COMMANDS={ 1640 "create" => ["create_cluster_cmd", -2, "host1:port1 ... hostN:portN"], 1641 "check" => ["check_cluster_cmd", 2, "host:port"], 1642 "info" => ["info_cluster_cmd", 2, "host:port"], 1643 "fix" => ["fix_cluster_cmd", 2, "host:port"], 1644 "reshard" => ["reshard_cluster_cmd", 2, "host:port"], 1645 "rebalance" => ["rebalance_cluster_cmd", -2, "host:port"], 1646 "add-node" => ["addnode_cluster_cmd", 3, "new_host:new_port existing_host:existing_port"], 1647 "del-node" => ["delnode_cluster_cmd", 3, "host:port node_id"], 1648 "set-timeout" => ["set_timeout_cluster_cmd", 3, "host:port milliseconds"], 1649 "call" => ["call_cluster_cmd", -3, "host:port command arg arg .. arg"], 1650 "import" => ["import_cluster_cmd", 2, "host:port"], 1651 "help" => ["help_cluster_cmd", 1, "(show this help)"] 1652} 1653 1654ALLOWED_OPTIONS={ 1655 "create" => {"replicas" => true}, 1656 "add-node" => {"slave" => false, "master-id" => true}, 1657 "import" => {"from" => :required, "copy" => false, "replace" => false}, 1658 "reshard" => {"from" => true, "to" => true, "slots" => true, "yes" => false, "timeout" => true, "pipeline" => true}, 1659 "rebalance" => {"weight" => [], "auto-weights" => false, "use-empty-masters" => false, "timeout" => true, "simulate" => false, "pipeline" => true, "threshold" => true}, 1660 "fix" => {"timeout" => MigrateDefaultTimeout}, 1661} 1662 1663def show_help 1664 puts "Usage: redis-trib <command> <options> <arguments ...>\n\n" 1665 COMMANDS.each{|k,v| 1666 o = "" 1667 puts " #{k.ljust(15)} #{v[2]}" 1668 if ALLOWED_OPTIONS[k] 1669 ALLOWED_OPTIONS[k].each{|optname,has_arg| 1670 puts " --#{optname}" + (has_arg ? " <arg>" : "") 1671 } 1672 end 1673 } 1674 puts "\nFor check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.\n" 1675end 1676 1677# Sanity check 1678if ARGV.length == 0 1679 show_help 1680 exit 1 1681end 1682 1683rt = RedisTrib.new 1684cmd_spec = COMMANDS[ARGV[0].downcase] 1685if !cmd_spec 1686 puts "Unknown redis-trib subcommand '#{ARGV[0]}'" 1687 exit 1 1688end 1689 1690# Parse options 1691cmd_options,first_non_option = rt.parse_options(ARGV[0].downcase) 1692rt.check_arity(cmd_spec[1],ARGV.length-(first_non_option-1)) 1693 1694# Dispatch 1695rt.send(cmd_spec[0],ARGV[first_non_option..-1],cmd_options) 1696