github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/worker/peergrouper/desired.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 10 "github.com/juju/loggo" 11 12 "launchpad.net/juju-core/replicaset" 13 ) 14 15 var logger = loggo.GetLogger("juju.worker.peergrouper") 16 17 // peerGroupInfo holds information that may contribute to 18 // a peer group. 19 type peerGroupInfo struct { 20 machines map[string]*machine // id -> machine 21 statuses []replicaset.MemberStatus 22 members []replicaset.Member 23 } 24 25 // desiredPeerGroup returns the mongo peer group according to the given 26 // servers and a map with an element for each machine in info.machines 27 // specifying whether that machine has been configured as voting. It may 28 // return (nil, nil, nil) if the current group is already correct. 29 func desiredPeerGroup(info *peerGroupInfo) ([]replicaset.Member, map[*machine]bool, error) { 30 if len(info.members) == 0 { 31 return nil, nil, fmt.Errorf("current member set is empty") 32 } 33 changed := false 34 members, extra, maxId := info.membersMap() 35 logger.Debugf("calculating desired peer group") 36 logger.Debugf("members: %#v", members) 37 logger.Debugf("extra: %#v", extra) 38 logger.Debugf("maxId: %v", maxId) 39 40 // We may find extra peer group members if the machines 41 // have been removed or their state server status removed. 42 // This should only happen if they had been set to non-voting 43 // before removal, in which case we want to remove it 44 // from the members list. If we find a member that's still configured 45 // to vote, it's an error. 46 // TODO There are some other possibilities 47 // for what to do in that case. 48 // 1) leave them untouched, but deal 49 // with others as usual "i didn't see that bit" 50 // 2) leave them untouched, deal with others, 51 // but make sure the extras aren't eligible to 52 // be primary. 53 // 3) remove them "get rid of bad rubbish" 54 // 4) do nothing "nothing to see here" 55 for _, member := range extra { 56 if member.Votes == nil || *member.Votes > 0 { 57 return nil, nil, fmt.Errorf("voting non-machine member %#v found in peer group", member) 58 } 59 changed = true 60 } 61 62 toRemoveVote, toAddVote, toKeep := possiblePeerGroupChanges(info, members) 63 64 // Set up initial record of machine votes. Any changes after 65 // this will trigger a peer group election. 66 machineVoting := make(map[*machine]bool) 67 for _, m := range info.machines { 68 if member := members[m]; member != nil && isVotingMember(member) { 69 machineVoting[m] = true 70 } 71 } 72 setVoting := func(m *machine, voting bool) { 73 setMemberVoting(members[m], voting) 74 machineVoting[m] = voting 75 changed = true 76 } 77 adjustVotes(toRemoveVote, toAddVote, setVoting) 78 79 addNewMembers(members, toKeep, maxId, setVoting) 80 if updateAddresses(members, info.machines) { 81 changed = true 82 } 83 if !changed { 84 return nil, nil, nil 85 } 86 var memberSet []replicaset.Member 87 for _, member := range members { 88 memberSet = append(memberSet, *member) 89 } 90 return memberSet, machineVoting, nil 91 } 92 93 func isVotingMember(member *replicaset.Member) bool { 94 return member.Votes == nil || *member.Votes > 0 95 } 96 97 // possiblePeerGroupChanges returns a set of slices 98 // classifying all the existing machines according to 99 // how their vote might move. 100 // toRemoveVote holds machines whose vote should 101 // be removed; toAddVote holds machines which are 102 // ready to vote; toKeep holds machines with no desired 103 // change to their voting status (this includes machines 104 // that are not yet represented in the peer group). 105 func possiblePeerGroupChanges( 106 info *peerGroupInfo, 107 members map[*machine]*replicaset.Member, 108 ) (toRemoveVote, toAddVote, toKeep []*machine) { 109 statuses := info.statusesMap(members) 110 111 logger.Debugf("assessing possible peer group changes:") 112 for _, m := range info.machines { 113 member := members[m] 114 isVoting := member != nil && isVotingMember(member) 115 switch { 116 case m.wantsVote && isVoting: 117 logger.Debugf("machine %q is already voting", m.id) 118 toKeep = append(toKeep, m) 119 case m.wantsVote && !isVoting: 120 if status, ok := statuses[m]; ok && isReady(status) { 121 logger.Debugf("machine %q is a potential voter", m.id) 122 toAddVote = append(toAddVote, m) 123 } else { 124 logger.Debugf("machine %q is not ready (has status: %v)", m.id, ok) 125 toKeep = append(toKeep, m) 126 } 127 case !m.wantsVote && isVoting: 128 logger.Debugf("machine %q is a potential non-voter", m.id) 129 toRemoveVote = append(toRemoveVote, m) 130 case !m.wantsVote && !isVoting: 131 logger.Debugf("machine %q does not want the vote", m.id) 132 toKeep = append(toKeep, m) 133 } 134 } 135 logger.Debugf("assessed") 136 // sort machines to be added and removed so that we 137 // get deterministic behaviour when testing. Earlier 138 // entries will be dealt with preferentially, so we could 139 // potentially sort by some other metric in each case. 140 sort.Sort(byId(toRemoveVote)) 141 sort.Sort(byId(toAddVote)) 142 sort.Sort(byId(toKeep)) 143 return toRemoveVote, toAddVote, toKeep 144 } 145 146 // updateAddresses updates the members' addresses from the machines' addresses. 147 // It reports whether any changes have been made. 148 func updateAddresses(members map[*machine]*replicaset.Member, machines map[string]*machine) bool { 149 changed := false 150 // Make sure all members' machine addresses are up to date. 151 for _, m := range machines { 152 if m.hostPort == "" { 153 continue 154 } 155 // TODO ensure that replicaset works correctly with IPv6 [host]:port addresses. 156 if m.hostPort != members[m].Address { 157 members[m].Address = m.hostPort 158 changed = true 159 } 160 } 161 return changed 162 } 163 164 // adjustVotes adjusts the votes of the given machines, taking 165 // care not to let the total number of votes become even at 166 // any time. It calls setVoting to change the voting status 167 // of a machine. 168 func adjustVotes(toRemoveVote, toAddVote []*machine, setVoting func(*machine, bool)) { 169 // Remove voting members if they can be replaced by 170 // candidates that are ready. This does not affect 171 // the total number of votes. 172 nreplace := min(len(toRemoveVote), len(toAddVote)) 173 for i := 0; i < nreplace; i++ { 174 from := toRemoveVote[i] 175 to := toAddVote[i] 176 setVoting(from, false) 177 setVoting(to, true) 178 } 179 toAddVote = toAddVote[nreplace:] 180 toRemoveVote = toRemoveVote[nreplace:] 181 182 // At this point, one or both of toAdd or toRemove is empty, so 183 // we can adjust the voting-member count by an even delta, 184 // maintaining the invariant that the total vote count is odd. 185 if len(toAddVote) > 0 { 186 toAddVote = toAddVote[0 : len(toAddVote)-len(toAddVote)%2] 187 for _, m := range toAddVote { 188 setVoting(m, true) 189 } 190 } else { 191 toRemoveVote = toRemoveVote[0 : len(toRemoveVote)-len(toRemoveVote)%2] 192 for _, m := range toRemoveVote { 193 setVoting(m, false) 194 } 195 } 196 } 197 198 // addNewMembers adds new members from toKeep 199 // to the given set of members, allocating ids from 200 // maxId upwards. It calls setVoting to set the voting 201 // status of each new member. 202 func addNewMembers( 203 members map[*machine]*replicaset.Member, 204 toKeep []*machine, 205 maxId int, 206 setVoting func(*machine, bool), 207 ) { 208 for _, m := range toKeep { 209 if members[m] == nil && m.hostPort != "" { 210 // This machine was not previously in the members list, 211 // so add it (as non-voting). We maintain the 212 // id manually to make it easier for tests. 213 maxId++ 214 member := &replicaset.Member{ 215 Tags: map[string]string{ 216 "juju-machine-id": m.id, 217 }, 218 Id: maxId, 219 } 220 members[m] = member 221 setVoting(m, false) 222 } else if m.hostPort == "" { 223 logger.Debugf("ignoring machine %q with no address", m.id) 224 } 225 } 226 } 227 228 func isReady(status replicaset.MemberStatus) bool { 229 return status.Healthy && (status.State == replicaset.PrimaryState || 230 status.State == replicaset.SecondaryState) 231 } 232 233 func setMemberVoting(member *replicaset.Member, voting bool) { 234 if voting { 235 member.Votes = nil 236 member.Priority = nil 237 } else { 238 votes := 0 239 member.Votes = &votes 240 priority := 0.0 241 member.Priority = &priority 242 } 243 } 244 245 type byId []*machine 246 247 func (l byId) Len() int { return len(l) } 248 func (l byId) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 249 func (l byId) Less(i, j int) bool { return l[i].id < l[j].id } 250 251 // membersMap returns the replica-set members inside info keyed 252 // by machine. Any members that do not have a corresponding 253 // machine are returned in extra. 254 // The maximum replica-set id is returned in maxId. 255 func (info *peerGroupInfo) membersMap() (members map[*machine]*replicaset.Member, extra []replicaset.Member, maxId int) { 256 maxId = -1 257 members = make(map[*machine]*replicaset.Member) 258 for _, member := range info.members { 259 member := member 260 mid, ok := member.Tags["juju-machine-id"] 261 var found *machine 262 if ok { 263 found = info.machines[mid] 264 } 265 if found != nil { 266 members[found] = &member 267 } else { 268 extra = append(extra, member) 269 } 270 if member.Id > maxId { 271 maxId = member.Id 272 } 273 } 274 return members, extra, maxId 275 } 276 277 // statusesMap returns the statuses inside info keyed by machine. 278 // The provided members map holds the members keyed by machine, 279 // as returned by membersMap. 280 func (info *peerGroupInfo) statusesMap(members map[*machine]*replicaset.Member) map[*machine]replicaset.MemberStatus { 281 statuses := make(map[*machine]replicaset.MemberStatus) 282 for _, status := range info.statuses { 283 for m, member := range members { 284 if member.Id == status.Id { 285 statuses[m] = status 286 break 287 } 288 } 289 } 290 return statuses 291 } 292 293 func min(i, j int) int { 294 if i < j { 295 return i 296 } 297 return j 298 }