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  }