Merge lp:~rogpeppe/juju-core/479-desired-peer-group into lp:~go-bot/juju-core/trunk

Proposed by Roger Peppe
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
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.

https://codereview.appspot.com/49920045/

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.

https://codereview.appspot.com/49920045/

To post a comment you must log in.
Revision history for this message
Roger Peppe (rogpeppe) wrote :

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):
   A [revision details]
   M replicaset/replicaset.go
   M replicaset/replicaset_test.go
   M testing/mgo.go
   A worker/peergrouper/desired.go
   A worker/peergrouper/desired_test.go
   M worker/uniter/uniter_test.go

Revision history for this message
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://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go
File worker/peergrouper/desired.go (right):

https://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode32
worker/peergrouper/desired.go:32: //func getPeerGroupInfo(st
*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://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode58
worker/peergrouper/desired.go:58: return j
Is there really nothing like min that we can just use?

https://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode66
worker/peergrouper/desired.go:66: func desiredPeerGroup(info
*peerGroupInfo) ([]replicaset.Member, error) {
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.

https://codereview.appspot.com/49920045/

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Please take a look.

https://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go
File worker/peergrouper/desired.go (right):

https://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode32
worker/peergrouper/desired.go:32: //func getPeerGroupInfo(st
*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://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode58
worker/peergrouper/desired.go:58: return j
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://codereview.appspot.com/49920045/diff/20001/worker/peergrouper/desired.go#newcode66
worker/peergrouper/desired.go:66: func desiredPeerGroup(info
*peerGroupInfo) ([]replicaset.Member, error) {
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.

https://codereview.appspot.com/49920045/

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Revision history for this message
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://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go
File worker/peergrouper/desired.go (right):

https://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go#newcode38
worker/peergrouper/desired.go:38: func desiredPeerGroup(info
*peerGroupInfo) ([]replicaset.Member, map[*machine]bool, error) {
This function is pretty long, would be a little easier to read if it
were broken up more, I think.

https://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go#newcode199
worker/peergrouper/desired.go:199: found = m
break here?

https://codereview.appspot.com/49920045/

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Please take a look.

https://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go
File worker/peergrouper/desired.go (right):

https://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go#newcode38
worker/peergrouper/desired.go:38: func desiredPeerGroup(info
*peerGroupInfo) ([]replicaset.Member, map[*machine]bool, error) {
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://codereview.appspot.com/49920045/diff/80001/worker/peergrouper/desired.go#newcode199
worker/peergrouper/desired.go:199: found = m
On 2014/01/13 18:47:15, nate.finch wrote:
> break here?

Done.

https://codereview.appspot.com/49920045/

Revision history for this message
Nate Finch (natefinch) wrote :

LGTM The refactor makes a big difference.

https://codereview.appspot.com/49920045/

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Revision history for this message
Go Bot (go-bot) wrote :
Download full text (465.0 KiB)

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.net/juju-core/agent 1.262s
ok launchpad.net/juju-core/agent/tools 0.279s
ok launchpad.net/juju-core/bzr 8.061s
ok launchpad.net/juju-core/cert 3.420s
ok launchpad.net/juju-core/charm 0.609s
? launchpad.net/juju-core/charm/hooks [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.067s
ok launchpad.net/juju-core/cmd 0.224s
ok launchpad.net/juju-core/cmd/charm-admin 0.848s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 174.789s

----------------------------------------------------------------------
FAIL: machine_test.go:273: MachineSuite.TestManageEnviron

[LOG] 30.61946 DEBUG juju.environs.configstore Making /tmp/gocheck-6129484611666145821/45/home/ubuntu/.juju/environments
[LOG] 30.73054 DEBUG juju.environs.tools reading v1.* tools
[LOG] 30.73059 INFO juju environs/testing: uploading FAKE tools 1.17.1-precise-amd64
[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.simplestreams fetchData failed for "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.sjson": file "tools/streams/v1/index.sjson" not found not found
[LOG] 30.73165 DEBUG juju.environs.simplestreams cannot load index "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.sjson": invalid URL "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.sjson" not found
[LOG] 30.73169 DEBUG juju.environs.simplestreams fetchData failed for "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.json": file "tools/streams/v1/index.json" not found not found
[LOG] 30.73171 DEBUG juju.environs.simplestreams cannot load index "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.json": invalid URL "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.json" not found
[LOG] 30.73198 INFO juju.environs.tools Writing tools/streams/v1/index.json
[LOG] 30.73202 INFO juju.environs.tools Writing tools/streams/v1/com.ubuntu.juju:released:tools.json
[LOG] 30.73218 INFO juju.environs.bootstrap bootstrapping environment "dummyenv"
[LOG] 30.73221 DEBUG juju.environs.bootstrap looking for bootstrap tools: series="precise", arch=<nil>, version=1.17.1
[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.simplestreams fetchData failed for "http://127.0.0.1:52068/dummyenv/private/tools/streams/v1/index.sjson": file "tools/streams/v1/index.sjson" not found not found
[LOG] 30.73233 DEBUG juju.environs.simplestreams cannot load index "http:/...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 }

Subscribers

People subscribed via source and target branches

to status/vote changes: