github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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  	"launchpad.net/juju-core/replicaset"
    13  )
    14  
    15  var logger = loggo.GetLogger("juju.worker.peergrouper")
    16  
    17  // peerGroupInfo holds information that may contribute to
    18  // a peer group.
    19  type peerGroupInfo struct {
    20  	machines map[string]*machine // id -> machine
    21  	statuses []replicaset.MemberStatus
    22  	members  []replicaset.Member
    23  }
    24  
    25  // desiredPeerGroup returns the mongo peer group according to the given
    26  // servers and a map with an element for each machine in info.machines
    27  // specifying whether that machine has been configured as voting. It may
    28  // return (nil, nil, nil) if the current group is already correct.
    29  func desiredPeerGroup(info *peerGroupInfo) ([]replicaset.Member, map[*machine]bool, error) {
    30  	if len(info.members) == 0 {
    31  		return nil, nil, fmt.Errorf("current member set is empty")
    32  	}
    33  	changed := false
    34  	members, extra, maxId := info.membersMap()
    35  	logger.Debugf("calculating desired peer group")
    36  	logger.Debugf("members: %#v", members)
    37  	logger.Debugf("extra: %#v", extra)
    38  	logger.Debugf("maxId: %v", maxId)
    39  
    40  	// We may find extra peer group members if the machines
    41  	// have been removed or their state server status removed.
    42  	// This should only happen if they had been set to non-voting
    43  	// before removal, in which case we want to remove it
    44  	// from the members list. If we find a member that's still configured
    45  	// to vote, it's an error.
    46  	// TODO There are some other possibilities
    47  	// for what to do in that case.
    48  	// 1) leave them untouched, but deal
    49  	// with others as usual "i didn't see that bit"
    50  	// 2) leave them untouched, deal with others,
    51  	// but make sure the extras aren't eligible to
    52  	// be primary.
    53  	// 3) remove them "get rid of bad rubbish"
    54  	// 4) do nothing "nothing to see here"
    55  	for _, member := range extra {
    56  		if member.Votes == nil || *member.Votes > 0 {
    57  			return nil, nil, fmt.Errorf("voting non-machine member %#v found in peer group", member)
    58  		}
    59  		changed = true
    60  	}
    61  
    62  	toRemoveVote, toAddVote, toKeep := possiblePeerGroupChanges(info, members)
    63  
    64  	// Set up initial record of machine votes. Any changes after
    65  	// this will trigger a peer group election.
    66  	machineVoting := make(map[*machine]bool)
    67  	for _, m := range info.machines {
    68  		if member := members[m]; member != nil && isVotingMember(member) {
    69  			machineVoting[m] = true
    70  		}
    71  	}
    72  	setVoting := func(m *machine, voting bool) {
    73  		setMemberVoting(members[m], voting)
    74  		machineVoting[m] = voting
    75  		changed = true
    76  	}
    77  	adjustVotes(toRemoveVote, toAddVote, setVoting)
    78  
    79  	addNewMembers(members, toKeep, maxId, setVoting)
    80  	if updateAddresses(members, info.machines) {
    81  		changed = true
    82  	}
    83  	if !changed {
    84  		return nil, nil, nil
    85  	}
    86  	var memberSet []replicaset.Member
    87  	for _, member := range members {
    88  		memberSet = append(memberSet, *member)
    89  	}
    90  	return memberSet, machineVoting, nil
    91  }
    92  
    93  func isVotingMember(member *replicaset.Member) bool {
    94  	return member.Votes == nil || *member.Votes > 0
    95  }
    96  
    97  // possiblePeerGroupChanges returns a set of slices
    98  // classifying all the existing machines according to
    99  // how their vote might move.
   100  // toRemoveVote holds machines whose vote should
   101  // be removed; toAddVote holds machines which are
   102  // ready to vote; toKeep holds machines with no desired
   103  // change to their voting status (this includes machines
   104  // that are not yet represented in the peer group).
   105  func possiblePeerGroupChanges(
   106  	info *peerGroupInfo,
   107  	members map[*machine]*replicaset.Member,
   108  ) (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  		if m.hostPort == "" {
   153  			continue
   154  		}
   155  		// TODO ensure that replicaset works correctly with IPv6 [host]:port addresses.
   156  		if m.hostPort != members[m].Address {
   157  			members[m].Address = m.hostPort
   158  			changed = true
   159  		}
   160  	}
   161  	return changed
   162  }
   163  
   164  // adjustVotes adjusts the votes of the given machines, taking
   165  // care not to let the total number of votes become even at
   166  // any time. It calls setVoting to change the voting status
   167  // of a machine.
   168  func adjustVotes(toRemoveVote, toAddVote []*machine, setVoting func(*machine, bool)) {
   169  	// Remove voting members if they can be replaced by
   170  	// candidates that are ready. This does not affect
   171  	// the total number of votes.
   172  	nreplace := min(len(toRemoveVote), len(toAddVote))
   173  	for i := 0; i < nreplace; i++ {
   174  		from := toRemoveVote[i]
   175  		to := toAddVote[i]
   176  		setVoting(from, false)
   177  		setVoting(to, true)
   178  	}
   179  	toAddVote = toAddVote[nreplace:]
   180  	toRemoveVote = toRemoveVote[nreplace:]
   181  
   182  	// At this point, one or both of toAdd or toRemove is empty, so
   183  	// we can adjust the voting-member count by an even delta,
   184  	// maintaining the invariant that the total vote count is odd.
   185  	if len(toAddVote) > 0 {
   186  		toAddVote = toAddVote[0 : len(toAddVote)-len(toAddVote)%2]
   187  		for _, m := range toAddVote {
   188  			setVoting(m, true)
   189  		}
   190  	} else {
   191  		toRemoveVote = toRemoveVote[0 : len(toRemoveVote)-len(toRemoveVote)%2]
   192  		for _, m := range toRemoveVote {
   193  			setVoting(m, false)
   194  		}
   195  	}
   196  }
   197  
   198  // addNewMembers adds new members from toKeep
   199  // to the given set of members, allocating ids from
   200  // maxId upwards. It calls setVoting to set the voting
   201  // status of each new member.
   202  func addNewMembers(
   203  	members map[*machine]*replicaset.Member,
   204  	toKeep []*machine,
   205  	maxId int,
   206  	setVoting func(*machine, bool),
   207  ) {
   208  	for _, m := range toKeep {
   209  		if members[m] == nil && m.hostPort != "" {
   210  			// This machine was not previously in the members list,
   211  			// so add it (as non-voting). We maintain the
   212  			// id manually to make it easier for tests.
   213  			maxId++
   214  			member := &replicaset.Member{
   215  				Tags: map[string]string{
   216  					"juju-machine-id": m.id,
   217  				},
   218  				Id: maxId,
   219  			}
   220  			members[m] = member
   221  			setVoting(m, false)
   222  		} else if m.hostPort == "" {
   223  			logger.Debugf("ignoring machine %q with no address", m.id)
   224  		}
   225  	}
   226  }
   227  
   228  func isReady(status replicaset.MemberStatus) bool {
   229  	return status.Healthy && (status.State == replicaset.PrimaryState ||
   230  		status.State == replicaset.SecondaryState)
   231  }
   232  
   233  func setMemberVoting(member *replicaset.Member, voting bool) {
   234  	if voting {
   235  		member.Votes = nil
   236  		member.Priority = nil
   237  	} else {
   238  		votes := 0
   239  		member.Votes = &votes
   240  		priority := 0.0
   241  		member.Priority = &priority
   242  	}
   243  }
   244  
   245  type byId []*machine
   246  
   247  func (l byId) Len() int           { return len(l) }
   248  func (l byId) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
   249  func (l byId) Less(i, j int) bool { return l[i].id < l[j].id }
   250  
   251  // membersMap returns the replica-set members inside info keyed
   252  // by machine. Any members that do not have a corresponding
   253  // machine are returned in extra.
   254  // The maximum replica-set id is returned in maxId.
   255  func (info *peerGroupInfo) membersMap() (members map[*machine]*replicaset.Member, extra []replicaset.Member, maxId int) {
   256  	maxId = -1
   257  	members = make(map[*machine]*replicaset.Member)
   258  	for _, member := range info.members {
   259  		member := member
   260  		mid, ok := member.Tags["juju-machine-id"]
   261  		var found *machine
   262  		if ok {
   263  			found = info.machines[mid]
   264  		}
   265  		if found != nil {
   266  			members[found] = &member
   267  		} else {
   268  			extra = append(extra, member)
   269  		}
   270  		if member.Id > maxId {
   271  			maxId = member.Id
   272  		}
   273  	}
   274  	return members, extra, maxId
   275  }
   276  
   277  // statusesMap returns the statuses inside info keyed by machine.
   278  // The provided members map holds the members keyed by machine,
   279  // as returned by membersMap.
   280  func (info *peerGroupInfo) statusesMap(members map[*machine]*replicaset.Member) map[*machine]replicaset.MemberStatus {
   281  	statuses := make(map[*machine]replicaset.MemberStatus)
   282  	for _, status := range info.statuses {
   283  		for m, member := range members {
   284  			if member.Id == status.Id {
   285  				statuses[m] = status
   286  				break
   287  			}
   288  		}
   289  	}
   290  	return statuses
   291  }
   292  
   293  func min(i, j int) int {
   294  	if i < j {
   295  		return i
   296  	}
   297  	return j
   298  }