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

overlord/patch: support for sublevel patches #5683

Merged
merged 27 commits into from Sep 8, 2018

Conversation

stolowski
Copy link
Contributor

@stolowski stolowski commented Aug 20, 2018

Support for sub-level patches and that do not prevent rollback. The patches become a map of arrays, e.g. patches[level] = {sublevelPatch1, sublevelPatch2...}.

This is needed for #5497.

@codecov-io
Copy link

codecov-io commented Aug 20, 2018

Codecov Report

Merging #5683 into master will decrease coverage by 0.01%.
The diff coverage is 74.74%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #5683      +/-   ##
=========================================
- Coverage   79.11%   79.1%   -0.02%     
=========================================
  Files         530     530              
  Lines       40444   40520      +76     
=========================================
+ Hits        31999   32052      +53     
- Misses       5853    5865      +12     
- Partials     2592    2603      +11
Impacted Files Coverage Δ
overlord/patch/patch2.go 86.2% <100%> (ø) ⬆️
overlord/patch/patch3.go 100% <100%> (ø) ⬆️
overlord/patch/patch1.go 58.06% <100%> (ø) ⬆️
overlord/patch/patch5.go 6.66% <100%> (ø) ⬆️
overlord/patch/patch6.go 85.36% <100%> (ø) ⬆️
overlord/patch/patch4.go 65.18% <100%> (ø) ⬆️
overlord/patch/patch.go 76.03% <73.11%> (-15.08%) ⬇️
overlord/hookstate/hookmgr.go 74.51% <0%> (+0.96%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c7f2b64...b6c366f. Read the comment docs.

@stolowski stolowski closed this Aug 20, 2018
@stolowski stolowski reopened this Aug 21, 2018
Copy link
Contributor

@chipaca chipaca left a comment

Choose a reason for hiding this comment

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

Say there's a backward-compatible transformation that you need to apply state. You do it with a sublevel patch and release it in version N. A user in on version M<N; they refresh to N, but then roll back to M for a while before refreshing to N once again. The second time they refresh to N, the transformation isn't applied (because the sublevel is already there), so they now can have bits of their state untransformed.

So, we need to handle untransformed state anyway. Why have subpatches at all?

@stolowski
Copy link
Contributor Author

stolowski commented Aug 21, 2018

@chipaca dang, you do have a point, I missed this scenario. I suppose we could update state sublevel when we detect a downgrade, so that subpatches are re-applied later when the user updates again.
The main motivation/advantage is to have all the state transformations in well defined place (and not have to iterate/inspect state if we know there is nothning to transform).

@niemeyer
Copy link
Contributor

So, we need to handle untransformed state anyway. Why have subpatches at all?

For the same reason we have patches, I think. We could always lump everything together in a single changeset that then needs all the smarts internally to verify when it really has to be applied. But with a patch system we can tell exactly what needs to be applied right now.

With sublevels, the situation arguably gets a bit more difficult because we're allowing the changeset to be "redone" when rolling backwards and forwards again, but it's still not so different. Which exact patches need to be applied right now when we just upgraded? Well, it's the delta between the prior level+sublevel and the current level+sublevel.

Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

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

Looking pretty good, thanks Pawel.

A few suggestions:

type PatchFunc func(s *state.State) error

// patches maps from patch level L to the list of sublevel patches.
var patches = make(map[int][]PatchFunc)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice structure.

if err == nil || err == state.ErrNoState {
err = s.Get("patch-sublevel", &stateSublevel)
if err == state.ErrNoState && stateLevel <= 6 {
// accommodate for the fact that sublevel patches got introduced at patch level 6.
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't have to special case patch 6. We can represent the fact that a level has a single sublevel in exactly the same, before and after patch 6, and no matter what the current sublevel is. Although I haven't finished reading the logic, this is probably important as we need to keep in mind that the current system may not actually be in patch level 6. So if there's something special about it, it's probably a bug.

// patches maps from patch level L to the function that moves from L-1 to L.
var patches = make(map[int]func(s *state.State) error)
// Sublevel is the current implemented sublevel for the Level. Sublevel patches do not prevent rollbacks.
var Sublevel = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to make more sense to say that the first sublevel on a level is zero (6.0). Seems more natural both in terms of the number (6.0) and also in the sense that this is the number we cannot go backwards from, as it introduced the changes of level N. It also makes more sense for indexing.. (patches[level][0] is the first patch of the given level).

Logic below will need to be adapted for that.


func applySublevelPatches(level, start int, s *state.State) error {
for sublevel := start; sublevel < len(patches[level]); sublevel++ {
logger.Noticef("Patching system state from level %d, sublevel %d to sublevel %d", level, sublevel, sublevel+1)
Copy link
Contributor

Choose a reason for hiding this comment

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

We just logged the level we're coming from and the one we're going to.. we can be more terse here, and perhaps avoid saying something for the first first sublevel, even because the first sublevel will be wrong otherwise (there's no sublevel FIRST-1).

How about:

if sublevel > 0 {
    logger.Noticef("Patching system state level %d to sublevel %d...", level, sublevel)
}

}

func applySublevelPatches(level, start int, s *state.State) error {
for sublevel := start; sublevel < len(patches[level]); sublevel++ {
Copy link
Contributor

Choose a reason for hiding this comment

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

Curiously, this logic already assumes that the first level is zero. Minor inconsistency, or hidden bug?

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 method is used to apply a series of patches for given level; we start from 0 when upgrading to a new level. I hope it makes more sense now after I adjusted all the surrounding code per your other comments.


// at the lower Level - apply all new level and sublevel patches
for level := stateLevel; level < Level; level++ {
pp := patches[level+1]
Copy link
Contributor

Choose a reason for hiding this comment

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

pp?

I suggest something like sublevels instead.

// at the lower Level - apply all new level and sublevel patches
for level := stateLevel; level < Level; level++ {
pp := patches[level+1]
logger.Noticef("Patching system state from level %d to %d, sublevel", level, level+1, len(pp))
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong number of parameters.. string is missing a placeholder.

I suggest dropping the sublevels and keeping the string as it is here. This is indeed moving from patch level N to N+1, which is the incompatible change and the most important one. Further sublevels will then log their individual messages per the other suggestion above.

@niemeyer
Copy link
Contributor

Per discussion with @pedronis online, there's also the issue of detecting when a revert to a 6.0 release happens. The old code won't know to reset the sublevel back to zero, which means that when an upgrade happens next sublevel 1 and on won't apply again.

To fix that, we can look at the state and see the history of revisions for core. Something along those lines:

  1. Is state level 6 and state sublevel > 0? If so, continue with the following...
  2. In the history of the core snap in state, is the previously installed core revision known as a patch 6 sublevel 0? If so, continue with the following...
  3. Is the last core refresh timestamp different from the stored patch-sublevel-reset? If so, continue with the following
  4. Update patch-sublevel to zero
  5. Update patch-sublevel-reset to the current core refresh timestamp

@pedronis @stolowski Does that sound right?

@niemeyer niemeyer requested a review from pedronis August 22, 2018 13:01
@stolowski
Copy link
Contributor Author

This is ready for re-review.

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

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

some questions/comments

@@ -61,7 +61,7 @@ func (ovs *overlordSuite) TearDownTest(c *C) {
}

func (ovs *overlordSuite) TestNew(c *C) {
restore := patch.Mock(42, nil)
restore := patch.Mock(42, 2, nil)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this test should also check sublevel for completeness I think

return fmt.Errorf("internal error: couldn't find current core revision in the snap sequence")
}

prevRev := snapst.Sequence[currentIndex-1].Revision
Copy link
Collaborator

Choose a reason for hiding this comment

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

in the case of a/because of revert the values in Sequence after currentIndex might count too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

return err
}
if len(snapst.Sequence) < 2 {
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

this case doesn't seem to be tested


var sublevelResetTime time.Time
var lastRefresh time.Time
if err := s.Get("last-refresh", &lastRefresh); err != nil && err != state.ErrNoState {
Copy link
Collaborator

Choose a reason for hiding this comment

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

last-refresh is the last auto-refresh only, is not the last core refresh time, the closest to that would be the /snap/core/current symlink time


// at the lower Level - apply all new level and sublevel patches
for level := stateLevel; level < Level; level++ {
sublevels := patches[level+1]
Copy link
Collaborator

Choose a reason for hiding this comment

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

the +1 could be moved to the for loop

level := stateLevel+1 ; level <= Level

no?

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. Done (small downside of doing it is "-1" needed for the debug messages though).

"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/snapstate"
Copy link
Contributor

Choose a reason for hiding this comment

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

I've reviewed this as part of a follow up PR by mistake. This cannot be here.. please see the review there for details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed.

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

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

some small comments

}

raw, ok := snaps["core"]
if err == state.ErrNoState || !ok {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why the err check here? is checked just above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, not needed, thanks.


func getCoreRefreshTime() (time.Time, error) {
path := filepath.Join(dirs.SnapMountDir, "core", "current")
info, err := os.Stat(path)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we want Lstat here, no?

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 we do, thanks for spotting!

os.Remove(currentPath)
}
}
func (s *patchSuite) TestRefreshBackFromLevel60ShortSequence(c *C) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing separator empty line before this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

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

thank you

Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@stolowski stolowski merged commit 600bb07 into snapcore:master Sep 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants