github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/multiwatcher/watcher_test.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package multiwatcher_test 5 6 import ( 7 "time" 8 9 "github.com/juju/clock" 10 "github.com/juju/errors" 11 "github.com/juju/loggo" 12 "github.com/juju/testing" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/worker/v3/workertest" 15 gc "gopkg.in/check.v1" 16 17 "github.com/juju/juju/core/multiwatcher" 18 "github.com/juju/juju/state" 19 mwWorker "github.com/juju/juju/worker/multiwatcher" 20 "github.com/juju/juju/worker/multiwatcher/testbacking" 21 ) 22 23 type watcherSuite struct { 24 testing.IsolationSuite 25 } 26 27 var _ = gc.Suite(&watcherSuite{}) 28 29 func (s *watcherSuite) startWorker(c *gc.C, backing state.AllWatcherBacking) *mwWorker.Worker { 30 logger := loggo.GetLogger("test") 31 logger.SetLogLevel(loggo.TRACE) 32 config := mwWorker.Config{ 33 Clock: clock.WallClock, 34 Logger: logger, 35 Backing: backing, 36 PrometheusRegisterer: noopRegisterer{}, 37 } 38 w, err := mwWorker.NewWorker(config) 39 c.Assert(err, jc.ErrorIsNil) 40 s.AddCleanup(func(c *gc.C) { 41 workertest.CleanKill(c, w) 42 }) 43 return w 44 } 45 46 func (s *watcherSuite) TestEmptyModel(c *gc.C) { 47 b := testbacking.New(nil) 48 f := s.startWorker(c, b) 49 w := f.WatchController() 50 checkNext(c, w, nil, "") 51 } 52 53 func (s *watcherSuite) TestRun(c *gc.C) { 54 b := testbacking.New([]multiwatcher.EntityInfo{ 55 &multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0"}, 56 &multiwatcher.ApplicationInfo{ModelUUID: "uuid", Name: "logging"}, 57 &multiwatcher.ApplicationInfo{ModelUUID: "uuid", Name: "wordpress"}, 58 }) 59 mw := s.startWorker(c, b) 60 w := mw.WatchController() 61 62 checkNext(c, w, []multiwatcher.Delta{ 63 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0"}}, 64 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid", Name: "logging"}}, 65 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid", Name: "wordpress"}}, 66 }, "") 67 68 b.UpdateEntity(&multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0", InstanceID: "i-0"}) 69 checkNext(c, w, []multiwatcher.Delta{ 70 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0", InstanceID: "i-0"}}, 71 }, "") 72 73 b.DeleteEntity(multiwatcher.EntityID{"machine", "uuid", "0"}) 74 checkNext(c, w, []multiwatcher.Delta{ 75 {Removed: true, Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid", ID: "0"}}, 76 }, "") 77 } 78 79 func (s *watcherSuite) TestMultipleModels(c *gc.C) { 80 b := testbacking.New([]multiwatcher.EntityInfo{ 81 &multiwatcher.MachineInfo{ModelUUID: "uuid0", ID: "0"}, 82 &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging"}, 83 &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "wordpress"}, 84 &multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0"}, 85 &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "logging"}, 86 &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "wordpress"}, 87 &multiwatcher.MachineInfo{ModelUUID: "uuid2", ID: "0"}, 88 }) 89 mw := s.startWorker(c, b) 90 w := mw.WatchController() 91 92 checkNext(c, w, []multiwatcher.Delta{ 93 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid0", ID: "0"}}, 94 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging"}}, 95 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "wordpress"}}, 96 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0"}}, 97 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "logging"}}, 98 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "wordpress"}}, 99 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid2", ID: "0"}}, 100 }, "") 101 102 b.UpdateEntity(&multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0", InstanceID: "i-0"}) 103 checkNext(c, w, []multiwatcher.Delta{ 104 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0", InstanceID: "i-0"}}, 105 }, "") 106 107 b.DeleteEntity(multiwatcher.EntityID{"machine", "uuid2", "0"}) 108 checkNext(c, w, []multiwatcher.Delta{ 109 {Removed: true, Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid2", ID: "0"}}, 110 }, "") 111 112 b.UpdateEntity(&multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging", Exposed: true}) 113 checkNext(c, w, []multiwatcher.Delta{ 114 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging", Exposed: true}}, 115 }, "") 116 } 117 118 func (s *watcherSuite) TestModelFiltering(c *gc.C) { 119 b := testbacking.New([]multiwatcher.EntityInfo{ 120 &multiwatcher.MachineInfo{ModelUUID: "uuid0", ID: "0"}, 121 &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging"}, 122 &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "wordpress"}, 123 &multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0"}, 124 &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "logging"}, 125 &multiwatcher.ApplicationInfo{ModelUUID: "uuid1", Name: "wordpress"}, 126 &multiwatcher.MachineInfo{ModelUUID: "uuid2", ID: "0"}, 127 }) 128 mw := s.startWorker(c, b) 129 w := watchWatcher(c, mw.WatchModel("uuid0")) 130 c.Logf("w.assertNext") 131 w.assertNext([]multiwatcher.Delta{ 132 {Entity: &multiwatcher.MachineInfo{ModelUUID: "uuid0", ID: "0"}}, 133 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging"}}, 134 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "wordpress"}}, 135 }) 136 c.Logf("w.assertNext") 137 138 // Updating uuid1 shouldn't signal a next call 139 b.UpdateEntity(&multiwatcher.MachineInfo{ModelUUID: "uuid1", ID: "0", InstanceID: "i-0"}) 140 b.DeleteEntity(multiwatcher.EntityID{"machine", "uuid2", "0"}) 141 c.Logf("w.assertNext") 142 w.assertNoChange() 143 144 c.Logf("w.assertNext") 145 b.UpdateEntity(&multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging", Exposed: true}) 146 w.assertNext([]multiwatcher.Delta{ 147 {Entity: &multiwatcher.ApplicationInfo{ModelUUID: "uuid0", Name: "logging", Exposed: true}}, 148 }) 149 } 150 151 func (s *watcherSuite) TestWatcherStop(c *gc.C) { 152 mw := s.startWorker(c, testbacking.New(nil)) 153 w := mw.WatchController() 154 155 err := w.Stop() 156 c.Assert(err, jc.ErrorIsNil) 157 checkNext(c, w, nil, multiwatcher.NewErrStopped().Error()) 158 } 159 160 func (s *watcherSuite) TestWatcherStopBecauseBackingError(c *gc.C) { 161 b := testbacking.New([]multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ID: "0"}}) 162 mw := s.startWorker(c, b) 163 w := mw.WatchController() 164 165 // Receive one delta to make sure that the storeManager 166 // has seen the initial state. 167 checkNext(c, w, []multiwatcher.Delta{{Entity: &multiwatcher.MachineInfo{ID: "0"}}}, "") 168 c.Logf("setting fetch error") 169 b.SetFetchError(errors.New("some error")) 170 171 c.Logf("updating entity") 172 b.UpdateEntity(&multiwatcher.MachineInfo{ID: "1"}) 173 checkNext(c, w, nil, "some error") 174 } 175 176 func (s *watcherSuite) TestWatcherErrorWhenWorkerStopped(c *gc.C) { 177 b := testbacking.New([]multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ID: "0"}}) 178 mw := s.startWorker(c, b) 179 w := mw.WatchController() 180 181 // Receive one delta to make sure that the storeManager 182 // has seen the initial state. 183 checkNext(c, w, []multiwatcher.Delta{{Entity: &multiwatcher.MachineInfo{ID: "0"}}}, "") 184 185 workertest.CleanKill(c, mw) 186 187 b.UpdateEntity(&multiwatcher.MachineInfo{ID: "1"}) 188 189 d, err := w.Next() 190 c.Assert(err, gc.ErrorMatches, "shared state watcher was stopped") 191 c.Assert(err, jc.Satisfies, multiwatcher.IsErrStopped) 192 c.Assert(d, gc.HasLen, 0) 193 } 194 195 func getNext(c *gc.C, w multiwatcher.Watcher, timeout time.Duration) ([]multiwatcher.Delta, error) { 196 var deltas []multiwatcher.Delta 197 var err error 198 ch := make(chan struct{}, 1) 199 go func() { 200 deltas, err = w.Next() 201 ch <- struct{}{} 202 }() 203 select { 204 case <-ch: 205 return deltas, err 206 case <-time.After(timeout): 207 } 208 return nil, errors.New("no change received in sufficient time") 209 } 210 211 func checkNext(c *gc.C, w multiwatcher.Watcher, deltas []multiwatcher.Delta, expectErr string) { 212 d, err := getNext(c, w, 1*time.Second) 213 if expectErr != "" { 214 c.Check(err, gc.ErrorMatches, expectErr) 215 return 216 } 217 c.Assert(err, jc.ErrorIsNil) 218 checkDeltasEqual(c, d, deltas) 219 } 220 221 func checkDeltasEqual(c *gc.C, d0, d1 []multiwatcher.Delta) { 222 // Deltas are returned in arbitrary order, so we compare them as maps. 223 c.Check(deltaMap(d0), jc.DeepEquals, deltaMap(d1)) 224 } 225 226 func deltaMap(deltas []multiwatcher.Delta) map[interface{}]multiwatcher.EntityInfo { 227 m := make(map[interface{}]multiwatcher.EntityInfo) 228 for _, d := range deltas { 229 id := d.Entity.EntityID() 230 if d.Removed { 231 m[id] = nil 232 } else { 233 m[id] = d.Entity 234 } 235 } 236 return m 237 } 238 239 // Need a way to test Next calls that block. This is needed to test filtering. 240 241 type watcher struct { 242 c *gc.C 243 inner multiwatcher.Watcher 244 next chan []multiwatcher.Delta 245 err chan error 246 logger loggo.Logger 247 } 248 249 func watchWatcher(c *gc.C, w multiwatcher.Watcher) *watcher { 250 result := &watcher{ 251 c: c, 252 inner: w, 253 // We use a buffered channels here so the final next call that returns 254 // an error when the worker stops can be pushed to the channel and allow 255 // the goroutine to stop. 256 next: make(chan []multiwatcher.Delta, 1), 257 err: make(chan error, 1), 258 logger: loggo.GetLogger("test"), 259 } 260 go result.loop() 261 return result 262 } 263 264 func (w *watcher) loop() { 265 for true { 266 deltas, err := w.inner.Next() 267 if err != nil { 268 w.err <- err 269 return 270 } 271 select { 272 case w.next <- deltas: 273 w.logger.Tracef("sent %d deltas down next", len(deltas)) 274 case <-time.After(testing.LongWait): 275 w.c.Fatalf("no one listening") 276 } 277 } 278 } 279 280 func (w *watcher) assertNext(deltas []multiwatcher.Delta) { 281 select { 282 case err := <-w.err: 283 w.c.Fatalf("watcher had err: %v", err) 284 case next := <-w.next: 285 checkDeltasEqual(w.c, next, deltas) 286 case <-time.After(testing.LongWait): 287 w.c.Fatalf("no results returned") 288 } 289 } 290 291 func (w *watcher) assertNoChange() { 292 select { 293 case <-time.After(testing.ShortWait): 294 // all good 295 case err := <-w.err: 296 w.c.Fatalf("watcher had err: %v", err) 297 case next := <-w.next: 298 w.c.Fatalf("unexpected results %#v", next) 299 } 300 }