github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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 []*machine
    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 state server -> 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), &machine{
   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), &machine{
   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  			machineMap := make(map[string]*machine)
   194  			for _, m := range test.machines {
   195  				c.Assert(machineMap[m.id], gc.IsNil)
   196  				machineMap[m.id] = m
   197  			}
   198  			info := &peerGroupInfo{
   199  				machines: machineMap,
   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  			sort.Sort(membersById(members))
   210  			c.Assert(members, jc.DeepEquals, test.expectMembers)
   211  			if len(members) == 0 {
   212  				continue
   213  			}
   214  			for i, m := range test.machines {
   215  				vote, votePresent := voting[m]
   216  				c.Check(votePresent, jc.IsTrue)
   217  				c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.id))
   218  			}
   219  			// Assure ourselves that the total number of desired votes is odd in
   220  			// all circumstances.
   221  			c.Assert(countVotes(members)%2, gc.Equals, 1)
   222  
   223  			// Make sure that when the members are set as
   224  			// required, that there's no further change
   225  			// if desiredPeerGroup is called again.
   226  			info.members = members
   227  			members, voting, err = desiredPeerGroup(info)
   228  			c.Assert(members, gc.IsNil)
   229  			for i, m := range test.machines {
   230  				vote, votePresent := voting[m]
   231  				c.Check(votePresent, jc.IsTrue)
   232  				c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("machine %s", m.id))
   233  			}
   234  			c.Assert(err, jc.ErrorIsNil)
   235  		}
   236  	})
   237  }
   238  
   239  func countVotes(members []replicaset.Member) int {
   240  	tot := 0
   241  	for _, m := range members {
   242  		v := 1
   243  		if m.Votes != nil {
   244  			v = *m.Votes
   245  		}
   246  		tot += v
   247  	}
   248  	return tot
   249  }
   250  
   251  func newInt(i int) *int {
   252  	return &i
   253  }
   254  
   255  func newFloat64(f float64) *float64 {
   256  	return &f
   257  }
   258  
   259  // mkMachines returns a slice of *machine based on
   260  // the given description.
   261  // Each machine in the description is white-space separated
   262  // and holds the decimal machine id followed by an optional
   263  // "v" if the machine wants a vote.
   264  func mkMachines(description string, ipVersion TestIPVersion) []*machine {
   265  	descrs := parseDescr(description)
   266  	ms := make([]*machine, len(descrs))
   267  	for i, d := range descrs {
   268  		ms[i] = &machine{
   269  			id: fmt.Sprint(d.id),
   270  			mongoHostPorts: []network.HostPort{{
   271  				Address: network.Address{
   272  					Value: fmt.Sprintf(ipVersion.machineFormatHost, d.id),
   273  					Type:  ipVersion.addressType,
   274  					Scope: network.ScopeCloudLocal,
   275  				},
   276  				Port: mongoPort,
   277  			}},
   278  			wantsVote: strings.Contains(d.flags, "v"),
   279  		}
   280  	}
   281  	return ms
   282  }
   283  
   284  func memberTag(id string) map[string]string {
   285  	return map[string]string{jujuMachineKey: id}
   286  }
   287  
   288  // mkMembers returns a slice of *replicaset.Member
   289  // based on the given description.
   290  // Each member in the description is white-space separated
   291  // and holds the decimal replica-set id optionally followed by the characters:
   292  //	- 'v' if the member is voting.
   293  // 	- 'T' if the member has no associated machine tags.
   294  // Unless the T flag is specified, the machine tag
   295  // will be the replica-set id + 10.
   296  func mkMembers(description string, ipVersion TestIPVersion) []replicaset.Member {
   297  	descrs := parseDescr(description)
   298  	ms := make([]replicaset.Member, len(descrs))
   299  	for i, d := range descrs {
   300  		machineId := d.id + 10
   301  		m := replicaset.Member{
   302  			Id:      d.id,
   303  			Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort),
   304  			Tags:    memberTag(fmt.Sprint(machineId)),
   305  		}
   306  		if !strings.Contains(d.flags, "v") {
   307  			m.Priority = newFloat64(0)
   308  			m.Votes = newInt(0)
   309  		}
   310  		if strings.Contains(d.flags, "T") {
   311  			m.Tags = nil
   312  		}
   313  		ms[i] = m
   314  	}
   315  	return ms
   316  }
   317  
   318  var stateFlags = map[rune]replicaset.MemberState{
   319  	'p': replicaset.PrimaryState,
   320  	's': replicaset.SecondaryState,
   321  }
   322  
   323  // mkStatuses returns a slice of *replicaset.Member
   324  // based on the given description.
   325  // Each member in the description is white-space separated
   326  // and holds the decimal replica-set id optionally followed by the
   327  // characters:
   328  // 	- 'H' if the instance is not healthy.
   329  //	- 'p' if the instance is in PrimaryState
   330  //	- 's' if the instance is in SecondaryState
   331  func mkStatuses(description string, ipVersion TestIPVersion) []replicaset.MemberStatus {
   332  	descrs := parseDescr(description)
   333  	ss := make([]replicaset.MemberStatus, len(descrs))
   334  	for i, d := range descrs {
   335  		machineId := d.id + 10
   336  		s := replicaset.MemberStatus{
   337  			Id:      d.id,
   338  			Address: fmt.Sprintf(ipVersion.formatHostPort, machineId, mongoPort),
   339  			Healthy: !strings.Contains(d.flags, "H"),
   340  			State:   replicaset.UnknownState,
   341  		}
   342  		for _, r := range d.flags {
   343  			if state, ok := stateFlags[r]; ok {
   344  				s.State = state
   345  			}
   346  		}
   347  		ss[i] = s
   348  	}
   349  	return ss
   350  }
   351  
   352  type descr struct {
   353  	id    int
   354  	flags string
   355  }
   356  
   357  func isNotDigit(r rune) bool {
   358  	return r < '0' || r > '9'
   359  }
   360  
   361  var parseDescrTests = []struct {
   362  	descr  string
   363  	expect []descr
   364  }{{
   365  	descr:  "",
   366  	expect: []descr{},
   367  }, {
   368  	descr:  "0",
   369  	expect: []descr{{id: 0}},
   370  }, {
   371  	descr:  "1foo",
   372  	expect: []descr{{id: 1, flags: "foo"}},
   373  }, {
   374  	descr: "10c  5 6443arble ",
   375  	expect: []descr{{
   376  		id:    10,
   377  		flags: "c",
   378  	}, {
   379  		id: 5,
   380  	}, {
   381  		id:    6443,
   382  		flags: "arble",
   383  	}},
   384  }}
   385  
   386  func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) {
   387  	for i, test := range parseDescrTests {
   388  		c.Logf("test %d. %q", i, test.descr)
   389  		c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect)
   390  	}
   391  }
   392  
   393  // parseDescr parses white-space separated fields of the form
   394  // <id><flags> into descr structures.
   395  func parseDescr(s string) []descr {
   396  	fields := strings.Fields(s)
   397  	descrs := make([]descr, len(fields))
   398  	for i, field := range fields {
   399  		d := &descrs[i]
   400  		i := strings.IndexFunc(field, isNotDigit)
   401  		if i == -1 {
   402  			i = len(field)
   403  		}
   404  		id, err := strconv.Atoi(field[0:i])
   405  		if err != nil {
   406  			panic(fmt.Errorf("bad field %q", field))
   407  		}
   408  		d.id = id
   409  		d.flags = field[i:]
   410  	}
   411  	return descrs
   412  }
   413  
   414  func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) {
   415  	c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{})
   416  	// Avoid mutating the obtained slice: because it's usually retrieved
   417  	// directly from the memberWatcher voyeur.Value,
   418  	// mutation can cause races.
   419  	obtainedMembers := deepCopy(obtained).([]replicaset.Member)
   420  	sort.Sort(membersById(obtainedMembers))
   421  	sort.Sort(membersById(expected))
   422  	c.Assert(obtainedMembers, jc.DeepEquals, expected)
   423  }
   424  
   425  type membersById []replicaset.Member
   426  
   427  func (l membersById) Len() int           { return len(l) }
   428  func (l membersById) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
   429  func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id }
   430  
   431  // AssertAPIHostPorts asserts of two sets of network.HostPort slices are the same.
   432  func AssertAPIHostPorts(c *gc.C, got, want [][]network.HostPort) {
   433  	c.Assert(got, gc.HasLen, len(want))
   434  	sort.Sort(hostPortSliceByHostPort(got))
   435  	sort.Sort(hostPortSliceByHostPort(want))
   436  	c.Assert(got, gc.DeepEquals, want)
   437  }
   438  
   439  type hostPortSliceByHostPort [][]network.HostPort
   440  
   441  func (h hostPortSliceByHostPort) Len() int      { return len(h) }
   442  func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
   443  func (h hostPortSliceByHostPort) Less(i, j int) bool {
   444  	a, b := h[i], h[j]
   445  	if len(a) != len(b) {
   446  		return len(a) < len(b)
   447  	}
   448  	for i := range a {
   449  		av, bv := a[i], b[i]
   450  		if av.Value != bv.Value {
   451  			return av.Value < bv.Value
   452  		}
   453  		if av.Port != bv.Port {
   454  			return av.Port < bv.Port
   455  		}
   456  	}
   457  	return false
   458  }