github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 []*machineTracker 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 controller -> 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), &machineTracker{ 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), &machineTracker{ 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 trackerMap := make(map[string]*machineTracker) 194 for _, m := range test.machines { 195 c.Assert(trackerMap[m.Id()], gc.IsNil) 196 trackerMap[m.Id()] = m 197 } 198 info := &peerGroupInfo{ 199 machineTrackers: trackerMap, 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 c.Assert(err, jc.ErrorIsNil) 210 211 sort.Sort(membersById(members)) 212 c.Assert(members, jc.DeepEquals, test.expectMembers) 213 if len(members) == 0 { 214 continue 215 } 216 for i, m := range test.machines { 217 vote, votePresent := voting[m] 218 c.Check(votePresent, jc.IsTrue) 219 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.Id())) 220 } 221 // Assure ourselves that the total number of desired votes is odd in 222 // all circumstances. 223 c.Assert(countVotes(members)%2, gc.Equals, 1) 224 225 // Make sure that when the members are set as 226 // required, that there's no further change 227 // if desiredPeerGroup is called again. 228 info.members = members 229 members, voting, err = desiredPeerGroup(info) 230 c.Assert(members, gc.IsNil) 231 for i, m := range test.machines { 232 vote, votePresent := voting[m] 233 c.Check(votePresent, jc.IsTrue) 234 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.Id())) 235 } 236 c.Assert(err, jc.ErrorIsNil) 237 } 238 }) 239 } 240 241 func countVotes(members []replicaset.Member) int { 242 tot := 0 243 for _, m := range members { 244 v := 1 245 if m.Votes != nil { 246 v = *m.Votes 247 } 248 tot += v 249 } 250 return tot 251 } 252 253 func newInt(i int) *int { 254 return &i 255 } 256 257 func newFloat64(f float64) *float64 { 258 return &f 259 } 260 261 // mkMachines returns a slice of *machineTracker based on 262 // the given description. 263 // Each machine in the description is white-space separated 264 // and holds the decimal machine id followed by an optional 265 // "v" if the machine wants a vote. 266 func mkMachines(description string, ipVersion TestIPVersion) []*machineTracker { 267 descrs := parseDescr(description) 268 ms := make([]*machineTracker, len(descrs)) 269 for i, d := range descrs { 270 ms[i] = &machineTracker{ 271 id: fmt.Sprint(d.id), 272 mongoHostPorts: []network.HostPort{{ 273 Address: network.Address{ 274 Value: fmt.Sprintf(ipVersion.machineFormatHost, d.id), 275 Type: ipVersion.addressType, 276 Scope: network.ScopeCloudLocal, 277 }, 278 Port: mongoPort, 279 }}, 280 wantsVote: strings.Contains(d.flags, "v"), 281 } 282 } 283 return ms 284 } 285 286 func memberTag(id string) map[string]string { 287 return map[string]string{jujuMachineKey: id} 288 } 289 290 // mkMembers returns a slice of *replicaset.Member 291 // based on the given description. 292 // Each member in the description is white-space separated 293 // and holds the decimal replica-set id optionally followed by the characters: 294 // - 'v' if the member is voting. 295 // - 'T' if the member has no associated machine tags. 296 // Unless the T flag is specified, the machine tag 297 // will be the replica-set id + 10. 298 func mkMembers(description string, ipVersion TestIPVersion) []replicaset.Member { 299 descrs := parseDescr(description) 300 ms := make([]replicaset.Member, len(descrs)) 301 for i, d := range descrs { 302 machineId := d.id + 10 303 m := replicaset.Member{ 304 Id: d.id, 305 Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort), 306 Tags: memberTag(fmt.Sprint(machineId)), 307 } 308 if !strings.Contains(d.flags, "v") { 309 m.Priority = newFloat64(0) 310 m.Votes = newInt(0) 311 } 312 if strings.Contains(d.flags, "T") { 313 m.Tags = nil 314 } 315 ms[i] = m 316 } 317 return ms 318 } 319 320 var stateFlags = map[rune]replicaset.MemberState{ 321 'p': replicaset.PrimaryState, 322 's': replicaset.SecondaryState, 323 } 324 325 // mkStatuses returns a slice of *replicaset.Member 326 // based on the given description. 327 // Each member in the description is white-space separated 328 // and holds the decimal replica-set id optionally followed by the 329 // characters: 330 // - 'H' if the instance is not healthy. 331 // - 'p' if the instance is in PrimaryState 332 // - 's' if the instance is in SecondaryState 333 func mkStatuses(description string, ipVersion TestIPVersion) []replicaset.MemberStatus { 334 descrs := parseDescr(description) 335 ss := make([]replicaset.MemberStatus, len(descrs)) 336 for i, d := range descrs { 337 machineId := d.id + 10 338 s := replicaset.MemberStatus{ 339 Id: d.id, 340 Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort), 341 Healthy: !strings.Contains(d.flags, "H"), 342 State: replicaset.UnknownState, 343 } 344 for _, r := range d.flags { 345 if state, ok := stateFlags[r]; ok { 346 s.State = state 347 } 348 } 349 ss[i] = s 350 } 351 return ss 352 } 353 354 type descr struct { 355 id int 356 flags string 357 } 358 359 func isNotDigit(r rune) bool { 360 return r < '0' || r > '9' 361 } 362 363 var parseDescrTests = []struct { 364 descr string 365 expect []descr 366 }{{ 367 descr: "", 368 expect: []descr{}, 369 }, { 370 descr: "0", 371 expect: []descr{{id: 0}}, 372 }, { 373 descr: "1foo", 374 expect: []descr{{id: 1, flags: "foo"}}, 375 }, { 376 descr: "10c 5 6443arble ", 377 expect: []descr{{ 378 id: 10, 379 flags: "c", 380 }, { 381 id: 5, 382 }, { 383 id: 6443, 384 flags: "arble", 385 }}, 386 }} 387 388 func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) { 389 for i, test := range parseDescrTests { 390 c.Logf("test %d. %q", i, test.descr) 391 c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect) 392 } 393 } 394 395 // parseDescr parses white-space separated fields of the form 396 // <id><flags> into descr structures. 397 func parseDescr(s string) []descr { 398 fields := strings.Fields(s) 399 descrs := make([]descr, len(fields)) 400 for i, field := range fields { 401 d := &descrs[i] 402 i := strings.IndexFunc(field, isNotDigit) 403 if i == -1 { 404 i = len(field) 405 } 406 id, err := strconv.Atoi(field[0:i]) 407 if err != nil { 408 panic(fmt.Errorf("bad field %q", field)) 409 } 410 d.id = id 411 d.flags = field[i:] 412 } 413 return descrs 414 } 415 416 func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) { 417 c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{}) 418 // Avoid mutating the obtained slice: because it's usually retrieved 419 // directly from the memberWatcher voyeur.Value, 420 // mutation can cause races. 421 obtainedMembers := deepCopy(obtained).([]replicaset.Member) 422 sort.Sort(membersById(obtainedMembers)) 423 sort.Sort(membersById(expected)) 424 c.Assert(obtainedMembers, jc.DeepEquals, expected) 425 } 426 427 type membersById []replicaset.Member 428 429 func (l membersById) Len() int { return len(l) } 430 func (l membersById) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 431 func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id } 432 433 // AssertAPIHostPorts asserts of two sets of network.HostPort slices are the same. 434 func AssertAPIHostPorts(c *gc.C, got, want [][]network.HostPort) { 435 c.Assert(got, gc.HasLen, len(want)) 436 sort.Sort(hostPortSliceByHostPort(got)) 437 sort.Sort(hostPortSliceByHostPort(want)) 438 c.Assert(got, gc.DeepEquals, want) 439 } 440 441 type hostPortSliceByHostPort [][]network.HostPort 442 443 func (h hostPortSliceByHostPort) Len() int { return len(h) } 444 func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 445 func (h hostPortSliceByHostPort) Less(i, j int) bool { 446 a, b := h[i], h[j] 447 if len(a) != len(b) { 448 return len(a) < len(b) 449 } 450 for i := range a { 451 av, bv := a[i], b[i] 452 if av.Value != bv.Value { 453 return av.Value < bv.Value 454 } 455 if av.Port != bv.Port { 456 return av.Port < bv.Port 457 } 458 } 459 return false 460 }