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  }