Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger upgrade-series-prepare hook. #8864

Merged
merged 45 commits into from Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1c0e8c7
Trigger upgrade-series-prepare hook.
ExternalReality Jun 27, 2018
0ac2132
API to check status.
ExternalReality Jun 30, 2018
02ce53c
Uniter identifies upgrade-series status changes.
ExternalReality Jul 2, 2018
be3d968
Trigger hook for upgrade series on build.
ExternalReality Jul 2, 2018
c5f67d3
Add uniter callback to update series upgade state.
ExternalReality Jul 3, 2018
678e376
Pull index to query nested array.
ExternalReality Jul 5, 2018
3d71446
thread through initial unit upgrade status.
ExternalReality Jul 6, 2018
9873531
ensure proper unit gets status set.
ExternalReality Jul 6, 2018
025d928
Resolve uniter status
ExternalReality Jul 6, 2018
095e39e
Ensure the uniter transitions prepare states.f
ExternalReality Jul 6, 2018
132b877
Remove redundant param, and more transactionsOp builder tests.
Jul 7, 2018
9e06d5e
Fix test name.
Jul 7, 2018
e7d842a
defer mutex release.
Jul 7, 2018
afd2b0e
Fix runhook tests.
Jul 8, 2018
ce7f0c9
Fix uniter tests.
ExternalReality Jul 9, 2018
418f215
Upgrade charm v6 reference.
ExternalReality Jul 9, 2018
737461e
Fix debug hooks test.
ExternalReality Jul 9, 2018
d31fefe
Fix api error check.
ExternalReality Jul 9, 2018
cd35901
Fix state transistion tests.
ExternalReality Jul 9, 2018
93b5a64
Fix known hooks test.
ExternalReality Jul 9, 2018
92d223c
Fix snapshot tests.
ExternalReality Jul 9, 2018
f7e1ab8
Remomve redundant lock call.
ExternalReality Jul 9, 2018
1ac1597
Fix abort error of set series upgrade transaction.
ExternalReality Jul 9, 2018
facc529
Do not return string api setter method.
ExternalReality Jul 9, 2018
ff4ac60
No need for status mutex in setUpgradeSeriesStatus
ExternalReality Jul 9, 2018
2eda0a1
Remove unused unit state update function.
ExternalReality Jul 9, 2018
7ca5aae
Fix typo in function name.
ExternalReality Jul 9, 2018
27b7ec7
Use more exact results struct.
ExternalReality Jul 10, 2018
cef8aed
Update test due to default change.
ExternalReality Jul 10, 2018
aa6dcff
Use more specific error resutls type.
ExternalReality Jul 10, 2018
e5c98bb
Remove error logging.
ExternalReality Jul 11, 2018
c622313
Move string contstants to core.
ExternalReality Jul 11, 2018
0d31aea
Pass around strings in the api and add validation.
ExternalReality Jul 11, 2018
71f3958
Refine assertions in update transaction.
ExternalReality Jul 11, 2018
44050d1
Test more upgrade series status assumptions.
ExternalReality Jul 11, 2018
600d130
Consume intial upgrade series notification on server side.
ExternalReality Jul 11, 2018
8cee315
Remove dangerous looking named returned value error
ExternalReality Jul 11, 2018
fb9f7cd
Documentation fix.
ExternalReality Jul 11, 2018
550ecd7
Documentation addition.
ExternalReality Jul 11, 2018
3d830be
Rename series lock to better match other state types.
ExternalReality Jul 12, 2018
b99b95a
Documentation fix.
ExternalReality Jul 12, 2018
2ab106c
Documentation fix.
ExternalReality Jul 12, 2018
6c00b4e
Change array search to switch statement.
ExternalReality Jul 12, 2018
a5c59a6
Allow different statuses.
ExternalReality Jul 13, 2018
8cc0cae
Fix redundant conversion.
ExternalReality Jul 13, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions api/uniter/unit.go
Expand Up @@ -642,6 +642,48 @@ func (u *Unit) WatchActionNotifications() (watcher.StringsWatcher, error) {
return w, nil
}

