github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/multiwatcher/worker_internal_test.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package multiwatcher 5 6 import ( 7 "fmt" 8 9 "github.com/juju/clock" 10 "github.com/juju/loggo" 11 "github.com/juju/testing" 12 jc "github.com/juju/testing/checkers" 13 gc "gopkg.in/check.v1" 14 15 "github.com/juju/juju/core/multiwatcher" 16 "github.com/juju/juju/worker/multiwatcher/testbacking" 17 ) 18 19 type workerSuite struct { 20 testing.IsolationSuite 21 } 22 23 var _ = gc.Suite(&workerSuite{}) 24 25 func (*workerSuite) TestHandle(c *gc.C) { 26 sm := &Worker{ 27 config: Config{ 28 Clock: clock.WallClock, 29 Logger: loggo.GetLogger("test.worker"), 30 Backing: testbacking.New(nil), 31 }, 32 request: make(chan *request), 33 waiting: make(map[*Watcher]*request), 34 store: multiwatcher.NewStore(loggo.GetLogger("test.store")), 35 } 36 37 // Add request from first watcher. 38 w0 := sm.newWatcher(nil) 39 req0 := &request{ 40 watcher: w0, 41 reply: make(chan bool, 1), 42 } 43 sm.handle(req0) 44 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 45 w0: {req0}, 46 }) 47 48 // Add second request from first watcher. 49 req1 := &request{ 50 watcher: w0, 51 reply: make(chan bool, 1), 52 } 53 sm.handle(req1) 54 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 55 w0: {req1, req0}, 56 }) 57 58 // Add request from second watcher. 59 w1 := sm.newWatcher(nil) 60 req2 := &request{ 61 watcher: w1, 62 reply: make(chan bool, 1), 63 } 64 sm.handle(req2) 65 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 66 w0: {req1, req0}, 67 w1: {req2}, 68 }) 69 70 // Stop first watcher. 71 sm.handle(&request{ 72 watcher: w0, 73 }) 74 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 75 w1: {req2}, 76 }) 77 assertReplied(c, false, req0) 78 assertReplied(c, false, req1) 79 80 // Stop second watcher. 81 sm.handle(&request{ 82 watcher: w1, 83 }) 84 assertWaitingRequests(c, sm, nil) 85 assertReplied(c, false, req2) 86 } 87 88 func (*workerSuite) TestRespondMultiple(c *gc.C) { 89 sm := &Worker{ 90 config: Config{ 91 Clock: clock.WallClock, 92 Logger: loggo.GetLogger("test.worker"), 93 Backing: testbacking.New(nil), 94 }, 95 request: make(chan *request), 96 waiting: make(map[*Watcher]*request), 97 store: multiwatcher.NewStore(loggo.GetLogger("test.store")), 98 } 99 100 sm.store.Update(&multiwatcher.MachineInfo{ID: "0"}) 101 102 // Add one request and respond. 103 // It should see the above change. 104 w0 := sm.newWatcher(nil) 105 req0 := &request{ 106 watcher: w0, 107 reply: make(chan bool, 1), 108 } 109 sm.handle(req0) 110 sm.respond() 111 assertReplied(c, true, req0) 112 c.Assert(req0.changes, gc.DeepEquals, []multiwatcher.Delta{{Entity: &multiwatcher.MachineInfo{ID: "0"}}}) 113 assertWaitingRequests(c, sm, nil) 114 115 // Add another request from the same watcher and respond. 116 // It should have no reply because nothing has changed. 117 req0 = &request{ 118 watcher: w0, 119 reply: make(chan bool, 1), 120 } 121 sm.handle(req0) 122 sm.respond() 123 assertNotReplied(c, req0) 124 125 // Add two requests from another watcher and respond. 126 // The request from the first watcher should still not 127 // be replied to, but the later of the two requests from 128 // the second watcher should get a reply. 129 w1 := sm.newWatcher(nil) 130 req1 := &request{ 131 watcher: w1, 132 reply: make(chan bool, 1), 133 } 134 sm.handle(req1) 135 req2 := &request{ 136 watcher: w1, 137 reply: make(chan bool, 1), 138 } 139 sm.handle(req2) 140 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 141 w0: {req0}, 142 w1: {req2, req1}, 143 }) 144 sm.respond() 145 assertNotReplied(c, req0) 146 assertNotReplied(c, req1) 147 assertReplied(c, true, req2) 148 c.Assert(req2.changes, gc.DeepEquals, []multiwatcher.Delta{{Entity: &multiwatcher.MachineInfo{ID: "0"}}}) 149 assertWaitingRequests(c, sm, map[*Watcher][]*request{ 150 w0: {req0}, 151 w1: {req1}, 152 }) 153 154 // Check that nothing more gets responded to if we call respond again. 155 sm.respond() 156 assertNotReplied(c, req0) 157 assertNotReplied(c, req1) 158 159 // Now make a change and check that both waiting requests 160 // get serviced. 161 sm.store.Update(&multiwatcher.MachineInfo{ID: "1"}) 162 sm.respond() 163 assertReplied(c, true, req0) 164 assertReplied(c, true, req1) 165 assertWaitingRequests(c, sm, nil) 166 167 deltas := []multiwatcher.Delta{{Entity: &multiwatcher.MachineInfo{ID: "1"}}} 168 c.Assert(req0.changes, gc.DeepEquals, deltas) 169 c.Assert(req1.changes, gc.DeepEquals, deltas) 170 } 171 172 var respondTestChanges = [...]func(store multiwatcher.Store){ 173 func(store multiwatcher.Store) { 174 store.Update(&multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0"}) 175 }, 176 func(store multiwatcher.Store) { 177 store.Update(&multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "1"}) 178 }, 179 func(store multiwatcher.Store) { 180 store.Update(&multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "2"}) 181 }, 182 func(store multiwatcher.Store) { 183 store.Remove(multiwatcher.EntityID{"machine", "uuid", "0"}) 184 }, 185 func(store multiwatcher.Store) { 186 store.Update(&multiwatcher.MachineInfo{ 187 ModelUUID: "uuid", 188 ID: "1", 189 InstanceID: "i-1", 190 }) 191 }, 192 func(store multiwatcher.Store) { 193 store.Remove(multiwatcher.EntityID{"machine", "uuid", "1"}) 194 }, 195 } 196 197 func (s *workerSuite) TestRespondResults(c *gc.C) { 198 // We test the response results for a pair of watchers by 199 // interleaving notional Next requests in all possible 200 // combinations after each change in respondTestChanges and 201 // checking that the view of the world as seen by the watchers 202 // matches the actual current state. 203 204 // We decide whether if we make a request for a given 205 // watcher by inspecting a number n - bit i of n determines whether 206 // a request will be responded to after running respondTestChanges[i]. 207 208 numCombinations := 1 << uint(len(respondTestChanges)) 209 const wcount = 2 210 ns := make([]int, wcount) 211 for ns[0] = 0; ns[0] < numCombinations; ns[0]++ { 212 for ns[1] = 0; ns[1] < numCombinations; ns[1]++ { 213 214 sm := &Worker{ 215 config: Config{ 216 Clock: clock.WallClock, 217 Logger: loggo.GetLogger("test.worker"), 218 Backing: testbacking.New(nil), 219 }, 220 request: make(chan *request), 221 waiting: make(map[*Watcher]*request), 222 store: multiwatcher.NewStore(loggo.GetLogger("test.store")), 223 } 224 225 c.Logf("test %0*b", len(respondTestChanges), ns) 226 var ( 227 ws []*Watcher 228 wstates []watcherState 229 reqs []*request 230 ) 231 for i := 0; i < wcount; i++ { 232 ws = append(ws, sm.newWatcher(nil)) 233 wstates = append(wstates, make(watcherState)) 234 reqs = append(reqs, nil) 235 } 236 // Make each change in turn, and make a request for each 237 // watcher if n and respond 238 for i, change := range respondTestChanges { 239 c.Logf("change %d", i) 240 change(sm.store) 241 needRespond := false 242 for wi, n := range ns { 243 if n&(1<<uint(i)) != 0 { 244 needRespond = true 245 if reqs[wi] == nil { 246 reqs[wi] = &request{ 247 watcher: ws[wi], 248 reply: make(chan bool, 1), 249 } 250 sm.handle(reqs[wi]) 251 } 252 } 253 } 254 if !needRespond { 255 continue 256 } 257 // Check that the expected requests are pending. 258 expectWaiting := make(map[*Watcher][]*request) 259 for wi, w := range ws { 260 if reqs[wi] != nil { 261 expectWaiting[w] = []*request{reqs[wi]} 262 } 263 } 264 assertWaitingRequests(c, sm, expectWaiting) 265 // Actually respond; then check that each watcher with 266 // an outstanding request now has an up to date view 267 // of the world. 268 sm.respond() 269 for wi, req := range reqs { 270 if req == nil { 271 continue 272 } 273 select { 274 case ok := <-req.reply: 275 c.Assert(ok, jc.IsTrue) 276 c.Assert(len(req.changes) > 0, jc.IsTrue) 277 wstates[wi].update(req.changes) 278 reqs[wi] = nil 279 default: 280 } 281 c.Logf("check %d", wi) 282 wstates[wi].check(c, sm.store) 283 } 284 } 285 // Stop the watcher and check that all ref counts end up at zero 286 // and removed objects are deleted. 287 for wi, w := range ws { 288 sm.handle(&request{watcher: w}) 289 if reqs[wi] != nil { 290 assertReplied(c, false, reqs[wi]) 291 } 292 } 293 294 c.Assert(sm.store.All(), jc.DeepEquals, []multiwatcher.EntityInfo{ 295 &multiwatcher.MachineInfo{ 296 ModelUUID: "uuid", 297 ID: "2", 298 }, 299 }) 300 c.Assert(sm.store.Size(), gc.Equals, 1) 301 } 302 } 303 } 304 305 func assertNotReplied(c *gc.C, req *request) { 306 select { 307 case v := <-req.reply: 308 c.Fatalf("request was unexpectedly replied to (got %v)", v) 309 default: 310 } 311 } 312 313 func assertReplied(c *gc.C, val bool, req *request) { 314 select { 315 case v := <-req.reply: 316 c.Assert(v, gc.Equals, val) 317 default: 318 c.Fatalf("request was not replied to") 319 } 320 } 321 322 func assertWaitingRequests(c *gc.C, worker *Worker, waiting map[*Watcher][]*request) { 323 c.Assert(worker.waiting, gc.HasLen, len(waiting)) 324 for w, reqs := range waiting { 325 i := 0 326 for req := worker.waiting[w]; ; req = req.next { 327 if i >= len(reqs) { 328 c.Assert(req, gc.IsNil) 329 break 330 } 331 c.Assert(req, gc.Equals, reqs[i]) 332 assertNotReplied(c, req) 333 i++ 334 } 335 } 336 } 337 338 // watcherState represents a Multiwatcher client's 339 // current view of the state. It holds the last delta that a given 340 // state watcher has seen for each entity. 341 type watcherState map[interface{}]multiwatcher.Delta 342 343 func (s watcherState) update(changes []multiwatcher.Delta) { 344 for _, d := range changes { 345 id := d.Entity.EntityID() 346 if d.Removed { 347 if _, ok := s[id]; !ok { 348 panic(fmt.Errorf("entity id %v removed when it wasn't there", id)) 349 } 350 delete(s, id) 351 } else { 352 s[id] = d 353 } 354 } 355 } 356 357 // check checks that the watcher state matches that 358 // held in current. 359 func (s watcherState) check(c *gc.C, store multiwatcher.Store) { 360 currentEntities := make(watcherState) 361 for _, info := range store.All() { 362 currentEntities[info.EntityID()] = multiwatcher.Delta{Entity: info} 363 } 364 c.Assert(s, gc.DeepEquals, currentEntities) 365 }