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