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
overlord,usersession: initial notifications of pending refreshes #9446
Changes from all commits
2de348f
0e7e301
f0b40bc
e9eb7f1
6066d68
1be2b1a
6c5ae0c
65d8eeb
db810ed
99295dd
2a36c49
670b01a
b6be078
24dbf7c
bf12216
9981312
59d3475
c9e348d
12434b8
b2fdb1f
48c80b4
17eb9e7
f680c1e
31dec23
c487d86
3d43e83
4e68782
268557e
20f559a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
package snapstate | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"time" | ||
|
@@ -35,6 +36,7 @@ import ( | |
"github.com/snapcore/snapd/strutil" | ||
"github.com/snapcore/snapd/timeutil" | ||
"github.com/snapcore/snapd/timings" | ||
userclient "github.com/snapcore/snapd/usersession/client" | ||
) | ||
|
||
// the default refresh pattern | ||
|
@@ -531,21 +533,45 @@ func getTime(st *state.State, timeKey string) (time.Time, error) { | |
return t1, nil | ||
} | ||
|
||
// asyncPendingRefreshNotification broadcasts desktop notification in a goroutine. | ||
// | ||
// This allows the, possibly slow, communication with each snapd session agent, | ||
// to be performed without holding the snap state lock. | ||
var asyncPendingRefreshNotification = func(context context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
go func() { | ||
if err := client.PendingRefreshNotification(context, refreshInfo); err != nil { | ||
logger.Noticef("Cannot send notification about pending refresh: %v", err) | ||
} | ||
}() | ||
} | ||
|
||
// inhibitRefresh returns an error if refresh is inhibited by running apps. | ||
// | ||
// Internally the snap state is updated to remember when the inhibition first | ||
// took place. Apps can inhibit refreshes for up to "maxInhibition", beyond | ||
// that period the refresh will go ahead despite application activity. | ||
func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker func(*snap.Info) error) error { | ||
if err := checker(info); err != nil { | ||
refreshInfo := &userclient.PendingSnapRefreshInfo{ | ||
InstanceName: info.InstanceName(), | ||
} | ||
if err, ok := err.(*BusySnapError); ok { | ||
refreshInfo = err.PendingSnapRefreshInfo() | ||
} | ||
|
||
days := int(maxInhibition.Truncate(time.Hour).Hours() / 24) | ||
now := time.Now() | ||
client := userclient.New() | ||
if snapst.RefreshInhibitedTime == nil { | ||
// Store the instant when the snap was first inhibited. | ||
// This is reset to nil on successful refresh. | ||
snapst.RefreshInhibitedTime = &now | ||
Set(st, info.InstanceName(), snapst) | ||
if _, ok := err.(*BusySnapError); ok { | ||
refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) | ||
// Send the notification asynchronously to avoid holding the state lock. | ||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) | ||
// XXX: remove the warning or send it only if no notification was delivered? | ||
st.Warnf(i18n.NG( | ||
"snap %q is currently in use. Its refresh will be postponed for up to %d day to wait for the snap to no longer be in use.", | ||
"snap %q is currently in use. Its refresh will be postponed for up to %d days to wait for the snap to no longer be in use.", days), | ||
|
@@ -557,9 +583,16 @@ func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker | |
if now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition { | ||
// If we are still in the allowed window then just return | ||
// the error but don't change the snap state again. | ||
refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) | ||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nitpick) we have the notification sending code in very small variations three times here, I wonder if this could be somehow simplified but definitely more a followup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will do this with the removal of the warning, in a follow-up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done (in a follow-up that needs this to land first) |
||
// TODO: as time left shrinks, send additional notifications with | ||
// increasing frequency, allowing the user to understand the | ||
// urgency. | ||
return err | ||
} | ||
if _, ok := err.(*BusySnapError); ok { | ||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is slightly asymmetric, yes? I mean, we generate a warning here, i.e. the "policy" how strongly we warn is part of "autorefresh.go" here. But for the notifications we just give the "refreshInfo" and leave the "policy" about how strongly it warns to the "client.PendingRefreshNotification". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right but I think this resolves itself when the warnings go away. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warnings are removed in a follow-up |
||
// XXX: remove the warning or send it only if no notification was delivered? | ||
st.Warnf(i18n.NG( | ||
"snap %q has been running for the maximum allowable %d day since its refresh was postponed. It will now be refreshed.", | ||
"snap %q has been running for the maximum allowable %d days since its refresh was postponed. It will now be refreshed.", days), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,7 @@ import ( | |
"github.com/snapcore/snapd/store/storetest" | ||
"github.com/snapcore/snapd/testutil" | ||
"github.com/snapcore/snapd/timeutil" | ||
userclient "github.com/snapcore/snapd/usersession/client" | ||
) | ||
|
||
type autoRefreshStore struct { | ||
|
@@ -800,30 +801,82 @@ func (s *autoRefreshTestSuite) TestRefreshOnMeteredConnNotMetered(c *C) { | |
c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) | ||
} | ||
|
||
func (s *autoRefreshTestSuite) TestInhibitRefreshWithinInhibitWindow(c *C) { | ||
func (s *autoRefreshTestSuite) TestInitialInhibitRefreshWithinInhibitWindow(c *C) { | ||
s.state.Lock() | ||
defer s.state.Unlock() | ||
|
||
notificationCount := 0 | ||
restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
notificationCount++ | ||
c.Check(refreshInfo.InstanceName, Equals, "pkg") | ||
c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24) | ||
}) | ||
defer restore() | ||
|
||
si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(1)} | ||
info := &snap.Info{SideInfo: *si} | ||
snapst := &snapstate.SnapState{ | ||
Sequence: []*snap.SideInfo{si}, | ||
Current: si.Revision, | ||
} | ||
err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { | ||
return &snapstate.BusySnapError{SnapName: "pkg"} | ||
return &snapstate.BusySnapError{SnapInfo: si} | ||
}) | ||
c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) | ||
|
||
pending, _ := s.state.PendingWarnings() | ||
c.Assert(pending, HasLen, 1) | ||
c.Check(pending[0].String(), Equals, `snap "pkg" is currently in use. Its refresh will be postponed for up to 14 days to wait for the snap to no longer be in use.`) | ||
c.Check(notificationCount, Equals, 1) | ||
} | ||
|
||
func (s *autoRefreshTestSuite) TestSubsequentInhibitRefreshWithinInhibitWindow(c *C) { | ||
s.state.Lock() | ||
defer s.state.Unlock() | ||
|
||
notificationCount := 0 | ||
restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
notificationCount++ | ||
c.Check(refreshInfo.InstanceName, Equals, "pkg") | ||
// XXX: This test measures real time, with second granularity. | ||
// It takes non-zero (hence the subtracted second) to execute the test. | ||
c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24/2-time.Second) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment here might be nice, something like: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually explained the XXX aspect of this test. Please see inline. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so this test is assuming that the test itself will always take less than one second? That seems ... brittle ... given our past experiences with running unit tests on arm* LP builders |
||
}) | ||
defer restore() | ||
|
||
instant := time.Now() | ||
pastInstant := instant.Add(-snapstate.MaxInhibition / 2) // In the middle of the allowed window | ||
|
||
si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(1)} | ||
info := &snap.Info{SideInfo: *si} | ||
snapst := &snapstate.SnapState{ | ||
Sequence: []*snap.SideInfo{si}, | ||
Current: si.Revision, | ||
RefreshInhibitedTime: &pastInstant, | ||
} | ||
|
||
err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { | ||
return &snapstate.BusySnapError{SnapInfo: si} | ||
}) | ||
c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) | ||
|
||
pending, _ := s.state.PendingWarnings() | ||
c.Assert(pending, HasLen, 0) // This case does not warn | ||
c.Check(notificationCount, Equals, 1) | ||
} | ||
|
||
func (s *autoRefreshTestSuite) TestInhibitRefreshWarnsAndRefreshesWhenOverdue(c *C) { | ||
s.state.Lock() | ||
defer s.state.Unlock() | ||
|
||
notificationCount := 0 | ||
restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
notificationCount++ | ||
c.Check(refreshInfo.InstanceName, Equals, "pkg") | ||
c.Check(refreshInfo.TimeRemaining, Equals, time.Duration(0)) | ||
}) | ||
defer restore() | ||
|
||
instant := time.Now() | ||
pastInstant := instant.Add(-snapstate.MaxInhibition * 2) | ||
|
||
|
@@ -835,11 +888,12 @@ func (s *autoRefreshTestSuite) TestInhibitRefreshWarnsAndRefreshesWhenOverdue(c | |
RefreshInhibitedTime: &pastInstant, | ||
} | ||
err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { | ||
return &snapstate.BusySnapError{SnapName: "pkg"} | ||
return &snapstate.BusySnapError{SnapInfo: si} | ||
}) | ||
c.Assert(err, IsNil) | ||
|
||
pending, _ := s.state.PendingWarnings() | ||
c.Assert(pending, HasLen, 1) | ||
c.Check(pending[0].String(), Equals, `snap "pkg" has been running for the maximum allowable 14 days since its refresh was postponed. It will now be refreshed.`) | ||
c.Check(notificationCount, Equals, 1) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think we should remove the warning. Fwiw, I think the warning here is really a bit of a misfeature because we cannot make it go away once the app was refreshed. But it's fine to keep the XXX and do that in a followup. Especially this first warning that just warns for the first time seems a bit unneeded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree but I'd like to remove them in a follow up.