github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/worker/peergrouper/desired_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package peergrouper 5 6 import ( 7 "fmt" 8 "sort" 9 "strconv" 10 "strings" 11 12 jc "github.com/juju/testing/checkers" 13 gc "launchpad.net/gocheck" 14 15 "github.com/juju/juju/instance" 16 "github.com/juju/juju/replicaset" 17 "github.com/juju/juju/testing" 18 ) 19 20 type desiredPeerGroupSuite struct { 21 testing.BaseSuite 22 } 23 24 var _ = gc.Suite(&desiredPeerGroupSuite{}) 25 26 const ( 27 mongoPort = 1234 28 apiPort = 5678 29 ) 30 31 var desiredPeerGroupTests = []struct { 32 about string 33 machines []*machine 34 statuses []replicaset.MemberStatus 35 members []replicaset.Member 36 37 expectMembers []replicaset.Member 38 expectVoting []bool 39 expectErr string 40 }{{ 41 // Note that this should never happen - mongo 42 // should always be bootstrapped with at least a single 43 // member in its member-set. 44 about: "no members - error", 45 expectErr: "current member set is empty", 46 }, { 47 about: "one machine, two more proposed members", 48 machines: mkMachines("10v 11v 12v"), 49 statuses: mkStatuses("0p"), 50 members: mkMembers("0v"), 51 52 expectMembers: mkMembers("0v 1 2"), 53 expectVoting: []bool{true, false, false}, 54 }, { 55 about: "single machine, no change", 56 machines: mkMachines("11v"), 57 members: mkMembers("1v"), 58 statuses: mkStatuses("1p"), 59 expectVoting: []bool{true}, 60 expectMembers: nil, 61 }, { 62 about: "extra member with nil Vote", 63 machines: mkMachines("11v"), 64 members: mkMembers("1v 2vT"), 65 statuses: mkStatuses("1p 2s"), 66 expectVoting: []bool{true}, 67 expectErr: "voting non-machine member.* found in peer group", 68 }, { 69 about: "extra member with >1 votes", 70 machines: mkMachines("11v"), 71 members: append(mkMembers("1v"), replicaset.Member{ 72 Id: 2, 73 Votes: newInt(2), 74 Address: "0.1.2.12:1234", 75 }), 76 statuses: mkStatuses("1p 2s"), 77 expectVoting: []bool{true}, 78 expectErr: "voting non-machine member.* found in peer group", 79 }, { 80 about: "new machine with no associated member", 81 machines: mkMachines("11v 12v"), 82 members: mkMembers("1v"), 83 statuses: mkStatuses("1p"), 84 expectVoting: []bool{true, false}, 85 expectMembers: mkMembers("1v 2"), 86 }, { 87 about: "one machine has become ready to vote (-> no change)", 88 machines: mkMachines("11v 12v"), 89 members: mkMembers("1v 2"), 90 statuses: mkStatuses("1p 2s"), 91 expectVoting: []bool{true, false}, 92 expectMembers: nil, 93 }, { 94 about: "two machines have become ready to vote (-> added)", 95 machines: mkMachines("11v 12v 13v"), 96 members: mkMembers("1v 2 3"), 97 statuses: mkStatuses("1p 2s 3s"), 98 expectVoting: []bool{true, true, true}, 99 expectMembers: mkMembers("1v 2v 3v"), 100 }, { 101 about: "two machines have become ready to vote but one is not healthy (-> no change)", 102 machines: mkMachines("11v 12v 13v"), 103 members: mkMembers("1v 2 3"), 104 statuses: mkStatuses("1p 2s 3sH"), 105 expectVoting: []bool{true, false, false}, 106 expectMembers: nil, 107 }, { 108 about: "three machines have become ready to vote (-> 2 added)", 109 machines: mkMachines("11v 12v 13v 14v"), 110 members: mkMembers("1v 2 3 4"), 111 statuses: mkStatuses("1p 2s 3s 4s"), 112 expectVoting: []bool{true, true, true, false}, 113 expectMembers: mkMembers("1v 2v 3v 4"), 114 }, { 115 about: "one machine ready to lose vote with no others -> no change", 116 machines: mkMachines("11"), 117 members: mkMembers("1v"), 118 statuses: mkStatuses("1p"), 119 expectVoting: []bool{true}, 120 expectMembers: nil, 121 }, { 122 about: "two machines ready to lose vote -> votes removed", 123 machines: mkMachines("11 12v 13"), 124 members: mkMembers("1v 2v 3v"), 125 statuses: mkStatuses("1p 2p 3p"), 126 expectVoting: []bool{false, true, false}, 127 expectMembers: mkMembers("1 2v 3"), 128 }, { 129 about: "machines removed as state server -> removed from members", 130 machines: mkMachines("11v"), 131 members: mkMembers("1v 2 3"), 132 statuses: mkStatuses("1p 2s 3s"), 133 expectVoting: []bool{true}, 134 expectMembers: mkMembers("1v"), 135 }, { 136 about: "a candidate can take the vote of a non-candidate when they're ready", 137 machines: mkMachines("11v 12v 13 14v"), 138 members: mkMembers("1v 2v 3v 4"), 139 statuses: mkStatuses("1p 2s 3s 4s"), 140 expectVoting: []bool{true, true, false, true}, 141 expectMembers: mkMembers("1v 2v 3 4v"), 142 }, { 143 about: "several candidates can take non-candidates' votes", 144 machines: mkMachines("11v 12v 13 14 15 16v 17v 18v"), 145 members: mkMembers("1v 2v 3v 4v 5v 6 7 8"), 146 statuses: mkStatuses("1p 2s 3s 4s 5s 6s 7s 8s"), 147 expectVoting: []bool{true, true, false, false, false, true, true, true}, 148 expectMembers: mkMembers("1v 2v 3 4 5 6v 7v 8v"), 149 }, { 150 about: "a changed machine address should propagate to the members", 151 machines: append(mkMachines("11v 12v"), &machine{ 152 id: "13", 153 wantsVote: true, 154 mongoHostPorts: []instance.HostPort{{ 155 Address: instance.Address{ 156 Value: "0.1.99.13", 157 Type: instance.Ipv4Address, 158 NetworkScope: instance.NetworkCloudLocal, 159 }, 160 Port: 1234, 161 }}, 162 }), 163 statuses: mkStatuses("1s 2p 3p"), 164 members: mkMembers("1v 2v 3v"), 165 expectVoting: []bool{true, true, true}, 166 expectMembers: append(mkMembers("1v 2v"), replicaset.Member{ 167 Id: 3, 168 Address: "0.1.99.13:1234", 169 Tags: memberTag("13"), 170 }), 171 }, { 172 about: "a machine's address is ignored if it changes to empty", 173 machines: append(mkMachines("11v 12v"), &machine{ 174 id: "13", 175 wantsVote: true, 176 mongoHostPorts: nil, 177 }), 178 statuses: mkStatuses("1s 2p 3p"), 179 members: mkMembers("1v 2v 3v"), 180 expectVoting: []bool{true, true, true}, 181 expectMembers: nil, 182 }} 183 184 func (*desiredPeerGroupSuite) TestDesiredPeerGroup(c *gc.C) { 185 for i, test := range desiredPeerGroupTests { 186 c.Logf("\ntest %d: %s", i, test.about) 187 machineMap := make(map[string]*machine) 188 for _, m := range test.machines { 189 c.Assert(machineMap[m.id], gc.IsNil) 190 machineMap[m.id] = m 191 } 192 info := &peerGroupInfo{ 193 machines: machineMap, 194 statuses: test.statuses, 195 members: test.members, 196 } 197 members, voting, err := desiredPeerGroup(info) 198 if test.expectErr != "" { 199 c.Assert(err, gc.ErrorMatches, test.expectErr) 200 c.Assert(members, gc.IsNil) 201 continue 202 } 203 sort.Sort(membersById(members)) 204 c.Assert(members, jc.DeepEquals, test.expectMembers) 205 if len(members) == 0 { 206 continue 207 } 208 for i, m := range test.machines { 209 vote, votePresent := voting[m] 210 c.Check(votePresent, jc.IsTrue) 211 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.id)) 212 } 213 // Assure ourselves that the total number of desired votes is odd in 214 // all circumstances. 215 c.Assert(countVotes(members)%2, gc.Equals, 1) 216 217 // Make sure that when the members are set as 218 // required, that there's no further change 219 // if desiredPeerGroup is called again. 220 info.members = members 221 members, voting, err = desiredPeerGroup(info) 222 c.Assert(members, gc.IsNil) 223 for i, m := range test.machines { 224 vote, votePresent := voting[m] 225 c.Check(votePresent, jc.IsTrue) 226 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.id)) 227 } 228 c.Assert(err, gc.IsNil) 229 } 230 } 231 232 func countVotes(members []replicaset.Member) int { 233 tot := 0 234 for _, m := range members { 235 v := 1 236 if m.Votes != nil { 237 v = *m.Votes 238 } 239 tot += v 240 } 241 return tot 242 } 243 244 func newInt(i int) *int { 245 return &i 246 } 247 248 func newFloat64(f float64) *float64 { 249 return &f 250 } 251 252 // mkMachines returns a slice of *machine based on 253 // the given description. 254 // Each machine in the description is white-space separated 255 // and holds the decimal machine id followed by an optional 256 // "v" if the machine wants a vote. 257 func mkMachines(description string) []*machine { 258 descrs := parseDescr(description) 259 ms := make([]*machine, len(descrs)) 260 for i, d := range descrs { 261 ms[i] = &machine{ 262 id: fmt.Sprint(d.id), 263 mongoHostPorts: []instance.HostPort{{ 264 Address: instance.Address{ 265 Value: fmt.Sprintf("0.1.2.%d", d.id), 266 Type: instance.Ipv4Address, 267 NetworkScope: instance.NetworkCloudLocal, 268 }, 269 Port: mongoPort, 270 }}, 271 wantsVote: strings.Contains(d.flags, "v"), 272 } 273 } 274 return ms 275 } 276 277 func memberTag(id string) map[string]string { 278 return map[string]string{jujuMachineTag: id} 279 } 280 281 // mkMembers returns a slice of *replicaset.Member 282 // based on the given description. 283 // Each member in the description is white-space separated 284 // and holds the decimal replica-set id optionally followed by the characters: 285 // - 'v' if the member is voting. 286 // - 'T' if the member has no associated machine tags. 287 // Unless the T flag is specified, the machine tag 288 // will be the replica-set id + 10. 289 func mkMembers(description string) []replicaset.Member { 290 descrs := parseDescr(description) 291 ms := make([]replicaset.Member, len(descrs)) 292 for i, d := range descrs { 293 machineId := d.id + 10 294 m := replicaset.Member{ 295 Id: d.id, 296 Address: fmt.Sprintf("0.1.2.%d:%d", machineId, mongoPort), 297 Tags: memberTag(fmt.Sprint(machineId)), 298 } 299 if !strings.Contains(d.flags, "v") { 300 m.Priority = newFloat64(0) 301 m.Votes = newInt(0) 302 } 303 if strings.Contains(d.flags, "T") { 304 m.Tags = nil 305 } 306 ms[i] = m 307 } 308 return ms 309 } 310 311 var stateFlags = map[rune]replicaset.MemberState{ 312 'p': replicaset.PrimaryState, 313 's': replicaset.SecondaryState, 314 } 315 316 // mkStatuses returns a slice of *replicaset.Member 317 // based on the given description. 318 // Each member in the description is white-space separated 319 // and holds the decimal replica-set id optionally followed by the 320 // characters: 321 // - 'H' if the instance is not healthy. 322 // - 'p' if the instance is in PrimaryState 323 // - 's' if the instance is in SecondaryState 324 func mkStatuses(description string) []replicaset.MemberStatus { 325 descrs := parseDescr(description) 326 ss := make([]replicaset.MemberStatus, len(descrs)) 327 for i, d := range descrs { 328 machineId := d.id + 10 329 s := replicaset.MemberStatus{ 330 Id: d.id, 331 Address: fmt.Sprintf("0.1.2.%d:%d", machineId, mongoPort), 332 Healthy: !strings.Contains(d.flags, "H"), 333 State: replicaset.UnknownState, 334 } 335 for _, r := range d.flags { 336 if state, ok := stateFlags[r]; ok { 337 s.State = state 338 } 339 } 340 ss[i] = s 341 } 342 return ss 343 } 344 345 type descr struct { 346 id int 347 flags string 348 } 349 350 func isNotDigit(r rune) bool { 351 return r < '0' || r > '9' 352 } 353 354 var parseDescrTests = []struct { 355 descr string 356 expect []descr 357 }{{ 358 descr: "", 359 expect: []descr{}, 360 }, { 361 descr: "0", 362 expect: []descr{{id: 0}}, 363 }, { 364 descr: "1foo", 365 expect: []descr{{id: 1, flags: "foo"}}, 366 }, { 367 descr: "10c 5 6443arble ", 368 expect: []descr{{ 369 id: 10, 370 flags: "c", 371 }, { 372 id: 5, 373 }, { 374 id: 6443, 375 flags: "arble", 376 }}, 377 }} 378 379 func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) { 380 for i, test := range parseDescrTests { 381 c.Logf("test %d. %q", i, test.descr) 382 c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect) 383 } 384 } 385 386 // parseDescr parses white-space separated fields of the form 387 // <id><flags> into descr structures. 388 func parseDescr(s string) []descr { 389 fields := strings.Fields(s) 390 descrs := make([]descr, len(fields)) 391 for i, field := range fields { 392 d := &descrs[i] 393 i := strings.IndexFunc(field, isNotDigit) 394 if i == -1 { 395 i = len(field) 396 } 397 id, err := strconv.Atoi(field[0:i]) 398 if err != nil { 399 panic(fmt.Errorf("bad field %q", field)) 400 } 401 d.id = id 402 d.flags = field[i:] 403 } 404 return descrs 405 } 406 407 func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) { 408 c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{}) 409 sort.Sort(membersById(obtained.([]replicaset.Member))) 410 sort.Sort(membersById(expected)) 411 c.Assert(obtained, jc.DeepEquals, expected) 412 } 413 414 type membersById []replicaset.Member 415 416 func (l membersById) Len() int { return len(l) } 417 func (l membersById) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 418 func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id } 419 420 // assertAPIHostPorts asserts of two sets of instance.HostPort slices are the same. 421 func assertAPIHostPorts(c *gc.C, got, want [][]instance.HostPort) { 422 c.Assert(got, gc.HasLen, len(want)) 423 sort.Sort(hostPortSliceByHostPort(got)) 424 sort.Sort(hostPortSliceByHostPort(want)) 425 c.Assert(got, gc.DeepEquals, want) 426 } 427 428 type hostPortSliceByHostPort [][]instance.HostPort 429 430 func (h hostPortSliceByHostPort) Len() int { return len(h) } 431 func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 432 func (h hostPortSliceByHostPort) Less(i, j int) bool { 433 a, b := h[i], h[j] 434 if len(a) != len(b) { 435 return len(a) < len(b) 436 } 437 for i := range a { 438 av, bv := a[i], b[i] 439 if av.Value != bv.Value { 440 return av.Value < bv.Value 441 } 442 if av.Port != bv.Port { 443 return av.Port < bv.Port 444 } 445 } 446 return false 447 }