github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/replicaset/replicaset_test.go (about)

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