gizzmo 0.11.0 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +11 -16
- data/VERSION +1 -1
- data/bin/setup_shards +173 -0
- data/lib/gizzard.rb +4 -0
- data/lib/gizzard/commands.rb +286 -152
- data/lib/gizzard/migrator.rb +192 -0
- data/lib/gizzard/nameserver.rb +206 -0
- data/lib/gizzard/shard_template.rb +252 -0
- data/lib/gizzard/thrift.rb +187 -135
- data/lib/gizzard/transformation.rb +230 -0
- data/lib/gizzard/transformation_op.rb +181 -0
- data/lib/gizzard/transformation_scheduler.rb +220 -0
- data/lib/gizzmo.rb +87 -20
- data/test/gizzmo_spec.rb +499 -0
- data/test/nameserver_spec.rb +139 -0
- data/test/scheduler_spec.rb +59 -0
- data/test/shard_template_spec.rb +103 -0
- data/test/spec.opts +7 -0
- data/test/spec_helper.rb +139 -0
- data/test/test_server/.gitignore +13 -0
- data/test/test_server/project/build.properties +8 -0
- data/test/test_server/project/build/Project.scala +13 -0
- data/test/test_server/project/plugins/Plugins.scala +6 -0
- data/test/test_server/src/main/scala/Main.scala +18 -0
- data/test/test_server/src/main/scala/TestServer.scala +247 -0
- data/test/test_server/src/main/thrift/TestServer.thrift +12 -0
- data/test/transformation_spec.rb +181 -0
- metadata +32 -5
- data/gizzmo.gemspec +0 -75
@@ -0,0 +1,192 @@
|
|
1
|
+
module Gizzard
|
2
|
+
class MigratorConfig
|
3
|
+
attr_accessor :prefix, :table_id, :source_type, :destination_type, :forwarding_space, :forwarding_space_min, :manifest
|
4
|
+
|
5
|
+
def initialize(opts = {})
|
6
|
+
opts.each {|(k,v)| send("#{k}=", v) if respond_to? "{k}=" }
|
7
|
+
end
|
8
|
+
|
9
|
+
def shard_name(enum)
|
10
|
+
table_id_segment = (table_id && table_id < 0) ? "n#{table_id.abs}" : table_id
|
11
|
+
[prefix, table_id, "%04d" % enum].compact.join("_")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Migrator
|
16
|
+
BALANCE_TOLERANCE = 1
|
17
|
+
|
18
|
+
attr_reader :configured_templates, :existing_map, :existing_templates, :total_shards
|
19
|
+
|
20
|
+
# populated via derive_changes
|
21
|
+
attr_reader :new_templates, :unrecognized_templates, :similar_templates, :unchanged_templates
|
22
|
+
|
23
|
+
def initialize(existing_map, config_templates, default_total_shards, config)
|
24
|
+
@configured_templates = config_templates
|
25
|
+
@existing_map = existing_map
|
26
|
+
|
27
|
+
@existing_templates = existing_map.keys
|
28
|
+
@total_shards = @existing_map.values.map { |a| a.length }.inject { |a, b| a + b } || default_total_shards
|
29
|
+
@config = config
|
30
|
+
|
31
|
+
derive_changes
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare!(nameserver)
|
35
|
+
transformations.each {|t| t.prepare! nameserver, @config }
|
36
|
+
end
|
37
|
+
|
38
|
+
def copy!(nameserver)
|
39
|
+
transformations.each {|t| t.copy! nameserver, @config }
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_copies(nameserver)
|
43
|
+
transformations.each {|t| t.wait_for_copies nameserver, @config }
|
44
|
+
end
|
45
|
+
|
46
|
+
def cleanup!(nameserver)
|
47
|
+
transformations.each {|t| t.cleanup! nameserver, @config }
|
48
|
+
end
|
49
|
+
|
50
|
+
def transformations
|
51
|
+
return @transformations if @transformations
|
52
|
+
|
53
|
+
# no changes
|
54
|
+
return @transformations = [] if similar_templates.empty? and unrecognized_templates.empty? and new_templates.empty?
|
55
|
+
|
56
|
+
configured_map = configured_templates.inject({}) {|h, t| h.update t => [] }
|
57
|
+
|
58
|
+
@transformations = []
|
59
|
+
|
60
|
+
if existing_templates.empty?
|
61
|
+
# no forwardings exist, we must populate the forwarding index.
|
62
|
+
forwardings = generate_new_forwardings(total_shards)
|
63
|
+
|
64
|
+
# add the new shard ids to a member of the configured map. will
|
65
|
+
# be rebalanced later.
|
66
|
+
configured_map.values.first.concat forwardings.values
|
67
|
+
|
68
|
+
@transformations << ForwardingTransformation.new(@config.table_id, forwardings.inject({}) {|f, (b, e)| f.update b => @config.shard_name(e) })
|
69
|
+
end
|
70
|
+
|
71
|
+
# map the unchanged templates straight over
|
72
|
+
move_unchanged(existing_map, configured_map)
|
73
|
+
|
74
|
+
# map similar templates over to their new versions
|
75
|
+
move_similar(existing_map, configured_map)
|
76
|
+
|
77
|
+
# move shards from unrecognized templates to new templates (or
|
78
|
+
# existing ones)
|
79
|
+
move_unrecognized_to_new(existing_map, configured_map)
|
80
|
+
|
81
|
+
# rebalance
|
82
|
+
rebalance_shards(configured_map)
|
83
|
+
|
84
|
+
# transformation generation
|
85
|
+
@transformations = generate_transformations(existing_map, configured_map) + @transformations
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def generate_new_forwardings(shard_count)
|
91
|
+
forwardings = {}
|
92
|
+
step_size = @config.forwarding_space / shard_count
|
93
|
+
bases = (0...shard_count).map { |i| @config.forwarding_space_min + (i * step_size) }
|
94
|
+
|
95
|
+
bases.each_with_index do |base_id, i|
|
96
|
+
forwardings[base_id] = i
|
97
|
+
end
|
98
|
+
|
99
|
+
forwardings
|
100
|
+
end
|
101
|
+
|
102
|
+
def move_unchanged(existing, configured)
|
103
|
+
unchanged_templates.each {|u| configured[u] = existing[u].dup }
|
104
|
+
end
|
105
|
+
|
106
|
+
def move_similar(existing, configured)
|
107
|
+
similar_templates.each {|from, to| configured[to] = existing[from].dup }
|
108
|
+
end
|
109
|
+
|
110
|
+
def move_unrecognized_to_new(existing, configured)
|
111
|
+
# duplicate so we can mutate our copy
|
112
|
+
unrecognized = unrecognized_templates.dup
|
113
|
+
|
114
|
+
# for each new template, grab an unrecognized one's shards
|
115
|
+
# and pop it off
|
116
|
+
new_templates.each do |n|
|
117
|
+
if u = unrecognized.pop
|
118
|
+
configured[n] = existing[u].dup
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# if there are any unrecognized templates for which we haven't
|
123
|
+
# moved shards over, add their shards to the first template. they will get rebalanced later
|
124
|
+
leftover_shards = unrecognized.inject([]) {|a, u| a.concat existing[u] }
|
125
|
+
|
126
|
+
configured.values.last.concat leftover_shards unless leftover_shards.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
def rebalance_shards(configured)
|
130
|
+
until shards_balanced? configured
|
131
|
+
smallest(configured) << largest(configured).pop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def generate_transformations(existing, configured)
|
136
|
+
existing_shards = shards_to_templates(existing)
|
137
|
+
configured_shards = shards_to_templates(configured)
|
138
|
+
|
139
|
+
# find the list of shards which have moved, and emit a
|
140
|
+
# transformation for each one.
|
141
|
+
(configured_shards.to_a - existing_shards.to_a).inject({}) do |transformations, (shard, to)|
|
142
|
+
from = existing_shards[shard]
|
143
|
+
(transformations[[from, to]] ||= Transformation.new(from, to, [])).shards << shard
|
144
|
+
transformations
|
145
|
+
end.values
|
146
|
+
end
|
147
|
+
|
148
|
+
def shards_balanced?(template_map)
|
149
|
+
sorted_sizes = template_map.values.map {|s| s.length }.uniq.sort.reverse
|
150
|
+
sorted_sizes.first - sorted_sizes.last <= BALANCE_TOLERANCE
|
151
|
+
end
|
152
|
+
|
153
|
+
def smallest(template_map)
|
154
|
+
template_map.values.sort {|a,b| a.length <=> b.length }.first
|
155
|
+
end
|
156
|
+
|
157
|
+
def largest(template_map)
|
158
|
+
template_map.values.sort {|a,b| b.length <=> a.length }.first
|
159
|
+
end
|
160
|
+
|
161
|
+
def shards_to_templates(templates_to_shards)
|
162
|
+
templates_to_shards.inject({}) do |h, (template, shards)|
|
163
|
+
shards.each {|shard| h[shard] = template }; h
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def derive_changes
|
168
|
+
@unrecognized_templates, @new_templates, related_templates =
|
169
|
+
split_set(existing_templates, configured_templates) {|a, b| a.similar? b }
|
170
|
+
|
171
|
+
@similar_templates = related_templates.reject {|(a,b)| a == b }
|
172
|
+
@unchanged_templates = related_templates.keys - @similar_templates.keys
|
173
|
+
end
|
174
|
+
|
175
|
+
def split_set(a, b, &predicate)
|
176
|
+
in_a = a.dup
|
177
|
+
in_b = b.dup
|
178
|
+
overlap = {}
|
179
|
+
|
180
|
+
in_a.each_with_index do |a, a_i|
|
181
|
+
in_b.each_with_index do |b, b_i|
|
182
|
+
if predicate.call(a, b)
|
183
|
+
overlap[a] = b
|
184
|
+
in_a[a_i] = in_b[b_i] = nil
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
[in_a.compact, in_b.compact, overlap]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module Gizzard
|
2
|
+
Shard = Struct.new(:info, :children, :weight)
|
3
|
+
|
4
|
+
class Shard
|
5
|
+
class << self
|
6
|
+
def canonical_table_prefix(enum, table_id = nil, base_prefix = "shard")
|
7
|
+
enum_s = "%0.4i" % enum
|
8
|
+
table_id_s = table_id.nil? ? nil : table_id < 0 ? "n#{table_id.abs}" : table_id.to_s
|
9
|
+
[base_prefix, table_id_s, enum_s].compact.join('_')
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_enumeration(table_prefix)
|
13
|
+
if match = table_prefix.match(/\d{3,}/)
|
14
|
+
match[0].to_i
|
15
|
+
else
|
16
|
+
raise "Cannot derive enumeration!"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
VIRTUAL_SHARD_TYPES = [
|
22
|
+
"FailingOverShard",
|
23
|
+
"ReplicatingShard",
|
24
|
+
"ReadOnlyShard",
|
25
|
+
"WriteOnlyShard",
|
26
|
+
"BlockedShard",
|
27
|
+
]
|
28
|
+
|
29
|
+
REPLICATING_SHARD_TYPES = ["ReplicatingShard", "FailingOverShard"]
|
30
|
+
|
31
|
+
INVALID_COPY_TYPES = ["ReadOnlyShard", "WriteOnlyShard", "BlockedShard"]
|
32
|
+
|
33
|
+
SHARD_SUFFIXES = {
|
34
|
+
"FailingOverShard" => 'replicating',
|
35
|
+
"ReplicatingShard" => 'replicating',
|
36
|
+
"ReadOnlyShard" => 'read_only',
|
37
|
+
"WriteOnlyShard" => 'write_only',
|
38
|
+
"BlockedShard" => 'blocked'
|
39
|
+
}
|
40
|
+
|
41
|
+
def id; info.id end
|
42
|
+
def hostname; id.hostname end
|
43
|
+
def table_prefix; id.table_prefix end
|
44
|
+
def class_name; info.class_name end
|
45
|
+
def source_type; info.source_type end
|
46
|
+
def destination_type; info.destination_type end
|
47
|
+
def busy; info.busy end
|
48
|
+
|
49
|
+
def template
|
50
|
+
child_templates = children.map {|c| c.template }
|
51
|
+
|
52
|
+
ShardTemplate.new(info.class_name,
|
53
|
+
id.hostname,
|
54
|
+
weight,
|
55
|
+
info.source_type,
|
56
|
+
info.destination_type,
|
57
|
+
child_templates)
|
58
|
+
end
|
59
|
+
|
60
|
+
def enumeration
|
61
|
+
self.class.parse_enumeration(table_prefix)
|
62
|
+
end
|
63
|
+
|
64
|
+
def canonical_shard_id_map(base_prefix = "shard", table_id = nil, enum = nil)
|
65
|
+
enum ||= self.enumeration
|
66
|
+
base = Shard.canonical_table_prefix(enum, table_id, base_prefix)
|
67
|
+
suffix = SHARD_SUFFIXES[class_name.split('.').last]
|
68
|
+
canonical_name = [base, suffix].compact.join('_')
|
69
|
+
canonical_id = ShardId.new(self.hostname, canonical_name)
|
70
|
+
|
71
|
+
children.inject(canonical_id => self.id) do |m, c|
|
72
|
+
m.update c.canonical_shard_id_map(base_prefix, table_id, enum)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Nameserver
|
78
|
+
|
79
|
+
DEFAULT_PORT = 7917
|
80
|
+
DEFAULT_RETRIES = 20
|
81
|
+
PARALLELISM = 10
|
82
|
+
|
83
|
+
attr_reader :hosts, :logfile, :dryrun, :framed
|
84
|
+
alias dryrun? dryrun
|
85
|
+
|
86
|
+
def initialize(*hosts)
|
87
|
+
options = hosts.last.is_a?(Hash) ? hosts.pop : {}
|
88
|
+
@retries = options[:retries] || DEFAULT_RETRIES
|
89
|
+
@logfile = options[:log] || "/tmp/gizzmo.log"
|
90
|
+
@dryrun = options[:dry_run] || false
|
91
|
+
@framed = options[:framed] || false
|
92
|
+
@hosts = hosts.flatten
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_shards(ids)
|
96
|
+
ids.map {|id| with_retry { client.get_shard(id) } }
|
97
|
+
end
|
98
|
+
|
99
|
+
def reload_config
|
100
|
+
all_clients.each {|c| with_retry { c.reload_config } }
|
101
|
+
end
|
102
|
+
|
103
|
+
def copy_shard(from_shard_id, to_shard_id)
|
104
|
+
c = random_client
|
105
|
+
with_retry { c.copy_shard(from_shard_id, to_shard_id) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def respond_to?(method)
|
109
|
+
client.respond_to? method or super
|
110
|
+
end
|
111
|
+
|
112
|
+
def method_missing(method, *args, &block)
|
113
|
+
if client.respond_to?(method)
|
114
|
+
with_retry { client.send(method, *args, &block) }
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def manifest(*table_ids)
|
121
|
+
Manifest.new(self, table_ids)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def client
|
127
|
+
@client ||= create_client(hosts.first)
|
128
|
+
end
|
129
|
+
|
130
|
+
def all_clients
|
131
|
+
@all_clients ||= hosts.map {|host| create_client(host) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def random_client
|
135
|
+
all_clients[rand(all_clients.length)]
|
136
|
+
end
|
137
|
+
|
138
|
+
def create_client(host)
|
139
|
+
host, port = host.split(":")
|
140
|
+
port ||= DEFAULT_PORT
|
141
|
+
Manager.new(host, port.to_i, logfile, framed, dryrun)
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def with_retry
|
147
|
+
times ||= @retries
|
148
|
+
yield
|
149
|
+
rescue ThriftClient::Simple::ThriftException, NoMethodError, Gizzard::GizzardException => e
|
150
|
+
raise if e.is_a? Gizzard::GizzardException and e.message !~ /Communications link failure/
|
151
|
+
|
152
|
+
times -= 1
|
153
|
+
(times < 0) ? raise : (sleep 2; retry)
|
154
|
+
end
|
155
|
+
|
156
|
+
class Manifest
|
157
|
+
attr_reader :forwardings, :links, :shard_infos, :trees, :templates
|
158
|
+
|
159
|
+
|
160
|
+
def initialize(nameserver, table_ids)
|
161
|
+
states = table_ids.map {|id| nameserver.dump_nameserver(id) }
|
162
|
+
|
163
|
+
@forwardings = states.map {|s| s.forwardings }.flatten
|
164
|
+
|
165
|
+
@links = states.map {|s| s.links }.flatten.inject({}) do |h, link|
|
166
|
+
(h[link.up_id] ||= []) << [link.down_id, link.weight]; h
|
167
|
+
end
|
168
|
+
|
169
|
+
@shard_infos = states.map {|s| s.shards }.flatten.inject({}) do |h, shard|
|
170
|
+
h.update shard.id => shard
|
171
|
+
end
|
172
|
+
|
173
|
+
@trees = @forwardings.inject({}) do |h, forwarding|
|
174
|
+
h.update forwarding => build_tree(forwarding.shard_id)
|
175
|
+
end
|
176
|
+
|
177
|
+
@templates = @trees.inject({}) do |h, (forwarding, shard)|
|
178
|
+
(h[shard.template] ||= []) << forwarding; h
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def get_filtered_forwardings(nameserver, filter)
|
185
|
+
return filter[:forwardings] if filter[:forwardings]
|
186
|
+
|
187
|
+
forwardings = nameserver.get_forwardings
|
188
|
+
|
189
|
+
if table_id = filter[:table_id]
|
190
|
+
forwardings.reject! {|f| f.table_id != table_id }
|
191
|
+
end
|
192
|
+
|
193
|
+
forwardings
|
194
|
+
end
|
195
|
+
|
196
|
+
def build_tree(shard_id, link_weight=ShardTemplate::DEFAULT_WEIGHT)
|
197
|
+
children = (links[shard_id] || []).map do |(child_id, child_weight)|
|
198
|
+
build_tree(child_id, child_weight)
|
199
|
+
end
|
200
|
+
|
201
|
+
info = shard_infos[shard_id] or raise "shard info not found for: #{shard_id}"
|
202
|
+
Shard.new(info, children, link_weight)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Gizzard
|
2
|
+
class ShardTemplate
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
ABSTRACT_HOST = "localhost"
|
6
|
+
DEFAULT_WEIGHT = 1
|
7
|
+
|
8
|
+
attr_reader :type, :weight, :source_type, :dest_type
|
9
|
+
|
10
|
+
def initialize(type, host, weight, source_type, dest_type, children)
|
11
|
+
@type, @host, @weight, @source_type, @dest_type, @children =
|
12
|
+
type, host, weight, source_type || '', dest_type || '', children
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.concrete?(type)
|
16
|
+
!Shard::VIRTUAL_SHARD_TYPES.include? type.split('.').last
|
17
|
+
end
|
18
|
+
|
19
|
+
def concrete?
|
20
|
+
self.class.concrete? type
|
21
|
+
end
|
22
|
+
|
23
|
+
def replicating?
|
24
|
+
Shard::REPLICATING_SHARD_TYPES.include? type.split('.').last
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_copy_source?
|
28
|
+
!Shard::INVALID_COPY_TYPES.include? type.split('.').last
|
29
|
+
end
|
30
|
+
|
31
|
+
def identifier
|
32
|
+
concrete? ? "#{type}/#{host}" : type.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_name_suffix
|
36
|
+
Shard::SHARD_SUFFIXES[type.split('.').last]
|
37
|
+
end
|
38
|
+
|
39
|
+
def host
|
40
|
+
if concrete?
|
41
|
+
@host
|
42
|
+
elsif replicating?
|
43
|
+
ABSTRACT_HOST
|
44
|
+
else
|
45
|
+
children.first.host
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def children
|
50
|
+
@children.sort { |a, b| b <=> a }
|
51
|
+
end
|
52
|
+
|
53
|
+
def descendants
|
54
|
+
[self].concat children.map {|c| c.descendants }.flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
alias flatten descendants
|
58
|
+
|
59
|
+
def concrete_descendants
|
60
|
+
descendants.select {|t| t.concrete? }
|
61
|
+
end
|
62
|
+
|
63
|
+
def copy_sources
|
64
|
+
return [] unless self.valid_copy_source?
|
65
|
+
self.concrete? ? [self] : children.inject([]) {|a, c| a.concat c.copy_sources }
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
to_config
|
70
|
+
end
|
71
|
+
alias to_s inspect
|
72
|
+
|
73
|
+
# Concretization
|
74
|
+
|
75
|
+
def to_shard_id(table_prefix, translations = {})
|
76
|
+
table_prefix = [table_prefix, table_name_suffix].compact.join('_')
|
77
|
+
shard_id = ShardId.new(host, table_prefix)
|
78
|
+
translations[shard_id] || shard_id
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_shard_info(table_prefix, translations = {})
|
82
|
+
ShardInfo.new(to_shard_id(table_prefix, translations), type, source_type, dest_type, 0)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_shard(table_prefix, translations = {})
|
86
|
+
Shard.new(to_shard_info(table_prefix, translations), children.map {|c|
|
87
|
+
c.to_shard(table_prefix, translations)
|
88
|
+
}, weight)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Similarity/Equality
|
92
|
+
|
93
|
+
def <=>(other)
|
94
|
+
raise ArgumentError, "other is not a ShardTemplate" unless other.is_a? ShardTemplate
|
95
|
+
|
96
|
+
to_a = lambda {|t| [t.host, t.type, t.source_type.to_s, t.dest_type.to_s, t.weight] }
|
97
|
+
|
98
|
+
if (cmp = to_a.call(self) <=> to_a.call(other)) == 0
|
99
|
+
children <=> other.children
|
100
|
+
else
|
101
|
+
cmp
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def eql?(other)
|
106
|
+
return false unless other.is_a? ShardTemplate
|
107
|
+
(self <=> other) == 0
|
108
|
+
end
|
109
|
+
|
110
|
+
def shard_eql?(other)
|
111
|
+
raise ArgumentError, "other is not a ShardTemplate" unless other.is_a? ShardTemplate
|
112
|
+
|
113
|
+
to_a = lambda {|t| [t.host, t.type, t.source_type.to_s, t.dest_type.to_s] }
|
114
|
+
to_a.call(self) == to_a.call(other)
|
115
|
+
end
|
116
|
+
|
117
|
+
def link_eql?(other)
|
118
|
+
raise ArgumentError, "other is not a ShardTemplate" unless other.is_a? ShardTemplate
|
119
|
+
|
120
|
+
to_a = lambda {|t| [t.host, t.type, t.source_type.to_s, t.dest_type.to_s, t.weight] }
|
121
|
+
to_a.call(self) == to_a.call(other)
|
122
|
+
end
|
123
|
+
|
124
|
+
def shared_host?(other)
|
125
|
+
raise ArgumentError, "other is not a ShardTemplate" unless other.is_a? ShardTemplate
|
126
|
+
|
127
|
+
(self.concrete_descendants & other.concrete_descendants).length > 0
|
128
|
+
end
|
129
|
+
|
130
|
+
def hash
|
131
|
+
weight.hash + host.hash + type.hash + children.hash
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
# Config
|
136
|
+
|
137
|
+
def config_definition
|
138
|
+
args = identifier.split("/")
|
139
|
+
args << weight
|
140
|
+
args.concat [@source_type,@dest_type] unless [@source_type, @dest_type].reject {|s| s.empty? }.empty?
|
141
|
+
|
142
|
+
type = args.shift
|
143
|
+
args_s = args.empty? ? "" : "(#{args.join(",")})"
|
144
|
+
|
145
|
+
type + args_s
|
146
|
+
end
|
147
|
+
|
148
|
+
private :config_definition
|
149
|
+
|
150
|
+
def to_config_struct
|
151
|
+
if children.empty?
|
152
|
+
config_definition
|
153
|
+
else
|
154
|
+
child_defs = children.map {|c| c.to_config_struct }
|
155
|
+
{ config_definition => (child_defs.length == 1 ? child_defs.first : child_defs) }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def to_config
|
160
|
+
if children.empty?
|
161
|
+
config_definition
|
162
|
+
else
|
163
|
+
child_defs = children.map {|c| c.to_config }
|
164
|
+
child_defs_s = child_defs.length == 1 ? child_defs.first : "(#{child_defs.join(", ")})"
|
165
|
+
"#{config_definition} -> #{child_defs_s}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Class Methods
|
171
|
+
|
172
|
+
class << self
|
173
|
+
def parse(string)
|
174
|
+
definition_s, children_s = string.split(/\s*->\s*/, 2)
|
175
|
+
|
176
|
+
children =
|
177
|
+
if children_s.nil?
|
178
|
+
[]
|
179
|
+
else
|
180
|
+
list = parse_arg_list(children_s).map {|c| parse c }
|
181
|
+
raise ArgumentError, "invalid shard config. -> given, no children found" if list.empty?
|
182
|
+
list
|
183
|
+
end
|
184
|
+
|
185
|
+
template_args = parse_definition(definition_s) << children
|
186
|
+
ShardTemplate.new(*template_args)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def parse_definition(definition_s)
|
192
|
+
type, arg_list = definition_s.split("(", 2)
|
193
|
+
|
194
|
+
host, weight, source_type, dest_type =
|
195
|
+
if arg_list.nil?
|
196
|
+
nil
|
197
|
+
else
|
198
|
+
args = parse_arg_list("(" + arg_list)
|
199
|
+
args.unshift nil unless concrete? type
|
200
|
+
args
|
201
|
+
end
|
202
|
+
|
203
|
+
validate_host_arg(host, definition_s) if concrete? type
|
204
|
+
validate_weight_arg(weight, definition_s)
|
205
|
+
|
206
|
+
weight = (weight || DEFAULT_WEIGHT).to_i
|
207
|
+
source_type ||= ""
|
208
|
+
dest_type ||= ""
|
209
|
+
|
210
|
+
[type, host, weight, source_type, dest_type]
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse_arg_list(string)
|
214
|
+
string = string.strip
|
215
|
+
if m = string.match(/\A\((.*)\)\Z/)
|
216
|
+
string = m[1]
|
217
|
+
end
|
218
|
+
|
219
|
+
depth = 0
|
220
|
+
results = [[]]
|
221
|
+
|
222
|
+
string.each_char do |c|
|
223
|
+
case c
|
224
|
+
when ","
|
225
|
+
if depth == 0
|
226
|
+
results << []
|
227
|
+
next
|
228
|
+
end
|
229
|
+
when "(" then depth += 1
|
230
|
+
when ")" then depth -= 1
|
231
|
+
end
|
232
|
+
|
233
|
+
results.last << c
|
234
|
+
end
|
235
|
+
|
236
|
+
results.map {|r| r.join.strip }
|
237
|
+
end
|
238
|
+
|
239
|
+
def validate_weight_arg(arg, definition)
|
240
|
+
if arg && YAML.load(arg.to_s).is_a?(String)
|
241
|
+
raise ArgumentError, "Invalid weight #{arg} for shard in: #{definition}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def validate_host_arg(arg, definition)
|
246
|
+
if arg.nil? || YAML.load(arg.to_s).is_a?(Numeric)
|
247
|
+
raise ArgumentError, "Invalid host #{arg} for shard in: #{definition}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|