gizzmo 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/gizzmo.gemspec +8 -4
- data/lib/gizzard.rb +1 -0
- data/lib/gizzard/commands.rb +84 -103
- data/lib/gizzard/nameserver.rb +24 -3
- data/lib/gizzard/rebalancer.rb +125 -0
- data/lib/gizzard/thrift.rb +4 -2
- data/lib/gizzard/transformation.rb +36 -5
- data/lib/gizzard/transformation_scheduler.rb +19 -16
- data/lib/gizzmo.rb +20 -20
- data/test/gizzmo_spec.rb +251 -130
- data/test/nameserver_spec.rb +1 -1
- data/test/scheduler_spec.rb +10 -10
- data/test/spec_helper.rb +26 -5
- data/test/test_server/project/build/Project.scala +1 -1
- data/test/test_server/project/plugins/Plugins.scala +1 -1
- data/test/transformation_spec.rb +8 -0
- metadata +16 -4
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0.11. | 
| 1 | 
            +
            0.11.3
         | 
    
        data/gizzmo.gemspec
    CHANGED
    
    | @@ -5,11 +5,11 @@ | |
| 5 5 |  | 
| 6 6 | 
             
            Gem::Specification.new do |s|
         | 
| 7 7 | 
             
              s.name = %q{gizzmo}
         | 
| 8 | 
            -
              s.version = "0.11. | 
| 8 | 
            +
              s.version = "0.11.3"
         | 
| 9 9 |  | 
| 10 10 | 
             
              s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
         | 
| 11 11 | 
             
              s.authors = ["Kyle Maxwell"]
         | 
