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