github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/raft/raftbackstop/worker_test.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package raftbackstop_test 5 6 import ( 7 "bytes" 8 "sync" 9 "time" 10 11 "github.com/hashicorp/go-msgpack/codec" 12 "github.com/hashicorp/raft" 13 "github.com/juju/loggo" 14 "github.com/juju/pubsub" 15 "github.com/juju/testing" 16 jc "github.com/juju/testing/checkers" 17 gc "gopkg.in/check.v1" 18 "gopkg.in/juju/names.v2" 19 "gopkg.in/juju/worker.v1" 20 "gopkg.in/juju/worker.v1/workertest" 21 22 "github.com/juju/juju/pubsub/apiserver" 23 "github.com/juju/juju/pubsub/centralhub" 24 coretesting "github.com/juju/juju/testing" 25 "github.com/juju/juju/worker/raft/raftbackstop" 26 ) 27 28 type workerFixture struct { 29 testing.IsolationSuite 30 raft *mockRaft 31 logStore *mockLogStore 32 hub *pubsub.StructuredHub 33 config raftbackstop.Config 34 } 35 36 func (s *workerFixture) SetUpTest(c *gc.C) { 37 s.IsolationSuite.SetUpTest(c) 38 tag := names.NewMachineTag("23") 39 s.raft = &mockRaft{} 40 s.logStore = &mockLogStore{} 41 s.hub = centralhub.New(tag) 42 s.config = raftbackstop.Config{ 43 Raft: s.raft, 44 LogStore: s.logStore, 45 Hub: s.hub, 46 LocalID: "23", 47 Logger: loggo.GetLogger("raftbackstop_test"), 48 } 49 } 50 51 type WorkerValidationSuite struct { 52 workerFixture 53 } 54 55 var _ = gc.Suite(&WorkerValidationSuite{}) 56 57 func (s *WorkerValidationSuite) TestValidateErrors(c *gc.C) { 58 type test struct { 59 f func(*raftbackstop.Config) 60 expect string 61 } 62 tests := []test{{ 63 func(cfg *raftbackstop.Config) { cfg.Raft = nil }, 64 "nil Raft not valid", 65 }, { 66 func(cfg *raftbackstop.Config) { cfg.Hub = nil }, 67 "nil Hub not valid", 68 }, { 69 func(cfg *raftbackstop.Config) { cfg.LogStore = nil }, 70 "nil LogStore not valid", 71 }, { 72 func(cfg *raftbackstop.Config) { cfg.LocalID = "" }, 73 "empty LocalID not valid", 74 }, { 75 func(cfg *raftbackstop.Config) { cfg.Logger = nil }, 76 "nil Logger not valid", 77 }} 78 for i, test := range tests { 79 c.Logf("test #%d (%s)", i, test.expect) 80 s.testValidateError(c, test.f, test.expect) 81 } 82 } 83 84 func (s *WorkerValidationSuite) testValidateError(c *gc.C, f func(*raftbackstop.Config), expect string) { 85 config := s.config 86 f(&config) 87 w, err := raftbackstop.NewWorker(config) 88 if !c.Check(err, gc.NotNil) { 89 workertest.DirtyKill(c, w) 90 return 91 } 92 c.Check(w, gc.IsNil) 93 c.Check(err, gc.ErrorMatches, expect) 94 } 95 96 type WorkerSuite struct { 97 workerFixture 98 worker worker.Worker 99 reqs chan apiserver.DetailsRequest 100 } 101 102 var _ = gc.Suite(&WorkerSuite{}) 103 104 func (s *WorkerSuite) SetUpTest(c *gc.C) { 105 s.workerFixture.SetUpTest(c) 106 s.reqs = make(chan apiserver.DetailsRequest, 10) 107 108 // Use a local variable to send to the channel in the callback, so 109 // we don't get races when a subsequent test overwrites s.reqs 110 // with a new channel. 111 reqs := s.reqs 112 unsubscribe, err := s.hub.Subscribe( 113 apiserver.DetailsRequestTopic, 114 func(topic string, req apiserver.DetailsRequest, err error) { 115 c.Check(err, jc.ErrorIsNil) 116 reqs <- req 117 }, 118 ) 119 c.Assert(err, jc.ErrorIsNil) 120 s.AddCleanup(func(c *gc.C) { unsubscribe() }) 121 122 worker, err := raftbackstop.NewWorker(s.config) 123 c.Assert(err, jc.ErrorIsNil) 124 s.AddCleanup(func(c *gc.C) { 125 workertest.DirtyKill(c, worker) 126 }) 127 s.worker = worker 128 } 129 130 func (s *WorkerSuite) TestCleanKill(c *gc.C) { 131 workertest.CleanKill(c, s.worker) 132 } 133 134 func (s *WorkerSuite) TestRequestsDetails(c *gc.C) { 135 // The worker is started in SetUpTest. 136 select { 137 case req := <-s.reqs: 138 c.Assert(req, gc.Equals, apiserver.DetailsRequest{ 139 Requester: "raft-backstop", 140 LocalOnly: true, 141 }) 142 case <-time.After(coretesting.LongWait): 143 c.Fatalf("timed out waiting for details request") 144 } 145 } 146 147 func (s *WorkerSuite) findStoreLogCalls() []*raft.Log { 148 var results []*raft.Log 149 for _, call := range s.logStore.Calls() { 150 if call.FuncName != "StoreLog" { 151 continue 152 } 153 results = append(results, call.Args[0].(*raft.Log)) 154 } 155 return results 156 } 157 158 func (s *WorkerSuite) assertRecovery(c *gc.C, index, term uint64, server raft.Server) { 159 for a := coretesting.LongAttempt.Start(); a.Next(); { 160 if len(s.findStoreLogCalls()) != 0 { 161 break 162 } 163 } 164 storedLogs := s.findStoreLogCalls() 165 c.Assert(storedLogs, gc.HasLen, 1) 166 log := storedLogs[0] 167 c.Assert(log.Type, gc.Equals, raft.LogConfiguration) 168 c.Assert(log.Index, gc.Equals, index) 169 c.Assert(log.Term, gc.Equals, term) 170 c.Assert(decodeConfiguration(c, log.Data), gc.DeepEquals, raft.Configuration{ 171 Servers: []raft.Server{server}, 172 }) 173 } 174 175 func (s *WorkerSuite) TestRecoversClusterOneNonvoter(c *gc.C) { 176 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 177 Servers: []raft.Server{{ 178 ID: "23", 179 Address: "address", 180 Suffrage: raft.Nonvoter, 181 }}, 182 }}) 183 // We don't care about other fields in this case. 184 s.logStore.setLastLog(&raft.Log{ 185 Index: 451, 186 Term: 66, 187 }) 188 s.publishDetails(c, map[string]string{"23": "address"}) 189 s.assertRecovery(c, 452, 66, raft.Server{ 190 ID: "23", 191 Address: "address", 192 Suffrage: raft.Voter, 193 }) 194 } 195 196 func (s *WorkerSuite) TestRecoversClusterTwoVoters(c *gc.C) { 197 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 198 Servers: []raft.Server{{ 199 ID: "23", 200 Address: "address", 201 Suffrage: raft.Voter, 202 }, { 203 ID: "100", 204 Address: "otheraddress", 205 Suffrage: raft.Voter, 206 }}, 207 }}) 208 s.logStore.setLastLog(&raft.Log{ 209 Index: 451, 210 Term: 66, 211 }) 212 s.publishDetails(c, map[string]string{"23": "address"}) 213 s.assertRecovery(c, 452, 66, raft.Server{ 214 ID: "23", 215 Address: "address", 216 Suffrage: raft.Voter, 217 }) 218 } 219 220 func (s *WorkerSuite) assertNoRecovery(c *gc.C) { 221 time.Sleep(coretesting.ShortWait) 222 c.Assert(s.findStoreLogCalls(), gc.HasLen, 0) 223 } 224 225 func (s *WorkerSuite) TestOnlyRecoversClusterOnce(c *gc.C) { 226 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 227 Servers: []raft.Server{{ 228 ID: "23", 229 Address: "address", 230 Suffrage: raft.Nonvoter, 231 }}, 232 }}) 233 // We don't care about other fields in this case. 234 s.logStore.setLastLog(&raft.Log{ 235 Index: 451, 236 Term: 66, 237 }) 238 s.publishDetails(c, map[string]string{"23": "address"}) 239 s.assertRecovery(c, 452, 66, raft.Server{ 240 ID: "23", 241 Address: "address", 242 Suffrage: raft.Voter, 243 }) 244 s.logStore.ResetCalls() 245 s.publishDetails(c, map[string]string{"23": "address"}) 246 s.assertNoRecovery(c) 247 } 248 249 func (s *WorkerSuite) TestNoRecoveryIfMultipleMachines(c *gc.C) { 250 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 251 Servers: []raft.Server{{ 252 ID: "23", 253 Address: "address", 254 Suffrage: raft.Voter, 255 }, { 256 ID: "100", 257 Address: "otheraddress", 258 Suffrage: raft.Voter, 259 }}, 260 }}) 261 s.logStore.setLastLog(&raft.Log{ 262 Index: 451, 263 Term: 66, 264 }) 265 s.publishDetails(c, map[string]string{ 266 "23": "address", 267 "100": "otheraddress", 268 }) 269 s.assertNoRecovery(c) 270 } 271 272 func (s *WorkerSuite) TestNoRecoveryIfNotInServerDetails(c *gc.C) { 273 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 274 Servers: []raft.Server{{ 275 ID: "23", 276 Address: "address", 277 Suffrage: raft.Voter, 278 }, { 279 ID: "100", 280 Address: "otheraddress", 281 Suffrage: raft.Voter, 282 }}, 283 }}) 284 s.logStore.setLastLog(&raft.Log{ 285 Index: 451, 286 Term: 66, 287 }) 288 s.publishDetails(c, map[string]string{"100": "otheraddress"}) 289 s.assertNoRecovery(c) 290 } 291 292 func (s *WorkerSuite) TestNoRecoveryIfNotInRaftConfig(c *gc.C) { 293 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 294 Servers: []raft.Server{{ 295 ID: "100", 296 Address: "otheraddress", 297 Suffrage: raft.Voter, 298 }}, 299 }}) 300 s.logStore.setLastLog(&raft.Log{ 301 Index: 451, 302 Term: 66, 303 }) 304 s.publishDetails(c, map[string]string{ 305 "23": "address", 306 }) 307 s.assertNoRecovery(c) 308 } 309 310 func (s *WorkerSuite) TestNoRecoveryIfOneRaftNodeAndVoter(c *gc.C) { 311 s.raft.setValues(raft.Follower, &mockConfigFuture{conf: raft.Configuration{ 312 Servers: []raft.Server{{ 313 ID: "23", 314 Address: "address", 315 Suffrage: raft.Voter, 316 }}, 317 }}) 318 s.logStore.setLastLog(&raft.Log{ 319 Index: 451, 320 Term: 66, 321 }) 322 s.publishDetails(c, map[string]string{"23": "address"}) 323 s.assertNoRecovery(c) 324 } 325 326 func (s *WorkerSuite) publishDetails(c *gc.C, serverAddrs map[string]string) { 327 details := makeDetails(serverAddrs) 328 received, err := s.hub.Publish(apiserver.DetailsTopic, details) 329 c.Assert(err, jc.ErrorIsNil) 330 select { 331 case <-received: 332 case <-time.After(coretesting.LongWait): 333 c.Fatal("timed out waiting for details to be received") 334 } 335 } 336 337 func makeDetails(serverInfo map[string]string) apiserver.Details { 338 servers := make(map[string]apiserver.APIServer) 339 for id, address := range serverInfo { 340 servers[id] = apiserver.APIServer{ 341 ID: id, 342 InternalAddress: address, 343 } 344 } 345 return apiserver.Details{Servers: servers} 346 } 347 348 func decodeConfiguration(c *gc.C, data []byte) (out raft.Configuration) { 349 buf := bytes.NewReader(data) 350 hd := codec.MsgpackHandle{} 351 dec := codec.NewDecoder(buf, &hd) 352 err := dec.Decode(&out) 353 c.Assert(err, jc.ErrorIsNil) 354 return out 355 } 356 357 type mockRaft struct { 358 mu sync.Mutex 359 state raft.RaftState 360 cf *mockConfigFuture 361 } 362 363 func (r *mockRaft) State() raft.RaftState { 364 r.mu.Lock() 365 defer r.mu.Unlock() 366 return r.state 367 } 368 369 func (r *mockRaft) GetConfiguration() raft.ConfigurationFuture { 370 r.mu.Lock() 371 defer r.mu.Unlock() 372 return r.cf 373 } 374 375 func (r *mockRaft) setValues(state raft.RaftState, cf *mockConfigFuture) { 376 r.mu.Lock() 377 defer r.mu.Unlock() 378 r.state = state 379 r.cf = cf 380 } 381 382 type mockConfigFuture struct { 383 raft.IndexFuture 384 testing.Stub 385 conf raft.Configuration 386 } 387 388 func (f *mockConfigFuture) Error() error { 389 f.AddCall("Error") 390 return f.NextErr() 391 } 392 393 func (f *mockConfigFuture) Configuration() raft.Configuration { 394 f.AddCall("Configuration") 395 return f.conf 396 } 397 398 type mockLogStore struct { 399 raft.LogStore 400 testing.Stub 401 mu sync.Mutex 402 lastLog raft.Log 403 } 404 405 func (s *mockLogStore) LastIndex() (uint64, error) { 406 s.mu.Lock() 407 defer s.mu.Unlock() 408 s.AddCall("LastIndex") 409 return s.lastLog.Index, s.NextErr() 410 } 411 412 func (s *mockLogStore) GetLog(index uint64, out *raft.Log) error { 413 s.AddCall("GetLog", index, out) 414 *out = s.lastLog 415 return s.NextErr() 416 } 417 418 func (s *mockLogStore) StoreLog(log *raft.Log) error { 419 s.AddCall("StoreLog", log) 420 return s.NextErr() 421 } 422 423 func (s *mockLogStore) setLastLog(lastLog *raft.Log) { 424 s.mu.Lock() 425 defer s.mu.Unlock() 426 s.lastLog = *lastLog 427 }