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  }