// WatchActionNotifications returns a StringsWatcher for observing the state of
// a series upgrade.
func (u *Unit) WatchUpgradeSeriesNotifications() (watcher.NotifyWatcher, error) {
var results params.NotifyWatchResults
args := params.Entities{
Entities: []params.Entity{{Tag: u.tag.String()}},
}
err := u.st.facade.FacadeCall("WatchUpgradeSeriesNotifications", args, &results)
if err != nil {
return nil, err
}
if len(results.Results) != 1 {
return nil, errors.Errorf("expected 1 result, got %d", len(results.Results))
}
result := results.Results[0]
if result.Error != nil {
return nil, result.Error
}
w := apiwatcher.NewNotifyWatcher(u.st.facade.RawAPICaller(), result)
return w, nil
}

// UpgradeSeriesStatus returns the upgrade series status of a unit from remote state
func (u *Unit) UpgradeSeriesStatus() (string, error) {
var results params.UpgradeSeriesStatusResults
args := params.Entities{
Entities: []params.Entity{{Tag: u.tag.String()}},
}
err := u.st.facade.FacadeCall("UpgradeSeriesStatus", args, &results)
if err != nil {
return "", err
}
if len(results.Results) != 1 {
return "", errors.Errorf("expected 1 result, got %d", len(results.Results))
}
result := results.Results[0]
if result.Error != nil {
return "", result.Error
}
return result.Status, nil
}

