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