github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/peergrouper/desired_test.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  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/juju/replicaset"
    13  	jc "github.com/juju/testing/checkers"
    14  	gc "gopkg.in/check.v1"
    15  
    16  	"github.com/juju/juju/network"
    17  	"github.com/juju/juju/testing"
    18  )
    19  
    20  type desiredPeerGroupSuite struct {
    21  	testing.BaseSuite
    22  }
    23  
    24  var _ = gc.Suite(&desiredPeerGroupSuite{})
    25  
    26  const (
    27  	mongoPort = 1234
    28  	apiPort   = 5678
    29  )
    30  
    31  type desiredPeerGroupTest struct {
    32  	about    string
    33  	machines []*machineTracker
    34  	statuses []replicaset.MemberStatus
    35  	members  []replicaset.Member
    36  
    37  	expectMembers []replicaset.Member
    38  	expectVoting  []bool
    39  	expectErr     string
    40  }
    41  
    42  func desiredPeerGroupTests(ipVersion TestIPVersion) []desiredPeerGroupTest {
    43  	return []desiredPeerGroupTest{
    44  		{
    45  			// Note that this should never happen - mongo
    46  			// should always be bootstrapped with at least a single
    47  			// member in its member-set.
    48  			about:     "no members - error",
    49  			expectErr: "current member set is empty",
    50  		}, {
    51  			about:    "one machine, two more proposed members",
    52  			machines: mkMachines("10v 11v 12v", ipVersion),
    53  			statuses: mkStatuses("0p", ipVersion),
    54  			members:  mkMembers("0v", ipVersion),
    55  
    56  			expectMembers: mkMembers("0v 1 2", ipVersion),
    57  			expectVoting:  []bool{true, false, false},
    58  		}, {
    59  			about:         "single machine, no change",
    60  			machines:      mkMachines("11v", ipVersion),
    61  			members:       mkMembers("1v", ipVersion),
    62  			statuses:      mkStatuses("1p", ipVersion),
    63  			expectVoting:  []bool{true},
    64  			expectMembers: nil,
    65  		}, {
    66  			about:        "extra member with nil Vote",
    67  			machines:     mkMachines("11v", ipVersion),
    68  			members:      mkMembers("1v 2v", ipVersion),
    69  			statuses:     mkStatuses("1p 2s", ipVersion),
    70  			expectVoting: []bool{true},
    71  			expectErr:    "voting non-machine member.* found in peer group",
    72  		}, {
    73  			about:    "extra member with >1 votes",
    74  			machines: mkMachines("11v", ipVersion),
    75  			members: append(mkMembers("1v", ipVersion), replicaset.Member{
    76  				Id:      2,
    77  				Votes:   newInt(2),
    78  				Address: fmt.Sprintf(ipVersion.formatHostPort, 12, mongoPort),
    79  			}),
    80  			statuses:     mkStatuses("1p 2s", ipVersion),
    81  			expectVoting: []bool{true},
    82  			expectErr:    "voting non-machine member.* found in peer group",
    83  		}, {
    84  			about:         "new machine with no associated member",
    85  			machines:      mkMachines("11v 12v", ipVersion),
    86  			members:       mkMembers("1v", ipVersion),
    87  			statuses:      mkStatuses("1p", ipVersion),
    88  			expectVoting:  []bool{true, false},
    89  			expectMembers: mkMembers("1v 2", ipVersion),
    90  		}, {
    91  			about:         "one machine has become ready to vote (-> no change)",
    92  			machines:      mkMachines("11v 12v", ipVersion),
    93  			members:       mkMembers("1v 2", ipVersion),
    94  			statuses:      mkStatuses("1p 2s", ipVersion),
    95  			expectVoting:  []bool{true, false},
    96  			expectMembers: nil,
    97  		}, {
    98  			about:         "two machines have become ready to vote (-> added)",
    99  			machines:      mkMachines("11v 12v 13v", ipVersion),
   100  			members:       mkMembers("1v 2 3", ipVersion),
   101  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   102  			expectVoting:  []bool{true, true, true},
   103  			expectMembers: mkMembers("1v 2v 3v", ipVersion),
   104  		}, {
   105  			about:         "two machines have become ready to vote but one is not healthy (-> no change)",
   106  			machines:      mkMachines("11v 12v 13v", ipVersion),
   107  			members:       mkMembers("1v 2 3", ipVersion),
   108  			statuses:      mkStatuses("1p 2s 3sH", ipVersion),
   109  			expectVoting:  []bool{true, false, false},
   110  			expectMembers: nil,
   111  		}, {
   112  			about:         "three machines have become ready to vote (-> 2 added)",
   113  			machines:      mkMachines("11v 12v 13v 14v", ipVersion),
   114  			members:       mkMembers("1v 2 3 4", ipVersion),
   115  			statuses:      mkStatuses("1p 2s 3s 4s", ipVersion),
   116  			expectVoting:  []bool{true, true, true, false},
   117  			expectMembers: mkMembers("1v 2v 3v 4", ipVersion),
   118  		}, {
   119  			about:         "one machine ready to lose vote with no others -> no change",
   120  			machines:      mkMachines("11", ipVersion),
   121  			members:       mkMembers("1v", ipVersion),
   122  			statuses:      mkStatuses("1p", ipVersion),
   123  			expectVoting:  []bool{true},
   124  			expectMembers: nil,
   125  		}, {
   126  			about:         "two machines ready to lose vote -> votes removed",
   127  			machines:      mkMachines("11 12v 13", ipVersion),
   128  			members:       mkMembers("1v 2v 3v", ipVersion),
   129  			statuses:      mkStatuses("1p 2p 3p", ipVersion),
   130  			expectVoting:  []bool{false, true, false},
   131  			expectMembers: mkMembers("1 2v 3", ipVersion),
   132  		}, {
   133  			about:         "machines removed as controller -> removed from members",
   134  			machines:      mkMachines("11v", ipVersion),
   135  			members:       mkMembers("1v 2 3", ipVersion),
   136  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   137  			expectVoting:  []bool{true},
   138  			expectMembers: mkMembers("1v", ipVersion),
   139  		}, {
   140  			about:         "a candidate can take the vote of a non-candidate when they're ready",
   141  			machines:      mkMachines("11v 12v 13 14v", ipVersion),
   142  			members:       mkMembers("1v 2v 3v 4", ipVersion),
   143  			statuses:      mkStatuses("1p 2s 3s 4s", ipVersion),
   144  			expectVoting:  []bool{true, true, false, true},
   145  			expectMembers: mkMembers("1v 2v 3 4v", ipVersion),
   146  		}, {
   147  			about:         "several candidates can take non-candidates' votes",
   148  			machines:      mkMachines("11v 12v 13 14 15 16v 17v 18v", ipVersion),
   149  			members:       mkMembers("1v 2v 3v 4v 5v 6 7 8", ipVersion),
   150  			statuses:      mkStatuses("1p 2s 3s 4s 5s 6s 7s 8s", ipVersion),
   151  			expectVoting:  []bool{true, true, false, false, false, true, true, true},
   152  			expectMembers: mkMembers("1v 2v 3 4 5 6v 7v 8v", ipVersion),
   153  		}, {
   154  			about: "a changed machine address should propagate to the members",
   155  			machines: append(mkMachines("11v 12v", ipVersion), &machineTracker{
   156  				id:        "13",
   157  				wantsVote: true,
   158  				mongoHostPorts: []network.HostPort{{
   159  					Address: network.Address{
   160  						Value: ipVersion.extraHost,
   161  						Type:  ipVersion.addressType,
   162  						Scope: network.ScopeCloudLocal,
   163  					},
   164  					Port: 1234,
   165  				}},
   166  			}),
   167  			statuses:     mkStatuses("1s 2p 3p", ipVersion),
   168  			members:      mkMembers("1v 2v 3v", ipVersion),
   169  			expectVoting: []bool{true, true, true},
   170  			expectMembers: append(mkMembers("1v 2v", ipVersion), replicaset.Member{
   171  				Id:      3,
   172  				Address: ipVersion.extraAddress,
   173  				Tags:    memberTag("13"),
   174  			}),
   175  		}, {
   176  			about: "a machine's address is ignored if it changes to empty",
   177  			machines: append(mkMachines("11v 12v", ipVersion), &machineTracker{
   178  				id:             "13",
   179  				wantsVote:      true,
   180  				mongoHostPorts: nil,
   181  			}),
   182  			statuses:      mkStatuses("1s 2p 3p", ipVersion),
   183  			members:       mkMembers("1v 2v 3v", ipVersion),
   184  			expectVoting:  []bool{true, true, true},
   185  			expectMembers: nil,
   186  		}}
   187  }
   188  
   189  func (*desiredPeerGroupSuite) TestDesiredPeerGroup(c *gc.C) {
   190  	DoTestForIPv4AndIPv6(func(ipVersion TestIPVersion) {
   191  		for i, test := range desiredPeerGroupTests(ipVersion) {
   192  			c.Logf("\ntest %d: %s", i, test.about)
   193  			trackerMap := make(map[string]*machineTracker)
   194  			for _, m := range test.machines {
   195  				c.Assert(trackerMap[m.Id()], gc.IsNil)
   196  				trackerMap[m.Id()] = m
   197  			}
   198  			info := &peerGroupInfo{
   199  				machineTrackers: trackerMap,
   200  				statuses:        test.statuses,
   201  				members:         test.members,
   202  			}
   203  			members, voting, err := desiredPeerGroup(info)
   204  			if test.expectErr != "" {
   205  				c.Assert(err, gc.ErrorMatches, test.expectErr)
   206  				c.Assert(members, gc.IsNil)
   207  				continue
   208  			}
   209  			c.Assert(err, jc.ErrorIsNil)
   210  
   211  			sort.Sort(membersById(members))
   212  			c.Assert(members, jc.DeepEquals, test.expectMembers)
   213  			if len(members) == 0 {
   214  				continue
   215  			}
   216  			for i, m := range test.machines {
   217  				vote, votePresent := voting[m]
   218  				c.Check(votePresent, jc.IsTrue)
   219  				c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.Id()))
   220  			}
   221  			// Assure ourselves that the total number of desired votes is odd in
   222  			// all circumstances.
   223  			c.Assert(countVotes(members)%2, gc.Equals, 1)
   224  
   225  			// Make sure that when the members are set as
   226  			// required, that there's no further change
   227  			// if desiredPeerGroup is called again.
   228  			info.members = members
   229  			members, voting, err = desiredPeerGroup(info)
   230  			c.Assert(members, gc.IsNil)
   231  			for i, m := range test.machines {
   232  				vote, votePresent := voting[m]
   233  				c.Check(votePresent, jc.IsTrue)
   234  				c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.Id()))
   235  			}
   236  			c.Assert(err, jc.ErrorIsNil)
   237  		}
   238  	})
   239  }
   240  
   241  func countVotes(members []replicaset.Member) int {
   242  	tot := 0
   243  	for _, m := range members {
   244  		v := 1
   245  		if m.Votes != nil {
   246  			v = *m.Votes
   247  		}
   248  		tot += v
   249  	}
   250  	return tot
   251  }
   252  
   253  func newInt(i int) *int {
   254  	return &i
   255  }
   256  
   257  func newFloat64(f float64) *float64 {
   258  	return &f
   259  }
   260  
   261  // mkMachines returns a slice of *machineTracker based on
   262  // the given description.
   263  // Each machine in the description is white-space separated
   264  // and holds the decimal machine id followed by an optional
   265  // "v" if the machine wants a vote.
   266  func mkMachines(description string, ipVersion TestIPVersion) []*machineTracker {
   267  	descrs := parseDescr(description)
   268  	ms := make([]*machineTracker, len(descrs))
   269  	for i, d := range descrs {
   270  		ms[i] = &machineTracker{
   271  			id: fmt.Sprint(d.id),
   272  			mongoHostPorts: []network.HostPort{{
   273  				Address: network.Address{
   274  					Value: fmt.Sprintf(ipVersion.machineFormatHost, d.id),
   275  					Type:  ipVersion.addressType,
   276  					Scope: network.ScopeCloudLocal,
   277  				},
   278  				Port: mongoPort,
   279  			}},
   280  			wantsVote: strings.Contains(d.flags, "v"),
   281  		}
   282  	}
   283  	return ms
   284  }
   285  
   286  func memberTag(id string) map[string]string {
   287  	return map[string]string{jujuMachineKey: id}
   288  }
   289  
   290  // mkMembers returns a slice of *replicaset.Member
   291  // based on the given description.
   292  // Each member in the description is white-space separated
   293  // and holds the decimal replica-set id optionally followed by the characters:
   294  //	- 'v' if the member is voting.
   295  // 	- 'T' if the member has no associated machine tags.
   296  // Unless the T flag is specified, the machine tag
   297  // will be the replica-set id + 10.
   298  func mkMembers(description string, ipVersion TestIPVersion) []replicaset.Member {
   299  	descrs := parseDescr(description)
   300  	ms := make([]replicaset.Member, len(descrs))
   301  	for i, d := range descrs {
   302  		machineId := d.id + 10
   303  		m := replicaset.Member{
   304  			Id:      d.id,
   305  			Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort),
   306  			Tags:    memberTag(fmt.Sprint(machineId)),
   307  		}
   308  		if !strings.Contains(d.flags, "v") {
   309  			m.Priority = newFloat64(0)
   310  			m.Votes = newInt(0)
   311  		}
   312  		if strings.Contains(d.flags, "T") {
   313  			m.Tags = nil
   314  		}
   315  		ms[i] = m
   316  	}
   317  	return ms
   318  }
   319  
   320  var stateFlags = map[rune]replicaset.MemberState{
   321  	'p': replicaset.PrimaryState,
   322  	's': replicaset.SecondaryState,
   323  }
   324  
   325  // mkStatuses returns a slice of *replicaset.Member
   326  // based on the given description.
   327  // Each member in the description is white-space separated
   328  // and holds the decimal replica-set id optionally followed by the
   329  // characters:
   330  // 	- 'H' if the instance is not healthy.
   331  //	- 'p' if the instance is in PrimaryState
   332  //	- 's' if the instance is in SecondaryState
   333  func mkStatuses(description string, ipVersion TestIPVersion) []replicaset.MemberStatus {
   334  	descrs := parseDescr(description)
   335  	ss := make([]replicaset.MemberStatus, len(descrs))
   336  	for i, d := range descrs {
   337  		machineId := d.id + 10
   338  		s := replicaset.MemberStatus{
   339  			Id:      d.id,
   340  			Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort),
   341  			Healthy: !strings.Contains(d.flags, "H"),
   342  			State:   replicaset.UnknownState,
   343  		}
   344  		for _, r := range d.flags {
   345  			if state, ok := stateFlags[r]; ok {
   346  				s.State = state
   347  			}
   348  		}
   349  		ss[i] = s
   350  	}
   351  	return ss
   352  }
   353  
   354  type descr struct {
   355  	id    int
   356  	flags string
   357  }
   358  
   359  func isNotDigit(r rune) bool {
   360  	return r < '0' || r > '9'
   361  }
   362  
   363  var parseDescrTests = []struct {
   364  	descr  string
   365  	expect []descr
   366  }{{
   367  	descr:  "",
   368  	expect: []descr{},
   369  }, {
   370  	descr:  "0",
   371  	expect: []descr{{id: 0}},
   372  }, {
   373  	descr:  "1foo",
   374  	expect: []descr{{id: 1, flags: "foo"}},
   375  }, {
   376  	descr: "10c  5 6443arble ",
   377  	expect: []descr{{
   378  		id:    10,
   379  		flags: "c",
   380  	}, {
   381  		id: 5,
   382  	}, {
   383  		id:    6443,
   384  		flags: "arble",
   385  	}},
   386  }}
   387  
   388  func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) {
   389  	for i, test := range parseDescrTests {
   390  		c.Logf("test %d. %q", i, test.descr)
   391  		c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect)
   392  	}
   393  }
   394  
   395  // parseDescr parses white-space separated fields of the form
   396  // <id><flags> into descr structures.
   397  func parseDescr(s string) []descr {
   398  	fields := strings.Fields(s)
   399  	descrs := make([]descr, len(fields))
   400  	for i, field := range fields {
   401  		d := &descrs[i]
   402  		i := strings.IndexFunc(field, isNotDigit)
   403  		if i == -1 {
   404  			i = len(field)
   405  		}
   406  		id, err := strconv.Atoi(field[0:i])
   407  		if err != nil {
   408  			panic(fmt.Errorf("bad field %q", field))
   409  		}
   410  		d.id = id
   411  		d.flags = field[i:]
   412  	}
   413  	return descrs
   414  }
   415  
   416  func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) {
   417  	c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{})
   418  	// Avoid mutating the obtained slice: because it's usually retrieved
   419  	// directly from the memberWatcher voyeur.Value,
   420  	// mutation can cause races.
   421  	obtainedMembers := deepCopy(obtained).([]replicaset.Member)
   422  	sort.Sort(membersById(obtainedMembers))
   423  	sort.Sort(membersById(expected))
   424  	c.Assert(obtainedMembers, jc.DeepEquals, expected)
   425  }
   426  
   427  type membersById []replicaset.Member
   428  
   429  func (l membersById) Len() int           { return len(l) }
   430  func (l membersById) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
   431  func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id }
   432  
   433  // AssertAPIHostPorts asserts of two sets of network.HostPort slices are the same.
   434  func AssertAPIHostPorts(c *gc.C, got, want [][]network.HostPort) {
   435  	c.Assert(got, gc.HasLen, len(want))
   436  	sort.Sort(hostPortSliceByHostPort(got))
   437  	sort.Sort(hostPortSliceByHostPort(want))
   438  	c.Assert(got, gc.DeepEquals, want)
   439  }
   440  
   441  type hostPortSliceByHostPort [][]network.HostPort
   442  
   443  func (h hostPortSliceByHostPort) Len() int      { return len(h) }
   444  func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
   445  func (h hostPortSliceByHostPort) Less(i, j int) bool {
   446  	a, b := h[i], h[j]
   447  	if len(a) != len(b) {
   448  		return len(a) < len(b)
   449  	}
   450  	for i := range a {
   451  		av, bv := a[i], b[i]
   452  		if av.Value != bv.Value {
   453  			return av.Value < bv.Value
   454  		}
   455  		if av.Port != bv.Port {
   456  			return av.Port < bv.Port
   457  		}
   458  	}
   459  	return false
   460  }