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 }