// RequestReboot sets the reboot flag for its machine agent
func (u *Unit) RequestReboot() error {
machineId, err := u.AssignedMachine()
Expand Down
38 changes: 38 additions & 0 deletions api/uniter/unit_test.go
Expand Up @@ -684,6 +684,44 @@ func (s *unitSuite) TestWatchActionNotificationsMoreResults(c *gc.C) {
c.Assert(err.Error(), gc.Equals, "expected 1 result, got 2")
}

func (s *unitSuite) TestWatchUpgradeSeriesNotifications(c *gc.C) {
watcher, err := s.apiUnit.WatchUpgradeSeriesNotifications()
c.Assert(err, jc.ErrorIsNil)

notifyWatcher := watchertest.NewNotifyWatcherC(c, watcher, s.BackingState.StartSync)
defer notifyWatcher.AssertStops()

// Why do I have two initial events?!?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are apparently two initial events coming out of this puppy. Why?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you find out why this is happening? I noticed the TestWatchActionNotificationsMoreResults() is testing that only 1 watch notification happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is what I mean above. No I don't know why, its innocuous (since the Uniter will just loop twice), but I don't know why its sending two initial events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Histerical raisins.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All watchers send an initial event to trigger the checking of initial state. Many times this isn't useful, and you'll see comments like "consume initial event".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fixed now.

_, ok := <-notifyWatcher.Watcher.Changes()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bare channel operation is almost always dangerous, as it can block indefinitely (and then you only find out when the test suite is killed after 25min).

However, getting 2 events is even worse, as it throws off everyone's expectations.
I don't know exactly how your code is initialized, but I'll note that the NotifyWatcher's that I know of don't transmit an event over-the-wire for the first event. They intentionally swallow the first event on the server side, and trust that the client side knows its a NotifyWatcher and thus triggers the event on the client side.
I'm guessing you have the same "always trigger an event" on your client side, but don't have the "swallow the initial event" on the server side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! Yes, the other watchers do consume an initial event on the server side - subsequently, on the client side, the additional event is not observed. Thank you @jameinel.

notifyWatcher.Assert(ok, jc.IsTrue)

notifyWatcher.AssertOneChange()

err = s.wordpressMachine.CreateUpgradeSeriesLock()
c.Assert(err, jc.ErrorIsNil)

// Expect a notification that the document was created (i.e. a lock was placed)
notifyWatcher.AssertOneChange()

err = s.wordpressMachine.RemoveUpgradeSeriesLock()
c.Assert(err, jc.ErrorIsNil)

// A notification that the document was removed (i.e. the lock was released)
notifyWatcher.AssertOneChange()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other than the initial double event, this test looks good.

}

func (s *unitSuite) TestUpgradeSeriesStatus(c *gc.C) {
// First we create the prepare lock
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth checking the pre-condition of s.apiUnit.UpgradeSeriesStatus() before we create the lock? So that we see the value changes to Started?

It seems odd that you have this test, which says the status is Started, even though there isn't a call to SetUpgradeSeriesStatus(UnitStarted).
Shouldn't the status of a unit start in something like Pending?

If we want to just have "started" as the initial state, then I'd say we don't need a test that you can set the state to Started (drop TestSetUpgradeSeriesStatus, or change it to set the status to Completed)
or clarify that we just treat setting the status to Started as a no-op?
Do we support going from Completed back to Started? Or should that be considered an error? (I'd tend toward the latter, though re-running a pre upgrade step is something we've discussed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestUpgradeSeriesStatus tests that a call to the api gets the current remote state - regardless of what it is.

It just so happens that the remote state starts in the UnitStartedState. When you create a lock, an upgrade is considered started. The uniter see's that and syncs its local state accordingly, so the initial states are as follows:

Uniter: UnitNotStarted
Controller: UnitStarted (as soon as a lock is created)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but correct, lets test our assumption! We will put in a check of the pre-condition. I only note it in a comment.

err := s.wordpressMachine.CreateUpgradeSeriesLock()
c.Assert(err, jc.ErrorIsNil)

// Then we check to see the status of our upgrade. We note that creating
// the lock essentially kicks off an upgrade for the perspective of assigned units.
status, err := s.apiUnit.UpgradeSeriesStatus()
c.Assert(err, jc.ErrorIsNil)
c.Assert(status, gc.Equals, "preparing")
}

func (s *unitSuite) TestApplicationNameAndTag(c *gc.C) {
c.Assert(s.apiUnit.ApplicationName(), gc.Equals, s.wordpressApplication.Name())
c.Assert(s.apiUnit.ApplicationTag(), gc.Equals, s.wordpressApplication.Tag())
Expand Down
71 changes: 71 additions & 0 deletions apiserver/facades/agent/uniter/uniter.go
Expand Up @@ -984,6 +984,77 @@ func (u *UniterAPI) WatchActionNotifications(args params.Entities) (params.Strin
return common.WatchActionNotifications(args, canAccess, watchOne), nil
}

// WatchActionNotifications returns a StringsWatcher for observing the status of an Upgrade Series
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc string: WatchActionNotifications creates a NotifyWatcher

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah, the copy pasta monster - or creepy pasta - depending on which way you want to look at it.

func (u *UniterAPI) WatchUpgradeSeriesNotifications(args params.Entities) (params.NotifyWatchResults, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can put these into an api shared between the uniter and the new upgrade series worker? Both need to WatchUpgradeSeriesNotifications(), UpgradeSeriesStatus() etc. Entities deal in tags, so we can get different data if necessary for machines and units.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is being moved into a new API as per the above, therefor there is no reason to bump version here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ExternalReality I'm not sure what you are meaning "as per the above". If we are adding methods to the uniter API, then we need to bump the facade version, even if they are brought in through a common implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@howbazaar, the above comment by @hmlanigan. The code is being moved out of the UniterAPI entirely in the next PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally we have a facade per-worker, and while they might both embed a common.Methods, they would be separate grouping of methods.

result := params.NotifyWatchResults{
Results: make([]params.NotifyWatchResult, len(args.Entities)),
}
canAccess, err := u.accessUnit()
if err != nil {
return params.NotifyWatchResults{}, err
}
for i, entity := range args.Entities {
tag, err := names.ParseUnitTag(entity.Tag)
if err != nil {
result.Results[i].Error = common.ServerError(common.ErrPerm)
continue
}
watcherId := ""
if !canAccess(tag) {
result.Results[i].Error = common.ServerError(common.ErrPerm)
continue
}
unit, err := u.getUnit(tag)
if err != nil {
result.Results[i].Error = common.ServerError(err)
continue
}
w, err := unit.WatchUpgradeSeriesNotifications()
if err != nil {
result.Results[i].Error = common.ServerError(err)
continue
}
watcherId = u.resources.Register(w)
result.Results[i].NotifyWatcherId = watcherId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per your earlier question of "why do I have 2 events":
https://github.com/juju/juju/blob/develop/apiserver/common/watch.go#L39
"
// Consume the initial event. Technically, API
// calls to Watch 'transmit' the initial event
// in the Watch response. But NotifyWatchers
// have no state to transmit.
if _, ok := <-watch.Changes(); ok {
"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(put another way, the call to Watch() should return the watcher identity, and the content of the first Next() result, and then on the client side, those should be split up into the appropriate responses. A NotifyWatcher doesn't have any actual content to return just an event that gets consumed on the server and regenerated on the client)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jameinel I'm unclear on what if anything should be added to the code here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to consume the first Next() event here, and "transmit" the value along with the return of Watch() which the transmit is a no-op for us.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @howbazaar, I swallowed the extra event on the server side, here I was swallowing both the servers initial event and the clients initial event on the client. My fault.

}
return result, nil
}

func (u *UniterAPI) UpgradeSeriesStatus(args params.Entities) (params.UpgradeSeriesStatusResults, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comment needed.

result := params.UpgradeSeriesStatusResults{
Results: make([]params.UpgradeSeriesStatusResult, len(args.Entities)),
}
canAccess, err := u.accessUnit()
if err != nil {
return params.UpgradeSeriesStatusResults{}, err
}
for i, entity := range args.Entities {
tag, err := names.ParseUnitTag(entity.Tag)
if err != nil {
result.Results[i].Error = common.ServerError(common.ErrPerm)
continue
}

if !canAccess(tag) {
result.Results[i].Error = common.ServerError(common.ErrPerm)
continue
}
unit, err := u.getUnit(tag)
if err != nil {
result.Results[i].Error = common.ServerError(err)
continue
}
status, err := unit.UpgradeSeriesStatus()
if err != nil {
result.Results[i].Error = common.ServerError(err)
continue
}
result.Results[i].Status = status
}

return result, nil
}

// ConfigSettings returns the complete set of application charm config
// settings available to each given unit.
func (u *UniterAPI) ConfigSettings(args params.Entities) (params.ConfigSettingsResults, error) {
Expand Down
9 changes: 9 additions & 0 deletions apiserver/params/params.go
Expand Up @@ -1276,3 +1276,12 @@ type DumpModelRequest struct {
Entities []Entity `json:"entities"`
Simplified bool `json:"simplified"`
}

type UpgradeSeriesStatusResult struct {
Error *Error `json:"error,omitempty"`
Status string `json:"status,omitempty"`
}

type UpgradeSeriesStatusResults struct {
Results []UpgradeSeriesStatusResult `json:"results,omitempty"`
}
5 changes: 5 additions & 0 deletions state/machine.go
Expand Up @@ -2203,6 +2203,11 @@ func (m *Machine) RemoveUpgradeSeriesLock() error {
return nil
}

// UpgradeSeriesStatus returns the status of a series upgrade.
func (m *Machine) UpgradeSeriesStatus() (string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why pass back a string? the two status types are typed.

return "preparing", nil
}

func createUpgradeSeriesLockTxnOps(machineDocId string, data *upgradeSeriesLock) []txn.Op {
return []txn.Op{
{
Expand Down
19 changes: 19 additions & 0 deletions state/watcher.go
Expand Up @@ -1636,6 +1636,25 @@ func (u *Unit) WatchMeterStatus() NotifyWatcher {
})
}

// WatchUpgradeStatus returns a watcher that observes the status of a series
// upgrade by monitoring changes to its parent machine's upgrade series lock.
func (u *Unit) WatchUpgradeSeriesNotifications() (NotifyWatcher, error) {
machine, err := u.machine()
if err != nil {
return nil, err
}
return newEntityWatcher(machine.st, machineUpgradeSeriesLocksC, machine.doc.DocID), nil
}

// UpgradeSeriesStatus returns the upgrade status of the units assigned machine.
func (u *Unit) UpgradeSeriesStatus() (string, error) {
machine, err := u.machine()
if err != nil {
return "", err
}
return machine.UpgradeSeriesStatus()
}

func newEntityWatcher(backend modelBackend, collName string, key interface{}) NotifyWatcher {
return newDocWatcher(backend, []docKey{{collName, key}})
}
Expand Down
2 changes: 1 addition & 1 deletion worker/uniter/hook/hook.go
Expand Up @@ -48,7 +48,7 @@ func (hi Info) Validate() error {
}
fallthrough
case hooks.Install, hooks.Start, hooks.ConfigChanged, hooks.UpgradeCharm, hooks.Stop, hooks.RelationBroken,
hooks.CollectMetrics, hooks.MeterStatusChanged, hooks.UpdateStatus:
hooks.CollectMetrics, hooks.MeterStatusChanged, hooks.UpdateStatus, hooks.UpgradeSeriesPrepare:
return nil
case hooks.Action:
return fmt.Errorf("hooks.Kind Action is deprecated")
Expand Down
9 changes: 9 additions & 0 deletions worker/uniter/remotestate/mock_test.go
Expand Up @@ -204,6 +204,7 @@ type mockUnit struct {
addressesWatcher *mockNotifyWatcher
configSettingsWatcher *mockNotifyWatcher
applicationConfigSettingsWatcher *mockNotifyWatcher
upgradeSeriesWatcher *mockNotifyWatcher
storageWatcher *mockStringsWatcher
actionWatcher *mockStringsWatcher
relationsWatcher *mockStringsWatcher
Expand Down Expand Up @@ -261,6 +262,14 @@ func (u *mockUnit) WatchRelations() (watcher.StringsWatcher, error) {
return u.relationsWatcher, nil
}

func (u *mockUnit) WatchUpgradeSeriesNotifications() (watcher.NotifyWatcher, error) {
return u.upgradeSeriesWatcher, nil
}

func (u *mockUnit) UpgradeSeriesStatus() (string, error) {
return "prepare", nil
}

type mockApplication struct {
tag names.ApplicationTag
life params.Life
Expand Down
3 changes: 3 additions & 0 deletions worker/uniter/remotestate/snapshot.go
Expand Up @@ -71,6 +71,9 @@ type Snapshot struct {

// Series is the current series running on the unit
Series string

// UpgradeSeriesStatus is the status of any currently running series upgrade
UpgradeSeriesStatus string
}

type RelationSnapshot struct {
Expand Down
2 changes: 2 additions & 0 deletions worker/uniter/remotestate/state.go
Expand Up @@ -42,11 +42,13 @@ type Unit interface {
WatchAddresses() (watcher.NotifyWatcher, error)
WatchConfigSettings() (watcher.NotifyWatcher, error)
WatchTrustConfigSettings() (watcher.NotifyWatcher, error)
WatchUpgradeSeriesNotifications() (watcher.NotifyWatcher, error)
WatchStorage() (watcher.StringsWatcher, error)
WatchActionNotifications() (watcher.StringsWatcher, error)
// WatchRelation returns a watcher that fires when relations
// relevant for this unit change.
WatchRelations() (watcher.StringsWatcher, error)
UpgradeSeriesStatus() (string, error)
}

type Application interface {
Expand Down
30 changes: 29 additions & 1 deletion worker/uniter/remotestate/watcher.go
Expand Up @@ -170,7 +170,7 @@ func (w *RemoteStateWatcher) CommandCompleted(completed string) {
}

func (w *RemoteStateWatcher) setUp(unitTag names.UnitTag) (err error) {
// TODO(dfc) named return value is a time bomb
// TODO(dfc) named return value is a time bomb (externalreality) I second the notion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we can't just remove 'err error' and change it to 'error' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a hesitance to dabble. Will change.

// TODO(axw) move this logic.
defer func() {
cause := errors.Cause(err)
Expand Down Expand Up @@ -317,6 +317,17 @@ func (w *RemoteStateWatcher) loop(unitTag names.UnitTag) (err error) {
// returned by the leadership tracker.
requiredEvents++

// TODO(externalreality) This pattern should probably be extracted
var seenUpgradeSeriesChange bool
upgradeSeriesw, err := w.unit.WatchUpgradeSeriesNotifications()
if err != nil {
return errors.Trace(err)
}
if err := w.catacomb.Add(upgradeSeriesw); err != nil {
return errors.Trace(err)
}
requiredEvents++

var eventsObserved int
observedEvent := func(flag *bool) {
if flag != nil && !*flag {
Expand Down Expand Up @@ -407,6 +418,12 @@ func (w *RemoteStateWatcher) loop(unitTag names.UnitTag) (err error) {
if err != nil {
return err
}
case _, ok := <-upgradeSeriesw.Changes():
logger.Debugf("got upgrade series change")
if !ok {
return errors.New("upgrades series watcher closed")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is going to close this? And is it really an error? The other watchers don't follow this pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop where this code lives has 12 other watchers that follow this pattern. They all return a similar error. The loop function is a catacomb worker and the function's watchers are bound the life cycle of that catacomb using the catacomb's Add function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is an appropriate safeguard that the event we got was an actual event rather than just a closed channel.
I agree with Eric that the other watchers are following a similar pattern. It might just be that the very previous watcher wasn't doing this that made it seem to be a different pattern.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, just saw the one above. Happy to have consistency here.

}
observedEvent(&seenUpgradeSeriesChange)
case _, ok := <-addressesChanges:
logger.Debugf("got address change: ok=%t", ok)
if !ok {
Expand Down Expand Up @@ -845,3 +862,14 @@ func (w *RemoteStateWatcher) watchStorageAttachment(
w.storageAttachmentWatchers[tag] = innerSAW
return nil
}

func (w *RemoteStateWatcher) watchUpgradeSeries() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this function used?

w.mu.Lock()
defer w.mu.Unlock()
status, err := w.unit.UpgradeSeriesStatus()
if err != nil {
return errors.Trace(err)
}
w.current.UpgradeSeriesStatus = status
return nil
}
6 changes: 6 additions & 0 deletions worker/uniter/remotestate/watcher_test.go
Expand Up @@ -60,6 +60,7 @@ func (s *WatcherSuite) SetUpTest(c *gc.C) {
addressesWatcher: newMockNotifyWatcher(),
configSettingsWatcher: newMockNotifyWatcher(),
applicationConfigSettingsWatcher: newMockNotifyWatcher(),
upgradeSeriesWatcher: newMockNotifyWatcher(),
storageWatcher: newMockStringsWatcher(),
actionWatcher: newMockStringsWatcher(),
relationsWatcher: newMockStringsWatcher(),
Expand Down Expand Up @@ -151,6 +152,7 @@ func (s *WatcherSuite) TestInitialSignal(c *gc.C) {
s.st.unit.addressesWatcher.changes <- struct{}{}
s.st.unit.configSettingsWatcher.changes <- struct{}{}
s.st.unit.applicationConfigSettingsWatcher.changes <- struct{}{}
s.st.unit.upgradeSeriesWatcher.changes <- struct{}{}
s.st.unit.storageWatcher.changes <- []string{}
s.st.unit.actionWatcher.changes <- []string{}
if s.st.unit.application.applicationWatcher != nil {
Expand All @@ -167,6 +169,7 @@ func (s *WatcherSuite) signalAll() {
s.st.unit.unitWatcher.changes <- struct{}{}
s.st.unit.configSettingsWatcher.changes <- struct{}{}
s.st.unit.applicationConfigSettingsWatcher.changes <- struct{}{}
s.st.unit.upgradeSeriesWatcher.changes <- struct{}{}
s.st.unit.actionWatcher.changes <- []string{}
s.st.unit.application.leaderSettingsWatcher.changes <- struct{}{}
s.st.unit.relationsWatcher.changes <- []string{}
Expand Down Expand Up @@ -276,6 +279,9 @@ func (s *WatcherSuite) TestRemoteStateChanged(c *gc.C) {
s.st.unit.relationsWatcher.changes <- []string{}
assertOneChange()

s.st.unit.upgradeSeriesWatcher.changes <- struct{}{}
assertOneChange()

s.clock.Advance(5 * time.Minute)
assertOneChange()
}
Expand Down
5 changes: 5 additions & 0 deletions worker/uniter/resolver.go
Expand Up @@ -278,6 +278,11 @@ func (s *uniterResolver) nextOp(
return opFactory.NewRunHook(hook.Info{Kind: hooks.ConfigChanged})
}

if localState.UpgradeSeriesStatus != remoteState.UpgradeSeriesStatus &&
remoteState.UpgradeSeriesStatus == "preparing" {
return opFactory.NewRunHook(hook.Info{Kind: hooks.UpgradeSeriesPrepare})
}

op, err := s.config.Relations.NextOp(localState, remoteState, opFactory)
if errors.Cause(err) != resolver.ErrNoOperation {
return op, err
Expand Down
4 changes: 4 additions & 0 deletions worker/uniter/resolver/interface.go
Expand Up @@ -98,4 +98,8 @@ type LocalState struct {
// Series is the current series running on the unit from remotestate.Snapshot
// for which a config-changed hook has been committed.
Series string

// UpgradeSeriesStatus is the current state of any currently running
// series upgrade or the empty string if no series upgrade has been started.
UpgradeSeriesStatus string
}