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 CHANGED
@@ -1 +1 @@
1
- 0.11.2
1
+ 0.11.3
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{gizzmo}
8
- s.version = "0.11.2"
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-06}
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.6}
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::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
99
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
96
100
  else
97
101
  end
98
102
  else
@@ -7,3 +7,4 @@ require "gizzard/shard_template"
7
7
  require "gizzard/migrator"
8
8
  require "gizzard/transformation"
9
9
  require "gizzard/digest"
10
+ require "gizzard/rebalancer"
@@ -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
- templates = manager.manifest(*global_options.tables).templates.inject({}) do |h, (t, fs)|
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.base_id, t] }; h }.
709
+ inject([]) { |h, (t, fs)| fs.each { |f| h << [f.inspect, t] }; h }.
803
710
  sort.
804
- each { |a| puts "%25d\t%s" % a }
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
@@ -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.each {|c| with_retry { c.reload_config } }
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
- attr_reader :forwardings, :links, :shard_infos, :trees, :templates
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 = table_ids.map {|id| nameserver.dump_nameserver(id) }
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