| 12 | 
            -
              s.date = %q{2011-01- | 
| 12 | 
            +
              s.date = %q{2011-01-26}
         | 
| 13 13 | 
             
              s.description = %q{Gizzmo is a command-line client for managing gizzard clusters.}
         | 
| 14 14 | 
             
              s.email = %q{kmaxwell@twitter.com}
         | 
| 15 15 | 
             
              s.executables = ["gizzmo", "setup_shards"]
         | 
| @@ -31,6 +31,7 @@ Gem::Specification.new do |s| | |
| 31 31 | 
             
                 "lib/gizzard/digest.rb",
         | 
| 32 32 | 
             
                 "lib/gizzard/migrator.rb",
         | 
| 33 33 | 
             
                 "lib/gizzard/nameserver.rb",
         | 
| 34 | 
            +
                 "lib/gizzard/rebalancer.rb",
         | 
| 34 35 | 
             
                 "lib/gizzard/shard_template.rb",
         | 
| 35 36 | 
             
                 "lib/gizzard/thrift.rb",
         | 
| 36 37 | 
             
                 "lib/gizzard/transformation.rb",
         | 
| @@ -76,7 +77,7 @@ Gem::Specification.new do |s| | |
| 76 77 | 
             
              s.homepage = %q{http://github.com/twitter/gizzmo}
         | 
| 77 78 | 
             
              s.rdoc_options = ["--charset=UTF-8"]
         | 
| 78 79 | 
             
              s.require_paths = ["lib"]
         | 
| 79 | 
            -
              s.rubygems_version = %q{1.3. | 
| 80 | 
            +
              s.rubygems_version = %q{1.3.7}
         | 
| 80 81 | 
             
              s.summary = %q{Gizzmo is a command-line client for managing gizzard clusters.}
         | 
| 81 82 | 
             
              s.test_files = [
         | 
| 82 83 | 
             
                "test/gizzmo_spec.rb",
         | 
| @@ -85,6 +86,9 @@ Gem::Specification.new do |s| | |
| 85 86 | 
             
                 "test/scheduler_spec.rb",
         | 
| 86 87 | 
             
                 "test/shard_template_spec.rb",
         | 
| 87 88 | 
             
                 "test/spec_helper.rb",
         | 
| 89 | 
            +
                 "test/test_server/target/gen-rb/test_server.rb",
         | 
| 90 | 
            +
                 "test/test_server/target/gen-rb/test_server_constants.rb",
         | 
| 91 | 
            +
                 "test/test_server/target/gen-rb/test_server_types.rb",
         | 
| 88 92 | 
             
                 "test/transformation_spec.rb"
         | 
| 89 93 | 
             
              ]
         | 
| 90 94 |  | 
| @@ -92,7 +96,7 @@ Gem::Specification.new do |s| | |
| 92 96 | 
             
                current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
         | 
| 93 97 | 
             
                s.specification_version = 3
         | 
| 94 98 |  | 
| 95 | 
            -
                if Gem::Version.new(Gem:: | 
| 99 | 
            +
                if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
         | 
| 96 100 | 
             
                else
         | 
| 97 101 | 
             
                end
         | 
| 98 102 | 
             
              else
         | 
    
        data/lib/gizzard.rb
    CHANGED
    
    
    
        data/lib/gizzard/commands.rb
    CHANGED
    
    | @@ -317,28 +317,6 @@ module Gizzard | |
| 317 317 | 
             
                end
         | 
| 318 318 | 
             
              end
         | 
| 319 319 |  | 
| 320 | 
            -
              class RepairCommand < Command
         | 
| 321 | 
            -
                def run
         | 
| 322 | 
            -
                  args = @argv.dup.map{|a| a.split(/\s+/)}.flatten
         | 
| 323 | 
            -
                  pairs = []
         | 
| 324 | 
            -
                  loop do
         | 
| 325 | 
            -
                    a = args.shift
         | 
| 326 | 
            -
                    b = args.shift
         | 
| 327 | 
            -
                    break unless a && b
         | 
| 328 | 
            -
                    pairs << [a, b]
         | 
| 329 | 
            -
                  end
         | 
| 330 | 
            -
                  pairs.each do |master, slave|
         | 
| 331 | 
            -
                    puts "#{master} #{slave}"
         | 
| 332 | 
            -
                    mprefixes = manager.shards_for_hostname(master).map{|s| s.id.table_prefix}
         | 
| 333 | 
            -
                    sprefixes = manager.shards_for_hostname(slave).map{|s| s.id.table_prefix}
         | 
| 334 | 
            -
                    delta = mprefixes - sprefixes
         | 
| 335 | 
            -
                    delta.each do |prefix|
         | 
| 336 | 
            -
                      puts "gizzmo copy #{master}/#{prefix} #{slave}/#{prefix}"
         | 
| 337 | 
            -
                    end
         | 
| 338 | 
            -
                  end
         | 
| 339 | 
            -
                end
         | 
| 340 | 
            -
              end
         | 
| 341 | 
            -
             | 
| 342 320 | 
             
              class WrapCommand < Command
         | 
| 343 321 | 
             
                def self.derive_wrapper_shard_id(shard_info, wrapping_class_name)
         | 
| 344 322 | 
             
                  suffix = "_" + wrapping_class_name.split(".").last.downcase.gsub("shard", "")
         | 
| @@ -366,84 +344,6 @@ module Gizzard | |
| 366 344 | 
             
                end
         | 
| 367 345 | 
             
              end
         | 
| 368 346 |  | 
| 369 | 
            -
              class RebalanceCommand < Command
         | 
| 370 | 
            -
             | 
| 371 | 
            -
                class NamedArray < Array
         | 
| 372 | 
            -
                  attr_reader :name
         | 
| 373 | 
            -
                  def initialize(name)
         | 
| 374 | 
            -
                    @name = name
         | 
| 375 | 
            -
                  end
         | 
| 376 | 
            -
                end
         | 
| 377 | 
            -
             | 
| 378 | 
            -
                def run
         | 
| 379 | 
            -
                  help! "No shards specified" if @argv.empty?
         | 
| 380 | 
            -
                  shards = []
         | 
| 381 | 
            -
                  command_options.write_only_shard ||= "com.twitter.gizzard.shards.WriteOnlyShard"
         | 
| 382 | 
            -
                  additional_hosts = (command_options.hosts || "").split(/[\s,]+/)
         | 
| 383 | 
            -
                  exclude_hosts = (command_options.exclude_hosts || "").split(/[\s,]+/)
         | 
| 384 | 
            -
                  ids = @argv.map{|arg| ShardId.new(*arg.split("/")) rescue nil }.compact
         | 
| 385 | 
            -
                  by_host = ids.inject({}) do |memo, id|
         | 
| 386 | 
            -
                    memo[id.hostname] ||= NamedArray.new(id.hostname)
         | 
| 387 | 
            -
                    memo[id.hostname] << id
         | 
| 388 | 
            -
                    memo
         | 
| 389 | 
            -
                  end
         | 
| 390 | 
            -
             | 
| 391 | 
            -
                  additional_hosts.each do |host|
         | 
| 392 | 
            -
                    by_host[host] ||= NamedArray.new(host)
         | 
| 393 | 
            -
                  end
         | 
| 394 | 
            -
             | 
| 395 | 
            -
                  exclude_hosts.each do |host|
         | 
| 396 | 
            -
                    by_host[host] ||= NamedArray.new(host)
         | 
| 397 | 
            -
                  end
         | 
| 398 | 
            -
             | 
| 399 | 
            -
                  sets = by_host.values
         | 
| 400 | 
            -
                  exclude_sets = exclude_hosts.map{|host| by_host[host]}
         | 
| 401 | 
            -
                  target_sets = sets - exclude_sets
         | 
| 402 | 
            -
             | 
| 403 | 
            -
                  exclude_sets.each do |set|
         | 
| 404 | 
            -
                    while set.length > 0
         | 
| 405 | 
            -
                      sorted = target_sets.sort_by{|s| s.length}
         | 
| 406 | 
            -
                      shortest = sorted.first
         | 
| 407 | 
            -
                      shortest.push set.pop
         | 
| 408 | 
            -
                    end
         | 
| 409 | 
            -
                  end
         | 
| 410 | 
            -
             | 
| 411 | 
            -
                  exclude_sets.each do |set|
         | 
| 412 | 
            -
                    while set.length > 0
         | 
| 413 | 
            -
                      sorted = target_sets.sort_by{|s| s.length}
         | 
| 414 | 
            -
                      shortest = sorted.first
         | 
| 415 | 
            -
                      shortest.push set.pop
         | 
| 416 | 
            -
                    end
         | 
| 417 | 
            -
                  end
         | 
| 418 | 
            -
             | 
| 419 | 
            -
                  begin
         | 
| 420 | 
            -
                    sorted = target_sets.sort_by{|s| s.length }
         | 
| 421 | 
            -
                    longest = sorted.last
         | 
| 422 | 
            -
                    shortest = sorted.first
         | 
| 423 | 
            -
                    shortest.push longest.pop
         | 
| 424 | 
            -
                  end while longest.length > shortest.length + 1
         | 
| 425 | 
            -
             | 
| 426 | 
            -
                  shard_info = nil
         | 
| 427 | 
            -
                  sets.each do |set|
         | 
| 428 | 
            -
                    host = set.name
         | 
| 429 | 
            -
                    set.each do |id|
         | 
| 430 | 
            -
                      if id.hostname != host
         | 
| 431 | 
            -
                        shard_info ||= manager.get_shard(id)
         | 
| 432 | 
            -
                        old = id.to_unix
         | 
| 433 | 
            -
                        id.hostname = host
         | 
| 434 | 
            -
                        shards << [old, id.to_unix]
         | 
| 435 | 
            -
                      end
         | 
| 436 | 
            -
                    end
         | 
| 437 | 
            -
                  end
         | 
| 438 | 
            -
             | 
| 439 | 
            -
                  new_shards = shards.map{|(old, new)| new }
         | 
| 440 | 
            -
             | 
| 441 | 
            -
                  puts "gizzmo create #{shard_info.class_name} -s '#{shard_info.source_type}' -d '#{shard_info.destination_type}' #{new_shards.join(" ")}"
         | 
| 442 | 
            -
                  puts "gizzmo wrap #{command_options.write_only_shard} #{new_shards.join(" ")}"
         | 
| 443 | 
            -
                  shards.map { |(old, new)| puts "gizzmo copy #{old} #{new}" }
         | 
| 444 | 
            -
                end
         | 
| 445 | 
            -
              end
         | 
| 446 | 
            -
             | 
| 447 347 | 
             
              class PairCommand < Command
         | 
| 448 348 | 
             
                def run
         | 
| 449 349 | 
             
                  ids = []
         | 
| @@ -791,17 +691,29 @@ module Gizzard | |
| 791 691 | 
             
                end
         | 
| 792 692 | 
             
              end
         | 
| 793 693 |  | 
| 694 | 
            +
              class TablesCommand < Command
         | 
| 695 | 
            +
                def run
         | 
| 696 | 
            +
                  puts manager.list_tables.join(" ")
         | 
| 697 | 
            +
                end
         | 
| 698 | 
            +
              end
         | 
| 699 | 
            +
             | 
| 794 700 | 
             
              class TopologyCommand < Command
         | 
| 795 701 | 
             
                def run
         | 
| 796 | 
            -
                   | 
| 702 | 
            +
                  manifest  = manager.manifest(*global_options.tables)
         | 
| 703 | 
            +
                  templates = manifest.templates.inject({}) do |h, (t, fs)|
         | 
| 797 704 | 
             
                    h.update t.to_config => fs
         | 
| 798 705 | 
             
                  end
         | 
| 799 706 |  | 
| 800 707 | 
             
                  if command_options.forwardings
         | 
| 801 708 | 
             
                    templates.
         | 
| 802 | 
            -
                      inject([]) { |h, (t, fs)| fs.each { |f| h << [f. | 
| 709 | 
            +
                      inject([]) { |h, (t, fs)| fs.each { |f| h << [f.inspect, t] }; h }.
         | 
| 803 710 | 
             
                      sort.
         | 
| 804 | 
            -
                      each { |a| puts "% | 
| 711 | 
            +
                      each { |a| puts "%s\t%s" % a }
         | 
| 712 | 
            +
                  elsif command_options.root_shards
         | 
| 713 | 
            +
                    templates.
         | 
| 714 | 
            +
                      inject([]) { |a, (t, fs)| fs.each { |f| a << [f.shard_id.inspect, t] }; a }.
         | 
| 715 | 
            +
                      sort.
         | 
| 716 | 
            +
                      each { |a| puts "%s\t%s" % a }
         | 
| 805 717 | 
             
                  else
         | 
| 806 718 | 
             
                    templates.
         | 
| 807 719 | 
             
                      map { |(t, fs)| [fs.length, t] }.
         | 
| @@ -830,6 +742,11 @@ module Gizzard | |
| 830 742 |  | 
| 831 743 | 
             
                  scheduler_options[:quiet] = be_quiet
         | 
| 832 744 |  | 
| 745 | 
            +
                  if transformation.noop?
         | 
| 746 | 
            +
                    puts "Nothing to do!"
         | 
| 747 | 
            +
                    exit
         | 
| 748 | 
            +
                  end
         | 
| 749 | 
            +
             | 
| 833 750 | 
             
                  unless be_quiet
         | 
| 834 751 | 
             
                    puts transformation.inspect
         | 
| 835 752 | 
             
                    puts ""
         | 
| @@ -869,6 +786,70 @@ module Gizzard | |
| 869 786 | 
             
                    transformations[transformation] = trees
         | 
| 870 787 | 
             
                  end
         | 
| 871 788 |  | 
| 789 | 
            +
                  transformations.reject! {|t,trees| t.noop? or trees.empty? }
         | 
| 790 | 
            +
             | 
| 791 | 
            +
                  if transformations.empty?
         | 
| 792 | 
            +
                    puts "Nothing to do!"
         | 
| 793 | 
            +
                    exit
         | 
| 794 | 
            +
                  end
         | 
| 795 | 
            +
             | 
| 796 | 
            +
                  base_name = transformations.values.first.values.first.id.table_prefix.split('_').first
         | 
| 797 | 
            +
             | 
| 798 | 
            +
                  unless be_quiet
         | 
| 799 | 
            +
                    transformations.sort.each do |transformation, trees|
         | 
| 800 | 
            +
                      puts transformation.inspect
         | 
| 801 | 
            +
                      puts "Applied to #{trees.length} shards:"
         | 
| 802 | 
            +
                      trees.keys.sort.each {|f| puts "  #{f.inspect}" }
         | 
| 803 | 
            +
                    end
         | 
| 804 | 
            +
                    puts ""
         | 
| 805 | 
            +
                  end
         | 
| 806 | 
            +
             | 
| 807 | 
            +
                  unless global_options.force
         | 
| 808 | 
            +
                    print "Continue? (y/n) "; $stdout.flush
         | 
| 809 | 
            +
                    exit unless $stdin.gets.chomp == "y"
         | 
| 810 | 
            +
                    puts ""
         | 
| 811 | 
            +
                  end
         | 
| 812 | 
            +
             | 
| 813 | 
            +
                  Gizzard.schedule! manager,
         | 
| 814 | 
            +
                                    base_name,
         | 
| 815 | 
            +
                                    transformations,
         | 
| 816 | 
            +
                                    scheduler_options
         | 
| 817 | 
            +
                end
         | 
| 818 | 
            +
              end
         | 
| 819 | 
            +
             | 
| 820 | 
            +
              class RebalanceCommand < Command
         | 
| 821 | 
            +
                def run
         | 
| 822 | 
            +
                  help!("must have an even number of arguments") unless @argv.length % 2 == 0
         | 
| 823 | 
            +
             | 
| 824 | 
            +
                  scheduler_options = command_options.scheduler_options || {}
         | 
| 825 | 
            +
                  manifest          = manager.manifest(*global_options.tables)
         | 
| 826 | 
            +
                  copy_wrapper      = scheduler_options[:copy_wrapper]
         | 
| 827 | 
            +
                  be_quiet          = global_options.force && command_options.quiet
         | 
| 828 | 
            +
                  transformations   = {}
         | 
| 829 | 
            +
             | 
| 830 | 
            +
                  scheduler_options[:quiet] = be_quiet
         | 
| 831 | 
            +
             | 
| 832 | 
            +
                  dest_templates_and_weights = {}
         | 
| 833 | 
            +
             | 
| 834 | 
            +
                  @argv.each_slice(2) do |(weight_s, to_template_s)|
         | 
| 835 | 
            +
                    to     = ShardTemplate.parse(to_template_s)
         | 
| 836 | 
            +
                    weight = weight_s.to_i
         | 
| 837 | 
            +
             | 
| 838 | 
            +
                    dest_templates_and_weights[to] = weight
         | 
| 839 | 
            +
                  end
         | 
| 840 | 
            +
             | 
| 841 | 
            +
                  transformations = global_options.tables.inject({}) do |all, table|
         | 
| 842 | 
            +
                    trees      = manifest.trees.reject {|(f, s)| f.table_id != table }
         | 
| 843 | 
            +
                    rebalancer = Rebalancer.new(trees, dest_templates_and_weights, copy_wrapper)
         | 
| 844 | 
            +
             | 
| 845 | 
            +
                    all.update(rebalancer.transformations) {|t,a,b| a.merge b }
         | 
| 846 | 
            +
                  end
         | 
| 847 | 
            +
             | 
| 848 | 
            +
                  if transformations.empty?
         | 
| 849 | 
            +
                    puts "Nothing to do!"
         | 
| 850 | 
            +
                    exit
         | 
| 851 | 
            +
                  end
         | 
| 852 | 
            +
             | 
| 872 853 | 
             
                  base_name = transformations.values.first.values.first.id.table_prefix.split('_').first
         | 
| 873 854 |  | 
| 874 855 | 
             
                  unless be_quiet
         | 
    
        data/lib/gizzard/nameserver.rb
    CHANGED
    
    | @@ -1,6 +1,17 @@ | |
| 1 1 | 
             
            module Gizzard
         | 
| 2 2 | 
             
              Shard = Struct.new(:info, :children, :weight)
         | 
| 3 3 |  | 
| 4 | 
            +
              module ParallelMap
         | 
| 5 | 
            +
                def parallel_map(enumerable, &block)
         | 
| 6 | 
            +
                  enumerable.map do |elem|
         | 
| 7 | 
            +
                    Thread.new { Thread.current[:result] = block.call(elem) }
         | 
| 8 | 
            +
                  end.map do |thread|
         | 
| 9 | 
            +
                    thread.join
         | 
| 10 | 
            +
                    thread[:result]
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 4 15 | 
             
              class Shard
         | 
| 5 16 | 
             
                class << self
         | 
| 6 17 | 
             
                  def canonical_table_prefix(enum, table_id = nil, base_prefix = "shard")
         | 
| @@ -75,6 +86,7 @@ module Gizzard | |
| 75 86 | 
             
              end
         | 
| 76 87 |  | 
| 77 88 | 
             
              class Nameserver
         | 
| 89 | 
            +
                include ParallelMap
         | 
| 78 90 |  | 
| 79 91 | 
             
                DEFAULT_PORT    = 7917
         | 
| 80 92 | 
             
                DEFAULT_RETRIES = 20
         | 
| @@ -96,8 +108,16 @@ module Gizzard | |
| 96 108 | 
             
                  ids.map {|id| with_retry { client.get_shard(id) } }
         | 
| 97 109 | 
             
                end
         | 
| 98 110 |  | 
| 111 | 
            +
                def reload_updated_forwardings
         | 
| 112 | 
            +
                  parallel_map all_clients do |c|
         | 
| 113 | 
            +
                    with_retry { c.reload_updated_forwardings }
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 99 117 | 
             
                def reload_config
         | 
| 100 | 
            -
                  all_clients | 
| 118 | 
            +
                  parallel_map all_clients do |c|
         | 
| 119 | 
            +
                    with_retry { c.reload_config }
         | 
| 120 | 
            +
                  end
         | 
| 101 121 | 
             
                end
         | 
| 102 122 |  | 
| 103 123 | 
             
                def copy_shard(from_shard_id, to_shard_id)
         | 
| @@ -154,11 +174,12 @@ module Gizzard | |
| 154 174 | 
             
                end
         | 
| 155 175 |  | 
| 156 176 | 
             
                class Manifest
         | 
| 157 | 
            -
                   | 
| 177 | 
            +
                  include ParallelMap
         | 
| 158 178 |  | 
| 179 | 
            +
                  attr_reader :forwardings, :links, :shard_infos, :trees, :templates
         | 
| 159 180 |  | 
| 160 181 | 
             
                  def initialize(nameserver, table_ids)
         | 
| 161 | 
            -
                    states =  | 
| 182 | 
            +
                    states = nameserver.dump_nameserver(table_ids)
         | 
| 162 183 |  | 
| 163 184 | 
             
                    @forwardings = states.map {|s| s.forwardings }.flatten
         | 
| 164 185 |  | 
| @@ -0,0 +1,125 @@ | |
| 1 | 
            +
            require 'set'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Gizzard
         | 
| 4 | 
            +
              class Rebalancer
         | 
| 5 | 
            +
                TemplateAndTree = Struct.new(:template, :forwarding, :tree)
         | 
| 6 | 
            +
                Bucket          = Struct.new(:template, :approx_shards, :set)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                class Bucket
         | 
| 9 | 
            +
                  def balance; set.length - approx_shards end
         | 
| 10 | 
            +
                  def add(e); set.add(e) end
         | 
| 11 | 
            +
                  def merge(es); set.merge(es) end
         | 
| 12 | 
            +
                  def delete(e); set.delete(e) end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # steps for rebalancing.
         | 
| 16 | 
            +
                #
         | 
| 17 | 
            +
                # 1. get a list of forwarding/template associations
         | 
| 18 | 
            +
                # 2. get a list of destination templates and weights
         | 
| 19 | 
            +
                # 3. order shards by weight. (ascending or descending)
         | 
| 20 | 
            +
                # 4. put shards in destinations based on reducing number of copies required.
         | 
| 21 | 
            +
                # 5.
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def initialize(forwardings_to_trees, dest_templates_and_weights, wrapper)
         | 
| 24 | 
            +
                  @copy_dest_wrapper = wrapper
         | 
| 25 | 
            +
                  @shards = forwardings_to_trees.map do |forwarding, tree|
         | 
| 26 | 
            +
                    TemplateAndTree.new(tree.template, forwarding, tree)
         | 
| 27 | 
            +
                  end.flatten
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  @dest_templates      = dest_templates_and_weights.keys
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  total_shards = @shards.length
         | 
| 32 | 
            +
                  total_weight = dest_templates_and_weights.values.inject {|a,b| a + b }
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  @result = dest_templates_and_weights.map do |template, weight|
         | 
| 35 | 
            +
                    weight_fraction = weight / total_weight.to_f
         | 
| 36 | 
            +
                    approx_shards   = total_shards * weight_fraction
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    Bucket.new template, approx_shards, Set.new
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def home!
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # list of [template, shards] in descending length of shards
         | 
| 45 | 
            +
                  templates_to_shards =
         | 
| 46 | 
            +
                    @shards.inject({}) do |h, shard|
         | 
| 47 | 
            +
                      (h[shard.template] ||= []) << shard; h
         | 
| 48 | 
            +
                    end.sort_by {|(_,ss)| ss.length * -1 }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  templates_to_shards.each do |(template, shards)|
         | 
| 51 | 
            +
                    descendants = memoized_concrete_descendants(template)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    most_similar_buckets = []
         | 
| 54 | 
            +
                    last_cost = nil
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    @result.each do |bucket|
         | 
| 57 | 
            +
                      cost      = (memoized_concrete_descendants(bucket.template) - descendants).length
         | 
| 58 | 
            +
                      last_cost = cost if last_cost.nil?
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      if cost == last_cost
         | 
| 61 | 
            +
                        most_similar_buckets << bucket
         | 
| 62 | 
            +
                      elsif cost < last_cost
         | 
| 63 | 
            +
                        last_cost = cost
         | 
| 64 | 
            +
                        most_similar_buckets = [bucket]
         | 
| 65 | 
            +
                      end
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    dest_bucket = most_similar_buckets.sort_by {|b| b.balance }.first
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    dest_bucket.merge shards
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def rebalance!
         | 
| 75 | 
            +
                  while bucket_disparity > 1
         | 
| 76 | 
            +
                    ordered = ordered_buckets
         | 
| 77 | 
            +
                    move_shard ordered.first.template, ordered.last.set.each {|e| break e }
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def transformations
         | 
| 82 | 
            +
                  return @transformations if @transformations
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  home!
         | 
| 85 | 
            +
                  rebalance!
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  @transformations = {}
         | 
| 88 | 
            +
                  @result.each do |bucket|
         | 
| 89 | 
            +
                    bucket.set.each do |shard|
         | 
| 90 | 
            +
                      trans = Transformation.new(shard.template, bucket.template, @copy_dest_wrapper)
         | 
| 91 | 
            +
                      forwardings_to_trees = (@transformations[trans] ||= {})
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      forwardings_to_trees.update(shard.forwarding => shard.tree)
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  @transformations.reject! {|t, _| t.noop? }
         | 
| 98 | 
            +
                  @transformations
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def ordered_buckets
         | 
| 102 | 
            +
                  @result.sort_by {|bucket| bucket.balance }
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def memoized_concrete_descendants(t)
         | 
| 106 | 
            +
                  @memoized_concrete_descendants ||= {}
         | 
| 107 | 
            +
                  @memoized_concrete_descendants[t] ||= t.concrete_descendants
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                def bucket_disparity
         | 
| 111 | 
            +
                  ordered = ordered_buckets
         | 
| 112 | 
            +
                  ordered.last.balance - ordered.first.balance
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def move_shard(template, shard)
         | 
| 116 | 
            +
                  @result.each do |bucket|
         | 
| 117 | 
            +
                    if bucket.template == template
         | 
| 118 | 
            +
                      bucket.add shard
         | 
| 119 | 
            +
                    else
         | 
| 120 | 
            +
                      bucket.delete shard
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
            end
         |