Merge lp:~rogpeppe/juju-core/479-desired-peer-group into lp:~go-bot/juju-core/trunk
- 479-desired-peer-group
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Roger Peppe |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2328 |
Proposed branch: | lp:~rogpeppe/juju-core/479-desired-peer-group |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
658 lines (+648/-0) 2 files modified
worker/peergrouper/desired.go (+290/-0) worker/peergrouper/desired_test.go (+358/-0) |
To merge this branch: | bzr merge lp:~rogpeppe/juju-core/479-desired-peer-group |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+201245@code.launchpad.net |
Commit message
worker/peergrouper: new package
This will be a worker that maintains the replica set
with respect to the state.
The function in this CL implements the core functionality - it's
a stateless function that looks at a representation of the
current state of affairs and decides what the replicaset
member list should look like.
It may well change when faced with reality (although I've been
trying to sanity check with experimental code),
but I think it should be reasonable to check in now
as a staging post.
There are current some extraneous changes in this
branch (everything outside worker/peergrouper)
which are made redundant by other MPs. Please
ignore for the purposes of this review; I'll remove
before submitting.
Description of the change
worker/peergrouper: new package
This will be a worker that maintains the replica set
with respect to the state.
The function in this CL implements the core functionality - it's
a stateless function that looks at a representation of the
current state of affairs and decides what the replicaset
member list should look like.
It may well change when faced with reality (although I've been
trying to sanity check with experimental code),
but I think it should be reasonable to check in now
as a staging post.
There are current some extraneous changes in this
branch (everything outside worker/peergrouper)
which are made redundant by other MPs. Please
ignore for the purposes of this review; I'll remove
before submitting.
Roger Peppe (rogpeppe) wrote : | # |
John A Meinel (jameinel) wrote : | # |
I didn't finish the review before I'm on the next call, but some
thoughts I didn't want to lose.
https:/
File worker/
https:/
worker/
*state.State, ms []*state.Machine) (*peerGroupInfo, error) {
this function is commented out. Generally either we should delete it, or
have it. I'm guessing this is just part of the process as you're getting
there.
https:/
worker/
Is there really nothing like min that we can just use?
https:/
worker/
*peerGroupInfo) ([]replicaset.
Your description makes it sound like this function mixes a query (what
peer group do you want?) with an action (set the voting status of all
the machines).
It would be best to split informational queries from side effects if
possible.
Roger Peppe (rogpeppe) wrote : | # |
Please take a look.
https:/
File worker/
https:/
worker/
*state.State, ms []*state.Machine) (*peerGroupInfo, error) {
On 2014/01/13 11:37:12, jameinel wrote:
> this function is commented out. Generally either we should delete it,
or have
> it. I'm guessing this is just part of the process as you're getting
there.
It is. I suppose I should probably delete it for this CL and bring it
back in the next one.
https:/
worker/
On 2014/01/13 11:37:12, jameinel wrote:
> Is there really nothing like min that we can just use?
nope. it's not considered worth adding to the standard library.
(well, it wouldn't be one - there would be 12 of them).
https:/
worker/
*peerGroupInfo) ([]replicaset.
On 2014/01/13 11:37:12, jameinel wrote:
> Your description makes it sound like this function mixes a query (what
peer
> group do you want?) with an action (set the voting status of all the
machines).
> It would be best to split informational queries from side effects if
possible.
I've changed it so that it returns a map of values rather than setting
the machine fields in place. We want the caller to set the votes of the
machines after calling this function, and it's much easier to work out
that information here, rather than have the caller try to work out the
info from the replicaset members, which may not include all the machines
passed in.
Roger Peppe (rogpeppe) wrote : | # |
Please take a look.
Nate Finch (natefinch) wrote : | # |
LGTM overall. I'd like to see comments on the functions even if they're
not exported... makes it a little clearer what each one is doing.
https:/
File worker/
https:/
worker/
*peerGroupInfo) ([]replicaset.
This function is pretty long, would be a little easier to read if it
were broken up more, I think.
https:/
worker/
break here?
Roger Peppe (rogpeppe) wrote : | # |
Please take a look.
https:/
File worker/
https:/
worker/
*peerGroupInfo) ([]replicaset.
On 2014/01/13 18:47:15, nate.finch wrote:
> This function is pretty long, would be a little easier to read if it
were broken
> up more, I think.
Yeah, I'd toyed with that idea but not mustered up the necessary energy.
Done; definitely a good idea, thanks.
https:/
worker/
On 2014/01/13 18:47:15, nate.finch wrote:
> break here?
Done.
Nate Finch (natefinch) wrote : | # |
LGTM The refactor makes a big difference.
Roger Peppe (rogpeppe) wrote : | # |
Please take a look.
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~rogpeppe/juju-core/479-desired-peer-group into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
-------
FAIL: machine_
[LOG] 30.61946 DEBUG juju.environs.
[LOG] 30.73054 DEBUG juju.environs.tools reading v1.* tools
[LOG] 30.73059 INFO juju environs/testing: uploading FAKE tools 1.17.1-
[LOG] 30.73153 DEBUG juju.environs.tools no architecture specified when finding tools, looking for any
[LOG] 30.73155 DEBUG juju.environs.tools no series specified when finding tools, looking for any
[LOG] 30.73162 DEBUG juju.environs.
[LOG] 30.73165 DEBUG juju.environs.
[LOG] 30.73169 DEBUG juju.environs.
[LOG] 30.73171 DEBUG juju.environs.
[LOG] 30.73198 INFO juju.environs.tools Writing tools/streams/
[LOG] 30.73202 INFO juju.environs.tools Writing tools/streams/
[LOG] 30.73218 INFO juju.environs.
[LOG] 30.73221 DEBUG juju.environs.
[LOG] 30.73223 INFO juju.environs.tools reading tools with major.minor version 1.17
[LOG] 30.73224 INFO juju.environs.tools filtering tools by version: 1.17.1
[LOG] 30.73225 INFO juju.environs.tools filtering tools by series: precise
[LOG] 30.73227 DEBUG juju.environs.tools no architecture specified when finding tools, looking for any
[LOG] 30.73231 DEBUG juju.environs.
[LOG] 30.73233 DEBUG juju.environs.
Preview Diff
1 | === added directory 'worker/peergrouper' |
2 | === added file 'worker/peergrouper/desired.go' |
3 | --- worker/peergrouper/desired.go 1970-01-01 00:00:00 +0000 |
4 | +++ worker/peergrouper/desired.go 2014-02-14 12:14:58 +0000 |
5 | @@ -0,0 +1,290 @@ |
6 | +package peergrouper |
7 | + |
8 | +import ( |
9 | + "fmt" |
10 | + "sort" |
11 | + |
12 | + "launchpad.net/juju-core/replicaset" |
13 | + "launchpad.net/loggo" |
14 | +) |
15 | + |
16 | +var logger = loggo.GetLogger("juju.worker.peergrouper") |
17 | + |
18 | +// peerGroupInfo holds information that may contribute to |
19 | +// a peer group. |
20 | +type peerGroupInfo struct { |
21 | + machines []*machine // possibly map[id] *machine |
22 | + statuses []replicaset.MemberStatus |
23 | + members []replicaset.Member |
24 | +} |
25 | + |
26 | +// machine represents a machine in State. |
27 | +type machine struct { |
28 | + id string |
29 | + wantsVote bool |
30 | + hostPort string |
31 | +} |
32 | + |
33 | +// desiredPeerGroup returns the mongo peer group according to the given |
34 | +// servers and a map with an element for each machine in info.machines |
35 | +// specifying whether that machine has been configured as voting. It may |
36 | +// return (nil, nil, nil) if the current group is already correct. |
37 | +func desiredPeerGroup(info *peerGroupInfo) ([]replicaset.Member, map[*machine]bool, error) { |
38 | + changed := false |
39 | + members, extra, maxId := info.membersMap() |
40 | + |
41 | + // We may find extra peer group members if the machines |
42 | + // have been removed or their state server status removed. |
43 | + // This should only happen if they had been set to non-voting |
44 | + // before removal, in which case we want to remove it |
45 | + // from the members list. If we find a member that's still configured |
46 | + // to vote, it's an error. |
47 | + // TODO There are some other possibilities |
48 | + // for what to do in that case. |
49 | + // 1) leave them untouched, but deal |
50 | + // with others as usual "i didn't see that bit" |
51 | + // 2) leave them untouched, deal with others, |
52 | + // but make sure the extras aren't eligible to |
53 | + // be primary. |
54 | + // 3) remove them "get rid of bad rubbish" |
55 | + // 4) bomb out "run in circles, scream and shout" |
56 | + // 5) do nothing "nothing to see here" |
57 | + for _, member := range extra { |
58 | + if member.Votes == nil || *member.Votes > 0 { |
59 | + return nil, nil, fmt.Errorf("voting non-machine member found in peer group") |
60 | + } |
61 | + changed = true |
62 | + } |
63 | + |
64 | + toRemoveVote, toAddVote, toKeep := possiblePeerGroupChanges(info, members) |
65 | + |
66 | + // Set up initial record of machine votes. Any changes after |
67 | + // this will trigger a peer group election. |
68 | + machineVoting := make(map[*machine]bool) |
69 | + for _, m := range info.machines { |
70 | + if member := members[m]; member != nil && isVotingMember(member) { |
71 | + machineVoting[m] = true |
72 | + } |
73 | + } |
74 | + setVoting := func(m *machine, voting bool) { |
75 | + setMemberVoting(members[m], voting) |
76 | + machineVoting[m] = voting |
77 | + changed = true |
78 | + } |
79 | + adjustVotes(toRemoveVote, toAddVote, setVoting) |
80 | + |
81 | + addNewMembers(members, toKeep, maxId, setVoting) |
82 | + if updateAddresses(members, info.machines) { |
83 | + changed = true |
84 | + } |
85 | + if !changed { |
86 | + return nil, nil, nil |
87 | + } |
88 | + var memberSet []replicaset.Member |
89 | + for _, member := range members { |
90 | + memberSet = append(memberSet, *member) |
91 | + } |
92 | + return memberSet, machineVoting, nil |
93 | +} |
94 | + |
95 | +func isVotingMember(member *replicaset.Member) bool { |
96 | + return member.Votes == nil || *member.Votes > 0 |
97 | +} |
98 | + |
99 | +// possiblePeerGroupChanges returns a set of slices |
100 | +// classifying all the existing machines according to |
101 | +// how their vote might move. |
102 | +// toRemoveVote holds machines whose vote should |
103 | +// be removed; toAddVote holds machines which are |
104 | +// ready to vote; toKeep holds machines with no desired |
105 | +// change to their voting status (this includes machines |
106 | +// that are not yet represented in the peer group). |
107 | +func possiblePeerGroupChanges( |
108 | + info *peerGroupInfo, |
109 | + members map[*machine]*replicaset.Member, |
110 | +) (toRemoveVote, toAddVote, toKeep []*machine) { |
111 | + statuses := info.statusesMap(members) |
112 | + |
113 | + for _, m := range info.machines { |
114 | + member := members[m] |
115 | + isVoting := member != nil && isVotingMember(member) |
116 | + switch { |
117 | + case m.wantsVote && isVoting: |
118 | + toKeep = append(toKeep, m) |
119 | + case m.wantsVote && !isVoting: |
120 | + if status, ok := statuses[m]; ok && isReady(status) { |
121 | + toAddVote = append(toAddVote, m) |
122 | + } else { |
123 | + toKeep = append(toKeep, m) |
124 | + } |
125 | + case !m.wantsVote && isVoting: |
126 | + toRemoveVote = append(toRemoveVote, m) |
127 | + case !m.wantsVote && !isVoting: |
128 | + toKeep = append(toKeep, m) |
129 | + } |
130 | + } |
131 | + // sort machines to be added and removed so that we |
132 | + // get deterministic behaviour when testing. Earlier |
133 | + // entries will be dealt with preferentially, so we could |
134 | + // potentially sort by some other metric in each case. |
135 | + sort.Sort(byId(toRemoveVote)) |
136 | + sort.Sort(byId(toAddVote)) |
137 | + sort.Sort(byId(toKeep)) |
138 | + return toRemoveVote, toAddVote, toKeep |
139 | +} |
140 | + |
141 | +// updateAddresses updates the members' addresses from the machines' addresses. |
142 | +// It reports whether any changes have been made. |
143 | +func updateAddresses(members map[*machine]*replicaset.Member, machines []*machine) bool { |
144 | + changed := false |
145 | + // Make sure all members' machine addresses are up to date. |
146 | + for _, m := range machines { |
147 | + if m.hostPort == "" { |
148 | + continue |
149 | + } |
150 | + // TODO ensure that replicaset works correctly with IPv6 [host]:port addresses. |
151 | + if m.hostPort != members[m].Address { |
152 | + members[m].Address = m.hostPort |
153 | + changed = true |
154 | + } |
155 | + } |
156 | + return changed |
157 | +} |
158 | + |
159 | +// adjustVotes adjusts the votes of the given machines, taking |
160 | +// care not to let the total number of votes become even at |
161 | +// any time. It calls setVoting to change the voting status |
162 | +// of a machine. |
163 | +func adjustVotes(toRemoveVote, toAddVote []*machine, setVoting func(*machine, bool)) { |
164 | + // Remove voting members if they can be replaced by |
165 | + // candidates that are ready. This does not affect |
166 | + // the total number of votes. |
167 | + nreplace := min(len(toRemoveVote), len(toAddVote)) |
168 | + for i := 0; i < nreplace; i++ { |
169 | + from := toRemoveVote[i] |
170 | + to := toAddVote[i] |
171 | + setVoting(from, false) |
172 | + setVoting(to, true) |
173 | + } |
174 | + toAddVote = toAddVote[nreplace:] |
175 | + toRemoveVote = toRemoveVote[nreplace:] |
176 | + |
177 | + // At this point, one or both of toAdd or toRemove is empty, so |
178 | + // we can adjust the voting-member count by an even delta, |
179 | + // maintaining the invariant that the total vote count is odd. |
180 | + if len(toAddVote) > 0 { |
181 | + toAddVote = toAddVote[0 : len(toAddVote)-len(toAddVote)%2] |
182 | + for _, m := range toAddVote { |
183 | + setVoting(m, true) |
184 | + } |
185 | + } else { |
186 | + toRemoveVote = toRemoveVote[0 : len(toRemoveVote)-len(toRemoveVote)%2] |
187 | + for _, m := range toRemoveVote { |
188 | + setVoting(m, false) |
189 | + } |
190 | + } |
191 | +} |
192 | + |
193 | +// addNewMembers adds new members from toKeep |
194 | +// to the given set of members, allocating ids from |
195 | +// maxId upwards. It calls setVoting to set the voting |
196 | +// status of each new member. |
197 | +func addNewMembers( |
198 | + members map[*machine]*replicaset.Member, |
199 | + toKeep []*machine, |
200 | + maxId int, |
201 | + setVoting func(*machine, bool), |
202 | +) { |
203 | + for _, m := range toKeep { |
204 | + if members[m] == nil && m.hostPort != "" { |
205 | + // This machine was not previously in the members list, |
206 | + // so add it (as non-voting). We maintain the |
207 | + // id manually to make it easier for tests. |
208 | + maxId++ |
209 | + member := &replicaset.Member{ |
210 | + Tags: map[string]string{ |
211 | + "juju-machine-id": m.id, |
212 | + }, |
213 | + Id: maxId, |
214 | + } |
215 | + members[m] = member |
216 | + setVoting(m, false) |
217 | + } |
218 | + } |
219 | +} |
220 | + |
221 | +func isReady(status replicaset.MemberStatus) bool { |
222 | + return status.Healthy && (status.State == replicaset.PrimaryState || |
223 | + status.State == replicaset.SecondaryState) |
224 | +} |
225 | + |
226 | +func setMemberVoting(member *replicaset.Member, voting bool) { |
227 | + if voting { |
228 | + member.Votes = nil |
229 | + member.Priority = nil |
230 | + } else { |
231 | + votes := 0 |
232 | + member.Votes = &votes |
233 | + priority := 0.0 |
234 | + member.Priority = &priority |
235 | + } |
236 | +} |
237 | + |
238 | +type byId []*machine |
239 | + |
240 | +func (l byId) Len() int { return len(l) } |
241 | +func (l byId) Swap(i, j int) { l[i], l[j] = l[j], l[i] } |
242 | +func (l byId) Less(i, j int) bool { return l[i].id < l[j].id } |
243 | + |
244 | +// membersMap returns the replica-set members inside info keyed |
245 | +// by machine. Any members that do not have a corresponding |
246 | +// machine are returned in extra. |
247 | +// The maximum replica-set id is returned in maxId. |
248 | +func (info *peerGroupInfo) membersMap() (members map[*machine]*replicaset.Member, extra []replicaset.Member, maxId int) { |
249 | + maxId = -1 |
250 | + members = make(map[*machine]*replicaset.Member) |
251 | + for _, member := range info.members { |
252 | + member := member |
253 | + var found *machine |
254 | + if mid, ok := member.Tags["juju-machine-id"]; ok { |
255 | + for _, m := range info.machines { |
256 | + if m.id == mid { |
257 | + found = m |
258 | + break |
259 | + } |
260 | + } |
261 | + } |
262 | + if found != nil { |
263 | + members[found] = &member |
264 | + } else { |
265 | + extra = append(extra, member) |
266 | + } |
267 | + if member.Id > maxId { |
268 | + maxId = member.Id |
269 | + } |
270 | + } |
271 | + return members, extra, maxId |
272 | +} |
273 | + |
274 | +// statusesMap returns the statuses inside info keyed by machine. |
275 | +// The provided members map holds the members keyed by machine, |
276 | +// as returned by membersMap. |
277 | +func (info *peerGroupInfo) statusesMap(members map[*machine]*replicaset.Member) map[*machine]replicaset.MemberStatus { |
278 | + statuses := make(map[*machine]replicaset.MemberStatus) |
279 | + for _, status := range info.statuses { |
280 | + for m, member := range members { |
281 | + if member.Id == status.Id { |
282 | + statuses[m] = status |
283 | + break |
284 | + } |
285 | + } |
286 | + } |
287 | + return statuses |
288 | +} |
289 | + |
290 | +func min(i, j int) int { |
291 | + if i < j { |
292 | + return i |
293 | + } |
294 | + return j |
295 | +} |
296 | |
297 | === added file 'worker/peergrouper/desired_test.go' |
298 | --- worker/peergrouper/desired_test.go 1970-01-01 00:00:00 +0000 |
299 | +++ worker/peergrouper/desired_test.go 2014-02-14 12:14:58 +0000 |
300 | @@ -0,0 +1,358 @@ |
301 | +package peergrouper |
302 | + |
303 | +import ( |
304 | + "fmt" |
305 | + "sort" |
306 | + "strconv" |
307 | + "strings" |
308 | + stdtesting "testing" |
309 | + |
310 | + gc "launchpad.net/gocheck" |
311 | + "launchpad.net/juju-core/replicaset" |
312 | + jc "launchpad.net/juju-core/testing/checkers" |
313 | + "launchpad.net/juju-core/testing/testbase" |
314 | +) |
315 | + |
316 | +func TestPackage(t *stdtesting.T) { |
317 | + gc.TestingT(t) |
318 | +} |
319 | + |
320 | +type desiredPeerGroupSuite struct { |
321 | + testbase.LoggingSuite |
322 | +} |
323 | + |
324 | +var _ = gc.Suite(&desiredPeerGroupSuite{}) |
325 | + |
326 | +const mongoPort = 1234 |
327 | + |
328 | +var desiredPeerGroupTests = []struct { |
329 | + about string |
330 | + machines []*machine |
331 | + statuses []replicaset.MemberStatus |
332 | + members []replicaset.Member |
333 | + |
334 | + expectMembers []replicaset.Member |
335 | + expectVoting []bool |
336 | + expectErr string |
337 | +}{{ |
338 | + about: "single machine, no change", |
339 | + machines: mkMachines("11v"), |
340 | + members: mkMembers("1v"), |
341 | + statuses: mkStatuses("1p"), |
342 | + expectVoting: []bool{true}, |
343 | + expectMembers: nil, |
344 | +}, { |
345 | + about: "extra member with nil Vote", |
346 | + machines: mkMachines("11v"), |
347 | + members: mkMembers("1v 2vT"), |
348 | + statuses: mkStatuses("1p 2s"), |
349 | + expectVoting: []bool{true}, |
350 | + expectErr: "voting non-machine member found in peer group", |
351 | +}, { |
352 | + about: "extra member with >1 votes", |
353 | + machines: mkMachines("11v"), |
354 | + members: append(mkMembers("1v"), replicaset.Member{ |
355 | + Id: 2, |
356 | + Votes: newInt(2), |
357 | + Address: "0.1.2.12:1234", |
358 | + }), |
359 | + statuses: mkStatuses("1p 2s"), |
360 | + expectVoting: []bool{true}, |
361 | + expectErr: "voting non-machine member found in peer group", |
362 | +}, { |
363 | + about: "new machine with no associated member", |
364 | + machines: mkMachines("11v 12v"), |
365 | + members: mkMembers("1v"), |
366 | + statuses: mkStatuses("1p"), |
367 | + expectVoting: []bool{true, false}, |
368 | + expectMembers: mkMembers("1v 2"), |
369 | +}, { |
370 | + about: "one machine has become ready to vote (-> no change)", |
371 | + machines: mkMachines("11v 12v"), |
372 | + members: mkMembers("1v 2"), |
373 | + statuses: mkStatuses("1p 2s"), |
374 | + expectVoting: []bool{true, false}, |
375 | + expectMembers: nil, |
376 | +}, { |
377 | + about: "two machines have become ready to vote (-> added)", |
378 | + machines: mkMachines("11v 12v 13v"), |
379 | + members: mkMembers("1v 2 3"), |
380 | + statuses: mkStatuses("1p 2s 3s"), |
381 | + expectVoting: []bool{true, true, true}, |
382 | + expectMembers: mkMembers("1v 2v 3v"), |
383 | +}, { |
384 | + about: "two machines have become ready to vote but one is not healthy (-> no change)", |
385 | + machines: mkMachines("11v 12v 13v"), |
386 | + members: mkMembers("1v 2 3"), |
387 | + statuses: mkStatuses("1p 2s 3sH"), |
388 | + expectVoting: []bool{true, false, false}, |
389 | + expectMembers: nil, |
390 | +}, { |
391 | + about: "three machines have become ready to vote (-> 2 added)", |
392 | + machines: mkMachines("11v 12v 13v 14v"), |
393 | + members: mkMembers("1v 2 3 4"), |
394 | + statuses: mkStatuses("1p 2s 3s 4s"), |
395 | + expectVoting: []bool{true, true, true, false}, |
396 | + expectMembers: mkMembers("1v 2v 3v 4"), |
397 | +}, { |
398 | + about: "one machine ready to lose vote with no others -> no change", |
399 | + machines: mkMachines("11"), |
400 | + members: mkMembers("1v"), |
401 | + statuses: mkStatuses("1p"), |
402 | + expectVoting: []bool{true}, |
403 | + expectMembers: nil, |
404 | +}, { |
405 | + about: "two machines ready to lose vote -> votes removed", |
406 | + machines: mkMachines("11 12v 13"), |
407 | + members: mkMembers("1v 2v 3v"), |
408 | + statuses: mkStatuses("1p 2p 3p"), |
409 | + expectVoting: []bool{false, true, false}, |
410 | + expectMembers: mkMembers("1 2v 3"), |
411 | +}, { |
412 | + about: "machines removed as state server -> removed from members", |
413 | + machines: mkMachines("11v"), |
414 | + members: mkMembers("1v 2 3"), |
415 | + statuses: mkStatuses("1p 2s 3s"), |
416 | + expectVoting: []bool{true}, |
417 | + expectMembers: mkMembers("1v"), |
418 | +}, { |
419 | + about: "a candidate can take the vote of a non-candidate when they're ready", |
420 | + machines: mkMachines("11v 12v 13 14v"), |
421 | + members: mkMembers("1v 2v 3v 4"), |
422 | + statuses: mkStatuses("1p 2s 3s 4s"), |
423 | + expectVoting: []bool{true, true, false, true}, |
424 | + expectMembers: mkMembers("1v 2v 3 4v"), |
425 | +}, { |
426 | + about: "several candidates can take non-candidates' votes", |
427 | + machines: mkMachines("11v 12v 13 14 15 16v 17v 18v"), |
428 | + members: mkMembers("1v 2v 3v 4v 5v 6 7 8"), |
429 | + statuses: mkStatuses("1p 2s 3s 4s 5s 6s 7s 8s"), |
430 | + expectVoting: []bool{true, true, false, false, false, true, true, true}, |
431 | + expectMembers: mkMembers("1v 2v 3 4 5 6v 7v 8v"), |
432 | +}, { |
433 | + about: "a changed machine address should propagate to the members", |
434 | + machines: append(mkMachines("11v 12v"), &machine{ |
435 | + id: "13", |
436 | + wantsVote: true, |
437 | + hostPort: "0.1.99.13:1234", |
438 | + }), |
439 | + statuses: mkStatuses("1s 2p 3p"), |
440 | + members: mkMembers("1v 2v 3v"), |
441 | + expectVoting: []bool{true, true, true}, |
442 | + expectMembers: append(mkMembers("1v 2v"), replicaset.Member{ |
443 | + Id: 3, |
444 | + Address: "0.1.99.13:1234", |
445 | + Tags: memberTag("13"), |
446 | + }), |
447 | +}} |
448 | + |
449 | +func (*desiredPeerGroupSuite) TestDesiredPeerGroup(c *gc.C) { |
450 | + for i, test := range desiredPeerGroupTests { |
451 | + c.Logf("\ntest %d: %s", i, test.about) |
452 | + info := &peerGroupInfo{ |
453 | + machines: test.machines, |
454 | + statuses: test.statuses, |
455 | + members: test.members, |
456 | + } |
457 | + members, voting, err := desiredPeerGroup(info) |
458 | + if test.expectErr != "" { |
459 | + c.Assert(err, gc.ErrorMatches, test.expectErr) |
460 | + c.Assert(members, gc.IsNil) |
461 | + continue |
462 | + } |
463 | + sort.Sort(membersById(members)) |
464 | + c.Assert(members, jc.DeepEquals, test.expectMembers) |
465 | + if len(members) == 0 { |
466 | + continue |
467 | + } |
468 | + for i, m := range info.machines { |
469 | + c.Assert(voting[m], gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.id)) |
470 | + } |
471 | + // Assure ourselves that the total number of desired votes is odd in |
472 | + // all circumstances. |
473 | + c.Assert(countVotes(members)%2, gc.Equals, 1) |
474 | + |
475 | + // Make sure that when the members are set as |
476 | + // required, that there's no further change |
477 | + // if desiredPeerGroup is called again. |
478 | + info.members = members |
479 | + members, voting, err = desiredPeerGroup(info) |
480 | + c.Assert(members, gc.IsNil) |
481 | + c.Assert(voting, gc.IsNil) |
482 | + c.Assert(err, gc.IsNil) |
483 | + } |
484 | +} |
485 | + |
486 | +func countVotes(members []replicaset.Member) int { |
487 | + tot := 0 |
488 | + for _, m := range members { |
489 | + v := 1 |
490 | + if m.Votes != nil { |
491 | + v = *m.Votes |
492 | + } |
493 | + tot += v |
494 | + } |
495 | + return tot |
496 | +} |
497 | + |
498 | +func newInt(i int) *int { |
499 | + return &i |
500 | +} |
501 | + |
502 | +func newFloat64(f float64) *float64 { |
503 | + return &f |
504 | +} |
505 | + |
506 | +// mkMachines returns a slice of *machine based on |
507 | +// the given description. |
508 | +// Each machine in the description is white-space separated |
509 | +// and holds the decimal machine id followed by an optional |
510 | +// "v" if the machine wants a vote. |
511 | +func mkMachines(description string) []*machine { |
512 | + descrs := parseDescr(description) |
513 | + ms := make([]*machine, len(descrs)) |
514 | + for i, d := range descrs { |
515 | + ms[i] = &machine{ |
516 | + id: fmt.Sprint(d.id), |
517 | + hostPort: fmt.Sprintf("0.1.2.%d:%d", d.id, mongoPort), |
518 | + wantsVote: strings.Contains(d.flags, "v"), |
519 | + } |
520 | + } |
521 | + return ms |
522 | +} |
523 | + |
524 | +func memberTag(id string) map[string]string { |
525 | + return map[string]string{"juju-machine-id": id} |
526 | +} |
527 | + |
528 | +// mkMembers returns a slice of *replicaset.Member |
529 | +// based on the given description. |
530 | +// Each member in the description is white-space separated |
531 | +// and holds the decimal replica-set id optionally followed by the characters: |
532 | +// - 'v' if the member is voting. |
533 | +// - 'T' if the member has no associated machine tags. |
534 | +// Unless the T flag is specified, the machine tag |
535 | +// will be the replica-set id + 10. |
536 | +func mkMembers(description string) []replicaset.Member { |
537 | + descrs := parseDescr(description) |
538 | + ms := make([]replicaset.Member, len(descrs)) |
539 | + for i, d := range descrs { |
540 | + machineId := d.id + 10 |
541 | + m := replicaset.Member{ |
542 | + Id: d.id, |
543 | + Address: fmt.Sprintf("0.1.2.%d:%d", machineId, mongoPort), |
544 | + Tags: memberTag(fmt.Sprint(machineId)), |
545 | + } |
546 | + if !strings.Contains(d.flags, "v") { |
547 | + m.Priority = newFloat64(0) |
548 | + m.Votes = newInt(0) |
549 | + } |
550 | + if strings.Contains(d.flags, "T") { |
551 | + m.Tags = nil |
552 | + } |
553 | + ms[i] = m |
554 | + } |
555 | + return ms |
556 | +} |
557 | + |
558 | +var stateFlags = map[rune]replicaset.MemberState{ |
559 | + 'p': replicaset.PrimaryState, |
560 | + 's': replicaset.SecondaryState, |
561 | +} |
562 | + |
563 | +// mkStatuses returns a slice of *replicaset.Member |
564 | +// based on the given description. |
565 | +// Each member in the description is white-space separated |
566 | +// and holds the decimal replica-set id optionally followed by the |
567 | +// characters: |
568 | +// - 'H' if the instance is not healthy. |
569 | +// - 'p' if the instance is in PrimaryState |
570 | +// - 's' if the instance is in SecondaryState |
571 | +func mkStatuses(description string) []replicaset.MemberStatus { |
572 | + descrs := parseDescr(description) |
573 | + ss := make([]replicaset.MemberStatus, len(descrs)) |
574 | + for i, d := range descrs { |
575 | + machineId := d.id + 10 |
576 | + s := replicaset.MemberStatus{ |
577 | + Id: d.id, |
578 | + Address: fmt.Sprintf("0.1.2.%d:%d", machineId, mongoPort), |
579 | + Healthy: !strings.Contains(d.flags, "H"), |
580 | + State: replicaset.UnknownState, |
581 | + } |
582 | + for _, r := range d.flags { |
583 | + if state, ok := stateFlags[r]; ok { |
584 | + s.State = state |
585 | + } |
586 | + } |
587 | + ss[i] = s |
588 | + } |
589 | + return ss |
590 | +} |
591 | + |
592 | +type descr struct { |
593 | + id int |
594 | + flags string |
595 | +} |
596 | + |
597 | +func isNotDigit(r rune) bool { |
598 | + return r < '0' || r > '9' |
599 | +} |
600 | + |
601 | +var parseDescrTests = []struct { |
602 | + descr string |
603 | + expect []descr |
604 | +}{{ |
605 | + descr: "", |
606 | + expect: []descr{}, |
607 | +}, { |
608 | + descr: "0", |
609 | + expect: []descr{{id: 0}}, |
610 | +}, { |
611 | + descr: "1foo", |
612 | + expect: []descr{{id: 1, flags: "foo"}}, |
613 | +}, { |
614 | + descr: "10c 5 6443arble ", |
615 | + expect: []descr{{ |
616 | + id: 10, |
617 | + flags: "c", |
618 | + }, { |
619 | + id: 5, |
620 | + }, { |
621 | + id: 6443, |
622 | + flags: "arble", |
623 | + }}, |
624 | +}} |
625 | + |
626 | +func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) { |
627 | + for i, test := range parseDescrTests { |
628 | + c.Logf("test %d. %q", i, test.descr) |
629 | + c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect) |
630 | + } |
631 | +} |
632 | + |
633 | +// parseDescr parses white-space separated fields of the form |
634 | +// <id><flags> into descr structures. |
635 | +func parseDescr(s string) []descr { |
636 | + fields := strings.Fields(s) |
637 | + descrs := make([]descr, len(fields)) |
638 | + for i, field := range fields { |
639 | + d := &descrs[i] |
640 | + i := strings.IndexFunc(field, isNotDigit) |
641 | + if i == -1 { |
642 | + i = len(field) |
643 | + } |
644 | + id, err := strconv.Atoi(field[0:i]) |
645 | + if err != nil { |
646 | + panic(fmt.Errorf("bad field %q", field)) |
647 | + } |
648 | + d.id = id |
649 | + d.flags = field[i:] |
650 | + } |
651 | + return descrs |
652 | +} |
653 | + |
654 | +type membersById []replicaset.Member |
655 | + |
656 | +func (l membersById) Len() int { return len(l) } |
657 | +func (l membersById) Swap(i, j int) { l[i], l[j] = l[j], l[i] } |
658 | +func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id } |
Reviewers: mp+201245_ code.launchpad. net,
Message:
Please take a look.
Description:
worker/peergrouper: new package
This will be a worker that maintains the replica set
with respect to the state.
The function in this CL implements the core functionality - it's
a stateless function that looks at a representation of the
current state of affairs and decides what the replicaset
member list should look like.
It may well change when faced with reality (although I've been
trying to sanity check with experimental code),
but I think it should be reasonable to check in now
as a staging post.
There are current some extraneous changes in this
branch (everything outside worker/peergrouper)
which are made redundant by other MPs. Please
ignore for the purposes of this review; I'll remove
before submitting.
https:/ /code.launchpad .net/~rogpeppe/ juju-core/ 479-desired- peer-group/ +merge/ 201245
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/49920045/
Affected files (+684, -38 lines): replicaset. go replicaset_ test.go peergrouper/ desired. go peergrouper/ desired_ test.go uniter/ uniter_ test.go
A [revision details]
M replicaset/
M replicaset/
M testing/mgo.go
A worker/
A worker/
M worker/