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  }