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