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