github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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 // jujuMachineTag is the key for the tag where we save the member's juju machine id. 16 const jujuMachineTag = "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( 109 info *peerGroupInfo, 110 members map[*machine]*replicaset.Member, 111 ) (toRemoveVote, toAddVote, toKeep []*machine) { 112 statuses := info.statusesMap(members) 113 114 logger.Debugf("assessing possible peer group changes:") 115 for _, m := range info.machines { 116 member := members[m] 117 isVoting := member != nil && isVotingMember(member) 118 switch { 119 case m.wantsVote && isVoting: 120 logger.Debugf("machine %q is already voting", m.id) 121 toKeep = append(toKeep, m) 122 case m.wantsVote && !isVoting: 123 if status, ok := statuses[m]; ok && isReady(status) { 124 logger.Debugf("machine %q is a potential voter", m.id) 125 toAddVote = append(toAddVote, m) 126 } else { 127 logger.Debugf("machine %q is not ready (has status: %v)", m.id, ok) 128 toKeep = append(toKeep, m) 129 } 130 case !m.wantsVote && isVoting: 131 logger.Debugf("machine %q is a potential non-voter", m.id) 132 toRemoveVote = append(toRemoveVote, m) 133 case !m.wantsVote && !isVoting: 134 logger.Debugf("machine %q does not want the vote", m.id) 135 toKeep = append(toKeep, m) 136 } 137 } 138 logger.Debugf("assessed") 139 // sort machines to be added and removed so that we 140 // get deterministic behaviour when testing. Earlier 141 // entries will be dealt with preferentially, so we could 142 // potentially sort by some other metric in each case. 143 sort.Sort(byId(toRemoveVote)) 144 sort.Sort(byId(toAddVote)) 145 sort.Sort(byId(toKeep)) 146 return toRemoveVote, toAddVote, toKeep 147 } 148 149 // updateAddresses updates the members' addresses from the machines' addresses. 150 // It reports whether any changes have been made. 151 func updateAddresses(members map[*machine]*replicaset.Member, machines map[string]*machine) bool { 152 changed := false 153 // Make sure all members' machine addresses are up to date. 154 for _, m := range machines { 155 hp := m.mongoHostPort() 156 if hp == "" { 157 continue 158 } 159 // TODO ensure that replicaset works correctly with IPv6 [host]:port addresses. 160 if hp != members[m].Address { 161 members[m].Address = hp 162 changed = true 163 } 164 } 165 return changed 166 } 167 168 // adjustVotes adjusts the votes of the given machines, taking 169 // care not to let the total number of votes become even at 170 // any time. It calls setVoting to change the voting status 171 // of a machine. 172 func adjustVotes(toRemoveVote, toAddVote []*machine, setVoting func(*machine, bool)) { 173 // Remove voting members if they can be replaced by 174 // candidates that are ready. This does not affect 175 // the total number of votes. 176 nreplace := min(len(toRemoveVote), len(toAddVote)) 177 for i := 0; i < nreplace; i++ { 178 from := toRemoveVote[i] 179 to := toAddVote[i] 180 setVoting(from, false) 181 setVoting(to, true) 182 } 183 toAddVote = toAddVote[nreplace:] 184 toRemoveVote = toRemoveVote[nreplace:] 185 186 // At this point, one or both of toAdd or toRemove is empty, so 187 // we can adjust the voting-member count by an even delta, 188 // maintaining the invariant that the total vote count is odd. 189 if len(toAddVote) > 0 { 190 toAddVote = toAddVote[0 : len(toAddVote)-len(toAddVote)%2] 191 for _, m := range toAddVote { 192 setVoting(m, true) 193 } 194 } else { 195 toRemoveVote = toRemoveVote[0 : len(toRemoveVote)-len(toRemoveVote)%2] 196 for _, m := range toRemoveVote { 197 setVoting(m, false) 198 } 199 } 200 } 201 202 // addNewMembers adds new members from toKeep 203 // to the given set of members, allocating ids from 204 // maxId upwards. It calls setVoting to set the voting 205 // status of each new member. 206 func addNewMembers( 207 members map[*machine]*replicaset.Member, 208 toKeep []*machine, 209 maxId int, 210 setVoting func(*machine, bool), 211 ) { 212 for _, m := range toKeep { 213 hasAddress := m.mongoHostPort() != "" 214 if members[m] == nil && hasAddress { 215 // This machine was not previously in the members list, 216 // so add it (as non-voting). We maintain the 217 // id manually to make it easier for tests. 218 maxId++ 219 member := &replicaset.Member{ 220 Tags: map[string]string{ 221 jujuMachineTag: m.id, 222 }, 223 Id: maxId, 224 } 225 members[m] = member 226 setVoting(m, false) 227 } else if !hasAddress { 228 logger.Debugf("ignoring machine %q with no address", m.id) 229 } 230 } 231 } 232 233 func isReady(status replicaset.MemberStatus) bool { 234 return status.Healthy && (status.State == replicaset.PrimaryState || 235 status.State == replicaset.SecondaryState) 236 } 237 238 func setMemberVoting(member *replicaset.Member, voting bool) { 239 if voting { 240 member.Votes = nil 241 member.Priority = nil 242 } else { 243 votes := 0 244 member.Votes = &votes 245 priority := 0.0 246 member.Priority = &priority 247 } 248 } 249 250 type byId []*machine 251 252 func (l byId) Len() int { return len(l) } 253 func (l byId) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 254 func (l byId) Less(i, j int) bool { return l[i].id < l[j].id } 255 256 // membersMap returns the replica-set members inside info keyed 257 // by machine. Any members that do not have a corresponding 258 // machine are returned in extra. 259 // The maximum replica-set id is returned in maxId. 260 func (info *peerGroupInfo) membersMap() (members map[*machine]*replicaset.Member, extra []replicaset.Member, maxId int) { 261 maxId = -1 262 members = make(map[*machine]*replicaset.Member) 263 for _, member := range info.members { 264 member := member 265 mid, ok := member.Tags[jujuMachineTag] 266 var found *machine 267 if ok { 268 found = info.machines[mid] 269 } 270 if found != nil { 271 members[found] = &member 272 } else { 273 extra = append(extra, member) 274 } 275 if member.Id > maxId { 276 maxId = member.Id 277 } 278 } 279 return members, extra, maxId 280 } 281 282 // statusesMap returns the statuses inside info keyed by machine. 283 // The provided members map holds the members keyed by machine, 284 // as returned by membersMap. 285 func (info *peerGroupInfo) statusesMap(members map[*machine]*replicaset.Member) map[*machine]replicaset.MemberStatus { 286 statuses := make(map[*machine]replicaset.MemberStatus) 287 for _, status := range info.statuses { 288 for m, member := range members { 289 if member.Id == status.Id { 290 statuses[m] = status 291 break 292 } 293 } 294 } 295 return statuses 296 } 297 298 func min(i, j int) int { 299 if i < j { 300 return i 301 } 302 return j 303 }