github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/replicaset/replicaset_test.go (about)

     1  package replicaset
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	jc "github.com/juju/testing/checkers"
     9  	"github.com/juju/utils"
    10  	"labix.org/v2/mgo"
    11  	gc "launchpad.net/gocheck"
    12  
    13  	coretesting "github.com/juju/juju/testing"
    14  )
    15  
    16  const rsName = "juju"
    17  
    18  func TestPackage(t *testing.T) {
    19  	gc.TestingT(t)
    20  }
    21  
    22  type MongoSuite struct {
    23  	coretesting.BaseSuite
    24  	root *coretesting.MgoInstance
    25  }
    26  
    27  func newServer(c *gc.C) *coretesting.MgoInstance {
    28  	inst := &coretesting.MgoInstance{Params: []string{"--replSet", rsName}}
    29  	err := inst.Start(true)
    30  	c.Assert(err, gc.IsNil)
    31  
    32  	session, err := inst.DialDirect()
    33  	if err != nil {
    34  		inst.Destroy()
    35  		c.Fatalf("error dialing mongo server: %v", err.Error())
    36  	}
    37  	defer session.Close()
    38  
    39  	session.SetMode(mgo.Monotonic, true)
    40  	if err = session.Ping(); err != nil {
    41  		inst.Destroy()
    42  		c.Fatalf("error pinging mongo server: %v", err.Error())
    43  	}
    44  	return inst
    45  }
    46  
    47  var _ = gc.Suite(&MongoSuite{})
    48  
    49  func (s *MongoSuite) SetUpTest(c *gc.C) {
    50  	s.BaseSuite.SetUpTest(c)
    51  	s.root = newServer(c)
    52  	s.dialAndTestInitiate(c)
    53  }
    54  
    55  func (s *MongoSuite) TearDownTest(c *gc.C) {
    56  	s.root.Destroy()
    57  	s.BaseSuite.TearDownTest(c)
    58  }
    59  
    60  var initialTags = map[string]string{"foo": "bar"}
    61  
    62  func (s *MongoSuite) dialAndTestInitiate(c *gc.C) {
    63  	session := s.root.MustDialDirect()
    64  	defer session.Close()
    65  
    66  	mode := session.Mode()
    67  	err := Initiate(session, s.root.Addr(), rsName, initialTags)
    68  	c.Assert(err, gc.IsNil)
    69  
    70  	// make sure we haven't messed with the session's mode
    71  	c.Assert(session.Mode(), gc.Equals, mode)
    72  
    73  	// Ids start at 1 for us, so we can differentiate between set and unset
    74  	expectedMembers := []Member{Member{Id: 1, Address: s.root.Addr(), Tags: initialTags}}
    75  
    76  	// need to set mode to strong so that we wait for the write to succeed
    77  	// before reading and thus ensure that we're getting consistent reads.
    78  	session.SetMode(mgo.Strong, false)
    79  
    80  	mems, err := CurrentMembers(session)
    81  	c.Assert(err, gc.IsNil)
    82  	c.Assert(mems, jc.DeepEquals, expectedMembers)
    83  
    84  	// now add some data so we get a more real-life test
    85  	loadData(session, c)
    86  }
    87  
    88  func loadData(session *mgo.Session, c *gc.C) {
    89  	type foo struct {
    90  		Name    string
    91  		Address string
    92  		Count   int
    93  	}
    94  
    95  	for col := 0; col < 10; col++ {
    96  		foos := make([]foo, 10000)
    97  		for n := range foos {
    98  			foos[n] = foo{
    99  				Name:    fmt.Sprintf("name_%d_%d", col, n),
   100  				Address: fmt.Sprintf("address_%d_%d", col, n),
   101  				Count:   n * (col + 1),
   102  			}
   103  		}
   104  
   105  		err := session.DB("testing").C(fmt.Sprintf("data%d", col)).Insert(foos)
   106  		c.Assert(err, gc.IsNil)
   107  	}
   108  }
   109  
   110  func attemptLoop(c *gc.C, strategy utils.AttemptStrategy, desc string, f func() error) {
   111  	var err error
   112  	start := time.Now()
   113  	attemptCount := 0
   114  	for attempt := strategy.Start(); attempt.Next(); {
   115  		attemptCount += 1
   116  		if err = f(); err == nil || !attempt.HasNext() {
   117  			break
   118  		}
   119  		c.Logf("%s failed: %v", desc, err)
   120  	}
   121  	c.Logf("%s: %d attempts in %s", desc, attemptCount, time.Since(start))
   122  	c.Assert(err, gc.IsNil)
   123  }
   124  
   125  func (s *MongoSuite) TestAddRemoveSet(c *gc.C) {
   126  	session := s.root.MustDial()
   127  	defer session.Close()
   128  
   129  	members := make([]Member, 0, 5)
   130  
   131  	// Add should be idempotent, so re-adding root here shouldn't result in
   132  	// two copies of root in the replica set
   133  	members = append(members, Member{Address: s.root.Addr(), Tags: initialTags})
   134  
   135  	instances := make([]*coretesting.MgoInstance, 5)
   136  	instances[0] = s.root
   137  	for i := 1; i < len(instances); i++ {
   138  		inst := newServer(c)
   139  		instances[i] = inst
   140  		defer inst.Destroy()
   141  		defer func() {
   142  			err := Remove(session, inst.Addr())
   143  			c.Assert(err, gc.IsNil)
   144  		}()
   145  		key := fmt.Sprintf("key%d", i)
   146  		val := fmt.Sprintf("val%d", i)
   147  		tags := map[string]string{key: val}
   148  		members = append(members, Member{Address: inst.Addr(), Tags: tags})
   149  	}
   150  
   151  	// We allow for up to 2m per operation, since Add, Set, etc. call
   152  	// replSetReconfig which may cause primary renegotiation. According
   153  	// to the Mongo docs, "typically this is 10-20 seconds, but could be
   154  	// as long as a minute or more."
   155  	//
   156  	// Note that the delay is set at 500ms to cater for relatively quick
   157  	// operations without thrashing on those that take longer.
   158  	strategy := utils.AttemptStrategy{Total: time.Minute * 2, Delay: time.Millisecond * 500}
   159  	attemptLoop(c, strategy, "Add()", func() error {
   160  		return Add(session, members...)
   161  	})
   162  
   163  	expectedMembers := make([]Member, len(members))
   164  	for i, m := range members {
   165  		// Ids should start at 1 (for the root) and go up
   166  		m.Id = i + 1
   167  		expectedMembers[i] = m
   168  	}
   169  
   170  	var cfg *Config
   171  	attemptLoop(c, strategy, "CurrentConfig()", func() error {
   172  		var err error
   173  		cfg, err = CurrentConfig(session)
   174  		return err
   175  	})
   176  	c.Assert(cfg.Name, gc.Equals, rsName)
   177  	// 2 since we already changed it once
   178  	c.Assert(cfg.Version, gc.Equals, 2)
   179  
   180  	mems := cfg.Members
   181  	c.Assert(mems, jc.DeepEquals, expectedMembers)
   182  
   183  	// Now remove the last two Members...
   184  	attemptLoop(c, strategy, "Remove()", func() error {
   185  		return Remove(session, members[3].Address, members[4].Address)
   186  	})
   187  	expectedMembers = expectedMembers[0:3]
   188  
   189  	// ... and confirm that CurrentMembers reflects the removal.
   190  	attemptLoop(c, strategy, "CurrentMembers()", func() error {
   191  		var err error
   192  		mems, err = CurrentMembers(session)
   193  		return err
   194  	})
   195  	c.Assert(mems, jc.DeepEquals, expectedMembers)
   196  
   197  	// now let's mix it up and set the new members to a mix of the previous
   198  	// plus the new arbiter
   199  	mems = []Member{members[3], mems[2], mems[0], members[4]}
   200  	attemptLoop(c, strategy, "Set()", func() error {
   201  		err := Set(session, mems)
   202  		if err != nil {
   203  			c.Logf("current session mode: %v", session.Mode())
   204  			session.Refresh()
   205  		}
   206  		return err
   207  	})
   208  
   209  	attemptLoop(c, strategy, "Ping()", func() error {
   210  		// can dial whichever replica address here, mongo will figure it out
   211  		if session != nil {
   212  			session.Close()
   213  		}
   214  		session = instances[0].MustDialDirect()
   215  		return session.Ping()
   216  	})
   217  
   218  	// any new members will get an id of max(other_ids...)+1
   219  	expectedMembers = []Member{members[3], expectedMembers[2], expectedMembers[0], members[4]}
   220  	expectedMembers[0].Id = 4
   221  	expectedMembers[3].Id = 5
   222  
   223  	attemptLoop(c, strategy, "CurrentMembers()", func() error {
   224  		var err error
   225  		mems, err = CurrentMembers(session)
   226  		return err
   227  	})
   228  	c.Assert(mems, jc.DeepEquals, expectedMembers)
   229  }
   230  
   231  func (s *MongoSuite) TestIsMaster(c *gc.C) {
   232  	session := s.root.MustDial()
   233  	defer session.Close()
   234  
   235  	expected := IsMasterResults{
   236  		// The following fields hold information about the specific mongodb node.
   237  		IsMaster:  true,
   238  		Secondary: false,
   239  		Arbiter:   false,
   240  		Address:   s.root.Addr(),
   241  		LocalTime: time.Time{},
   242  
   243  		// The following fields hold information about the replica set.
   244  		ReplicaSetName: rsName,
   245  		Addresses:      []string{s.root.Addr()},
   246  		Arbiters:       nil,
   247  		PrimaryAddress: s.root.Addr(),
   248  	}
   249  
   250  	res, err := IsMaster(session)
   251  	c.Assert(err, gc.IsNil)
   252  	c.Check(closeEnough(res.LocalTime, time.Now()), gc.Equals, true)
   253  	res.LocalTime = time.Time{}
   254  	c.Check(*res, jc.DeepEquals, expected)
   255  }
   256  
   257  func (s *MongoSuite) TestMasterHostPort(c *gc.C) {
   258  	session := s.root.MustDial()
   259  	defer session.Close()
   260  
   261  	expected := s.root.Addr()
   262  	result, err := MasterHostPort(session)
   263  
   264  	c.Logf("TestMasterHostPort expected: %v, got: %v", expected, result)
   265  	c.Assert(err, gc.IsNil)
   266  	c.Assert(result, gc.Equals, expected)
   267  }
   268  
   269  func (s *MongoSuite) TestMasterHostPortOnUnconfiguredReplicaSet(c *gc.C) {
   270  	inst := &coretesting.MgoInstance{}
   271  	err := inst.Start(true)
   272  	c.Assert(err, gc.IsNil)
   273  	defer inst.Destroy()
   274  	session := inst.MustDial()
   275  	hp, err := MasterHostPort(session)
   276  	c.Assert(err, gc.Equals, ErrMasterNotConfigured)
   277  	c.Assert(hp, gc.Equals, "")
   278  }
   279  
   280  func (s *MongoSuite) TestCurrentStatus(c *gc.C) {
   281  	session := s.root.MustDial()
   282  	defer session.Close()
   283  
   284  	inst1 := newServer(c)
   285  	defer inst1.Destroy()
   286  	defer Remove(session, inst1.Addr())
   287  
   288  	inst2 := newServer(c)
   289  	defer inst2.Destroy()
   290  	defer Remove(session, inst2.Addr())
   291  
   292  	var err error
   293  	strategy := utils.AttemptStrategy{Total: time.Minute * 2, Delay: time.Millisecond * 500}
   294  	attempt := strategy.Start()
   295  	for attempt.Next() {
   296  		err = Add(session, Member{Address: inst1.Addr()}, Member{Address: inst2.Addr()})
   297  		if err == nil || !attempt.HasNext() {
   298  			break
   299  		}
   300  	}
   301  	c.Assert(err, gc.IsNil)
   302  
   303  	expected := &Status{
   304  		Name: rsName,
   305  		Members: []MemberStatus{{
   306  			Id:      1,
   307  			Address: s.root.Addr(),
   308  			Self:    true,
   309  			ErrMsg:  "",
   310  			Healthy: true,
   311  			State:   PrimaryState,
   312  		}, {
   313  			Id:      2,
   314  			Address: inst1.Addr(),
   315  			Self:    false,
   316  			ErrMsg:  "",
   317  			Healthy: true,
   318  			State:   SecondaryState,
   319  		}, {
   320  			Id:      3,
   321  			Address: inst2.Addr(),
   322  			Self:    false,
   323  			ErrMsg:  "",
   324  			Healthy: true,
   325  			State:   SecondaryState,
   326  		}},
   327  	}
   328  
   329  	strategy.Total = time.Second * 90
   330  	attempt = strategy.Start()
   331  	var res *Status
   332  	for attempt.Next() {
   333  		var err error
   334  		res, err = CurrentStatus(session)
   335  		if err != nil {
   336  			if !attempt.HasNext() {
   337  				c.Errorf("Couldn't get status before timeout, got err: %v", err)
   338  				return
   339  			} else {
   340  				// try again
   341  				continue
   342  			}
   343  		}
   344  
   345  		if res.Members[0].State == PrimaryState &&
   346  			res.Members[1].State == SecondaryState &&
   347  			res.Members[2].State == SecondaryState {
   348  			break
   349  		}
   350  		if !attempt.HasNext() {
   351  			c.Errorf("Servers did not get into final state before timeout.  Status: %#v", res)
   352  			return
   353  		}
   354  	}
   355  
   356  	for x, _ := range res.Members {
   357  		// non-empty uptime and ping
   358  		c.Check(res.Members[x].Uptime, gc.Not(gc.Equals), 0)
   359  
   360  		// ping is always going to be zero since we're on localhost
   361  		// so we can't really test it right now
   362  
   363  		// now overwrite Uptime so it won't throw off DeepEquals
   364  		res.Members[x].Uptime = 0
   365  	}
   366  	c.Check(res, jc.DeepEquals, expected)
   367  }
   368  
   369  func closeEnough(expected, obtained time.Time) bool {
   370  	t := obtained.Sub(expected)
   371  	return (-500*time.Millisecond) < t && t < (500*time.Millisecond)
   372  }