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 }