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  }