github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/uniter/resolver/loop_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resolver_test
     5  
     6  import (
     7  	"errors"
     8  	"time"
     9  
    10  	"github.com/juju/charm/v12/hooks"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/mutex/v2"
    13  	envtesting "github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "gopkg.in/check.v1"
    16  
    17  	"github.com/juju/juju/testcharms"
    18  	"github.com/juju/juju/testing"
    19  	coretesting "github.com/juju/juju/testing"
    20  	"github.com/juju/juju/worker/uniter/hook"
    21  	"github.com/juju/juju/worker/uniter/operation"
    22  	"github.com/juju/juju/worker/uniter/remotestate"
    23  	"github.com/juju/juju/worker/uniter/resolver"
    24  )
    25  
    26  type LoopSuite struct {
    27  	testing.BaseSuite
    28  
    29  	resolver  resolver.Resolver
    30  	watcher   *mockRemoteStateWatcher
    31  	opFactory *mockOpFactory
    32  	executor  *mockOpExecutor
    33  	charmURL  string
    34  	charmDir  string
    35  	abort     chan struct{}
    36  	onIdle    func() error
    37  }
    38  
    39  var _ = gc.Suite(&LoopSuite{})
    40  
    41  func (s *LoopSuite) SetUpTest(c *gc.C) {
    42  	s.BaseSuite.SetUpTest(c)
    43  	s.resolver = resolver.ResolverFunc(func(resolver.LocalState, remotestate.Snapshot, operation.Factory) (operation.Operation, error) {
    44  		return nil, resolver.ErrNoOperation
    45  	})
    46  	s.watcher = &mockRemoteStateWatcher{
    47  		changes: make(chan struct{}, 1),
    48  	}
    49  	s.opFactory = &mockOpFactory{}
    50  	s.executor = &mockOpExecutor{}
    51  	s.charmURL = "ch:trusty/mysql-1"
    52  	s.abort = make(chan struct{})
    53  }
    54  
    55  func (s *LoopSuite) loop() (resolver.LocalState, error) {
    56  	localState := resolver.LocalState{
    57  		CharmURL: s.charmURL,
    58  	}
    59  	err := resolver.Loop(resolver.LoopConfig{
    60  		Resolver:      s.resolver,
    61  		Factory:       s.opFactory,
    62  		Watcher:       s.watcher,
    63  		Executor:      s.executor,
    64  		Abort:         s.abort,
    65  		OnIdle:        s.onIdle,
    66  		CharmDir:      s.charmDir,
    67  		CharmDirGuard: &mockCharmDirGuard{},
    68  		Logger:        loggo.GetLogger("test"),
    69  	}, &localState)
    70  	return localState, err
    71  }
    72  
    73  func (s *LoopSuite) TestAbort(c *gc.C) {
    74  	close(s.abort)
    75  	_, err := s.loop()
    76  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
    77  }
    78  
    79  func (s *LoopSuite) TestOnIdle(c *gc.C) {
    80  	onIdleCh := make(chan interface{}, 1)
    81  	s.onIdle = func() error {
    82  		onIdleCh <- nil
    83  		return nil
    84  	}
    85  
    86  	done := make(chan interface{}, 1)
    87  	go func() {
    88  		_, err := s.loop()
    89  		done <- err
    90  	}()
    91  
    92  	waitChannel(c, onIdleCh, "waiting for onIdle")
    93  	s.watcher.changes <- struct{}{}
    94  	waitChannel(c, onIdleCh, "waiting for onIdle")
    95  	close(s.abort)
    96  
    97  	err := waitChannel(c, done, "waiting for loop to exit")
    98  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
    99  
   100  	select {
   101  	case <-onIdleCh:
   102  		c.Fatal("unexpected onIdle call")
   103  	default:
   104  	}
   105  }
   106  
   107  func (s *LoopSuite) TestOnIdleError(c *gc.C) {
   108  	s.onIdle = func() error {
   109  		return errors.New("onIdle failed")
   110  	}
   111  	close(s.abort)
   112  	_, err := s.loop()
   113  	c.Assert(err, gc.ErrorMatches, "onIdle failed")
   114  }
   115  
   116  func (s *LoopSuite) TestErrWaitingNoOnIdle(c *gc.C) {
   117  	var onIdleCalled bool
   118  	s.onIdle = func() error {
   119  		onIdleCalled = true
   120  		return nil
   121  	}
   122  	s.resolver = resolver.ResolverFunc(func(
   123  		_ resolver.LocalState,
   124  		_ remotestate.Snapshot,
   125  		_ operation.Factory,
   126  	) (operation.Operation, error) {
   127  		return nil, resolver.ErrWaiting
   128  	})
   129  	close(s.abort)
   130  	_, err := s.loop()
   131  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   132  	c.Assert(onIdleCalled, jc.IsFalse)
   133  }
   134  
   135  func (s *LoopSuite) TestInitialFinalLocalState(c *gc.C) {
   136  	var local resolver.LocalState
   137  	s.resolver = resolver.ResolverFunc(func(
   138  		l resolver.LocalState,
   139  		_ remotestate.Snapshot,
   140  		_ operation.Factory,
   141  	) (operation.Operation, error) {
   142  		local = l
   143  		return nil, resolver.ErrNoOperation
   144  	})
   145  
   146  	close(s.abort)
   147  	lastLocal, err := s.loop()
   148  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   149  	c.Assert(local, jc.DeepEquals, resolver.LocalState{
   150  		CharmURL: s.charmURL,
   151  	})
   152  	c.Assert(lastLocal, jc.DeepEquals, local)
   153  }
   154  
   155  func (s *LoopSuite) TestLoop(c *gc.C) {
   156  	var resolverCalls int
   157  	theOp := &mockOp{}
   158  	s.resolver = resolver.ResolverFunc(func(
   159  		_ resolver.LocalState,
   160  		_ remotestate.Snapshot,
   161  		_ operation.Factory,
   162  	) (operation.Operation, error) {
   163  		resolverCalls++
   164  		switch resolverCalls {
   165  		// On the first call, return an operation.
   166  		case 1:
   167  			return theOp, nil
   168  		// On the second call, simulate having
   169  		// no operations to perform, at which
   170  		// point we'll wait for a remote state
   171  		// change.
   172  		case 2:
   173  			s.watcher.changes <- struct{}{}
   174  			break
   175  		// On the third call, kill the loop.
   176  		case 3:
   177  			close(s.abort)
   178  			break
   179  		}
   180  		return nil, resolver.ErrNoOperation
   181  	})
   182  
   183  	_, err := s.loop()
   184  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   185  	c.Assert(resolverCalls, gc.Equals, 3)
   186  	s.executor.CheckCallNames(c, "State", "State", "State", "Run", "State", "State")
   187  
   188  	runArgs := s.executor.Calls()[3].Args
   189  	c.Assert(runArgs, gc.HasLen, 2)
   190  	c.Assert(runArgs[0], gc.DeepEquals, theOp)
   191  	c.Assert(runArgs[1], gc.NotNil)
   192  }
   193  
   194  func (s *LoopSuite) TestLoopWithChange(c *gc.C) {
   195  	var resolverCalls int
   196  	theOp := &mockOp{}
   197  	s.resolver = resolver.ResolverFunc(func(
   198  		_ resolver.LocalState,
   199  		_ remotestate.Snapshot,
   200  		_ operation.Factory,
   201  	) (operation.Operation, error) {
   202  		resolverCalls++
   203  		switch resolverCalls {
   204  		// On the first call, return an operation.
   205  		case 1:
   206  			return theOp, nil
   207  		// On the second call, simulate having
   208  		// no operations to perform, at which
   209  		// point we'll wait for a remote state
   210  		// change.
   211  		case 2:
   212  			s.watcher.changes <- struct{}{}
   213  			break
   214  		case 3:
   215  			break
   216  		// On the fourth call, kill the loop.
   217  		case 4:
   218  			close(s.abort)
   219  			break
   220  		}
   221  		return nil, resolver.ErrNoOperation
   222  	})
   223  
   224  	var remoteStateSnapshotChan <-chan remotestate.Snapshot
   225  	remoteStateSnapshotCount := 0
   226  	s.executor.run = func(op operation.Operation, rs <-chan remotestate.Snapshot) error {
   227  		remoteStateSnapshotChan = rs
   228  		for i := 0; i < 5; i++ {
   229  			// queue up a change to trigger snapshot channel.
   230  			s.watcher.changes <- struct{}{}
   231  			// wait for changes to propagate
   232  			select {
   233  			case _, ok := <-rs:
   234  				c.Assert(ok, jc.IsTrue)
   235  				remoteStateSnapshotCount++
   236  			case <-time.After(testing.ShortWait):
   237  				c.Fatalf("timed out waiting for remote state snapshot")
   238  				panic("unreachable")
   239  			}
   240  		}
   241  		return nil
   242  	}
   243  
   244  	_, err := s.loop()
   245  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   246  	c.Assert(resolverCalls, gc.Equals, 4)
   247  	s.executor.CheckCallNames(c, "State", "State", "State", "Run", "State", "State", "State")
   248  
   249  	c.Assert(remoteStateSnapshotCount, gc.Equals, 5)
   250  	select {
   251  	case _, ok := <-remoteStateSnapshotChan:
   252  		c.Assert(ok, jc.IsTrue)
   253  		c.Fatalf("remote state snapshot channel fired more than once")
   254  	default:
   255  	}
   256  
   257  	runArgs := s.executor.Calls()[3].Args
   258  	c.Assert(runArgs, gc.HasLen, 2)
   259  	c.Assert(runArgs[0], gc.DeepEquals, theOp)
   260  	c.Assert(runArgs[1], gc.NotNil)
   261  }
   262  
   263  func (s *LoopSuite) TestRunFails(c *gc.C) {
   264  	s.executor.SetErrors(errors.New("run fails"))
   265  	s.resolver = resolver.ResolverFunc(func(
   266  		_ resolver.LocalState,
   267  		_ remotestate.Snapshot,
   268  		_ operation.Factory,
   269  	) (operation.Operation, error) {
   270  		return mockOp{}, nil
   271  	})
   272  	_, err := s.loop()
   273  	c.Assert(err, gc.ErrorMatches, "run fails")
   274  }
   275  
   276  func (s *LoopSuite) TestNextOpFails(c *gc.C) {
   277  	s.resolver = resolver.ResolverFunc(func(
   278  		_ resolver.LocalState,
   279  		_ remotestate.Snapshot,
   280  		_ operation.Factory,
   281  	) (operation.Operation, error) {
   282  		return nil, errors.New("NextOp fails")
   283  	})
   284  	_, err := s.loop()
   285  	c.Assert(err, gc.ErrorMatches, "NextOp fails")
   286  }
   287  
   288  func (s *LoopSuite) TestCheckCharmUpgradeUpgradeCharmHook(c *gc.C) {
   289  	s.executor = &mockOpExecutor{
   290  		Executor: nil,
   291  		Stub:     envtesting.Stub{},
   292  		st: operation.State{
   293  			Installed: true,
   294  			Kind:      operation.Continue,
   295  			Hook:      &hook.Info{Kind: hooks.UpgradeCharm},
   296  		},
   297  		run: nil,
   298  	}
   299  	s.testCheckCharmUpgradeDoesNothing(c)
   300  }
   301  
   302  func (s *LoopSuite) TestCheckCharmUpgradeSameURL(c *gc.C) {
   303  	s.executor = &mockOpExecutor{
   304  		Executor: nil,
   305  		Stub:     envtesting.Stub{},
   306  		st: operation.State{
   307  			Installed: true,
   308  			Kind:      operation.Continue,
   309  		},
   310  		run: nil,
   311  	}
   312  	s.watcher = &mockRemoteStateWatcher{
   313  		snapshot: remotestate.Snapshot{
   314  			CharmURL: s.charmURL,
   315  		},
   316  	}
   317  	s.charmDir = testcharms.Repo.CharmDirPath("mysql")
   318  	s.testCheckCharmUpgradeDoesNothing(c)
   319  }
   320  
   321  func (s *LoopSuite) TestCheckCharmUpgradeNotInstalled(c *gc.C) {
   322  	s.executor = &mockOpExecutor{
   323  		Executor: nil,
   324  		Stub:     envtesting.Stub{},
   325  		st: operation.State{
   326  			Kind: operation.Continue,
   327  		},
   328  		run: nil,
   329  	}
   330  	s.watcher = &mockRemoteStateWatcher{
   331  		snapshot: remotestate.Snapshot{
   332  			CharmURL: "ch:trusty/mysql-2",
   333  		},
   334  	}
   335  	s.charmDir = testcharms.Repo.CharmDirPath("mysql")
   336  	s.testCheckCharmUpgradeDoesNothing(c)
   337  }
   338  
   339  func (s *LoopSuite) TestCheckCharmUpgradeIncorrectLXDProfile(c *gc.C) {
   340  	s.executor = &mockOpExecutor{
   341  		Executor: nil,
   342  		Stub:     envtesting.Stub{},
   343  		st: operation.State{
   344  			Installed: true,
   345  			Started:   true,
   346  			Kind:      operation.Continue,
   347  		},
   348  		run: nil,
   349  	}
   350  	s.watcher = &mockRemoteStateWatcher{
   351  		snapshot: remotestate.Snapshot{
   352  			CharmURL:             "ch:trusty/mysql-2",
   353  			CharmProfileRequired: true,
   354  			LXDProfileName:       "juju-test-mysql-1",
   355  		},
   356  	}
   357  	s.testCheckCharmUpgradeDoesNothing(c)
   358  }
   359  
   360  func (s *LoopSuite) testCheckCharmUpgradeDoesNothing(c *gc.C) {
   361  	s.resolver = resolver.ResolverFunc(func(
   362  		_ resolver.LocalState,
   363  		_ remotestate.Snapshot,
   364  		_ operation.Factory,
   365  	) (operation.Operation, error) {
   366  		return nil, resolver.ErrWaiting
   367  	})
   368  	close(s.abort)
   369  	_, err := s.loop()
   370  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   371  
   372  	// Run not called
   373  	c.Assert(s.executor.Calls(), gc.HasLen, 3)
   374  	s.executor.CheckCallNames(c, "State", "State", "State")
   375  }
   376  
   377  func (s *LoopSuite) TestCheckCharmUpgrade(c *gc.C) {
   378  	s.executor = &mockOpExecutor{
   379  		Executor: nil,
   380  		Stub:     envtesting.Stub{},
   381  		st: operation.State{
   382  			Installed: true,
   383  			Kind:      operation.Continue,
   384  		},
   385  		run: nil,
   386  	}
   387  	s.watcher = &mockRemoteStateWatcher{
   388  		snapshot: remotestate.Snapshot{
   389  			CharmURL: "ch:trusty/mysql-2",
   390  		},
   391  	}
   392  	s.testCheckCharmUpgradeCallsRun(c, "Upgrade")
   393  }
   394  
   395  func (s *LoopSuite) TestCheckCharmUpgradeMissingCharmDir(c *gc.C) {
   396  	s.executor = &mockOpExecutor{
   397  		Executor: nil,
   398  		Stub:     envtesting.Stub{},
   399  		st: operation.State{
   400  			Installed: true,
   401  			Kind:      operation.Continue,
   402  		},
   403  		run: nil,
   404  	}
   405  	s.watcher = &mockRemoteStateWatcher{
   406  		snapshot: remotestate.Snapshot{
   407  			CharmURL: s.charmURL,
   408  		},
   409  	}
   410  	s.testCheckCharmUpgradeCallsRun(c, "Upgrade")
   411  }
   412  
   413  func (s *LoopSuite) TestCheckCharmInstallMissingCharmDirInstallHookFail(c *gc.C) {
   414  	s.executor = &mockOpExecutor{
   415  		Executor: nil,
   416  		Stub:     envtesting.Stub{},
   417  		st: operation.State{
   418  			Installed: false,
   419  			Kind:      operation.RunHook,
   420  			Step:      operation.Pending,
   421  			Hook:      &hook.Info{Kind: hooks.Install},
   422  		},
   423  		run: nil,
   424  	}
   425  	s.watcher = &mockRemoteStateWatcher{
   426  		snapshot: remotestate.Snapshot{
   427  			CharmURL: s.charmURL,
   428  		},
   429  	}
   430  	s.testCheckCharmUpgradeCallsRun(c, "Install")
   431  }
   432  
   433  func (s *LoopSuite) TestCheckCharmUpgradeLXDProfile(c *gc.C) {
   434  	s.executor = &mockOpExecutor{
   435  		Executor: nil,
   436  		Stub:     envtesting.Stub{},
   437  		st: operation.State{
   438  			Installed: true,
   439  			Started:   true,
   440  			Kind:      operation.Continue,
   441  		},
   442  		run: nil,
   443  	}
   444  	s.watcher = &mockRemoteStateWatcher{
   445  		snapshot: remotestate.Snapshot{
   446  			CharmURL:             "ch:trusty/mysql-2",
   447  			CharmProfileRequired: true,
   448  			LXDProfileName:       "juju-test-mysql-2",
   449  		},
   450  	}
   451  	s.testCheckCharmUpgradeCallsRun(c, "Upgrade")
   452  }
   453  
   454  func (s *LoopSuite) testCheckCharmUpgradeCallsRun(c *gc.C, op string) {
   455  	s.opFactory = &mockOpFactory{
   456  		Factory: nil,
   457  		Stub:    envtesting.Stub{},
   458  		op:      mockOp{},
   459  	}
   460  	s.resolver = resolver.ResolverFunc(func(
   461  		_ resolver.LocalState,
   462  		_ remotestate.Snapshot,
   463  		_ operation.Factory,
   464  	) (operation.Operation, error) {
   465  		return nil, resolver.ErrWaiting
   466  	})
   467  	close(s.abort)
   468  	_, err := s.loop()
   469  	c.Assert(err, gc.Equals, resolver.ErrLoopAborted)
   470  
   471  	// Run not called
   472  	c.Assert(s.executor.Calls(), gc.HasLen, 4)
   473  	s.executor.CheckCallNames(c, "State", "State", "Run", "State")
   474  
   475  	c.Assert(s.opFactory.Calls(), gc.HasLen, 1)
   476  	s.opFactory.CheckCallNames(c, "New"+op)
   477  }
   478  
   479  func (s *LoopSuite) TestCancelledLockAcquisitionCausesRestart(c *gc.C) {
   480  	s.executor = &mockOpExecutor{
   481  		Executor: nil,
   482  		Stub:     envtesting.Stub{},
   483  		st: operation.State{
   484  			Started: true,
   485  			Kind:    operation.Continue,
   486  		},
   487  		run: func(operation.Operation, <-chan remotestate.Snapshot) error {
   488  			return mutex.ErrCancelled
   489  		},
   490  	}
   491  
   492  	s.resolver = resolver.ResolverFunc(func(
   493  		_ resolver.LocalState,
   494  		_ remotestate.Snapshot,
   495  		_ operation.Factory,
   496  	) (operation.Operation, error) {
   497  		return &mockOp{}, nil
   498  	})
   499  
   500  	_, err := s.loop()
   501  	c.Assert(err, gc.Equals, resolver.ErrRestart)
   502  }
   503  
   504  func waitChannel(c *gc.C, ch <-chan interface{}, activity string) interface{} {
   505  	select {
   506  	case v := <-ch:
   507  		return v
   508  	case <-time.After(coretesting.LongWait):
   509  		c.Fatalf("timed out " + activity)
   510  		panic("unreachable")
   511  	}
   512  }