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