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
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
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.
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?
@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. |
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. |
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.
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) |
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.
Nice structure.
overlord/patch/patch.go
Outdated
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. |
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.
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.
overlord/patch/patch.go
Outdated
// 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 |
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.
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.
overlord/patch/patch.go
Outdated
|
||
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) |
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.
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)
}
overlord/patch/patch.go
Outdated
} | ||
|
||
func applySublevelPatches(level, start int, s *state.State) error { | ||
for sublevel := start; sublevel < len(patches[level]); sublevel++ { |
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.
Curiously, this logic already assumes that the first level is zero. Minor inconsistency, or hidden bug?
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.
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.
overlord/patch/patch.go
Outdated
|
||
// at the lower Level - apply all new level and sublevel patches | ||
for level := stateLevel; level < Level; level++ { | ||
pp := patches[level+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.
pp?
I suggest something like sublevels instead.
overlord/patch/patch.go
Outdated
// 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)) |
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.
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.
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:
@pedronis @stolowski Does that sound right? |
This is ready for re-review. |
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.
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) |
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.
this test should also check sublevel for completeness I think
overlord/patch/patch.go
Outdated
return fmt.Errorf("internal error: couldn't find current core revision in the snap sequence") | ||
} | ||
|
||
prevRev := snapst.Sequence[currentIndex-1].Revision |
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.
in the case of a/because of revert the values in Sequence after currentIndex might count too
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.
Done.
overlord/patch/patch.go
Outdated
return err | ||
} | ||
if len(snapst.Sequence) < 2 { | ||
return nil |
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.
this case doesn't seem to be tested
overlord/patch/patch.go
Outdated
|
||
var sublevelResetTime time.Time | ||
var lastRefresh time.Time | ||
if err := s.Get("last-refresh", &lastRefresh); err != nil && err != state.ErrNoState { |
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.
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
overlord/patch/patch.go
Outdated
|
||
// at the lower Level - apply all new level and sublevel patches | ||
for level := stateLevel; level < Level; level++ { | ||
sublevels := patches[level+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.
the +1
could be moved to the for loop
level := stateLevel+1 ; level <= Level
no?
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. Done (small downside of doing it is "-1" needed for the debug messages though).
overlord/patch/patch.go
Outdated
"github.com/snapcore/snapd/logger" | ||
"github.com/snapcore/snapd/overlord/snapstate" |
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've reviewed this as part of a follow up PR by mistake. This cannot be here.. please see the review there for details.
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.
Thanks, fixed.
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.
some small comments
overlord/patch/patch.go
Outdated
} | ||
|
||
raw, ok := snaps["core"] | ||
if err == state.ErrNoState || !ok { |
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.
why the err check here? is checked just above
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.
Indeed, not needed, thanks.
overlord/patch/patch.go
Outdated
|
||
func getCoreRefreshTime() (time.Time, error) { | ||
path := filepath.Join(dirs.SnapMountDir, "core", "current") | ||
info, err := os.Stat(path) |
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 think we want Lstat here, no?
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 we do, thanks for spotting!
overlord/patch/patch_test.go
Outdated
os.Remove(currentPath) | ||
} | ||
} | ||
func (s *patchSuite) TestRefreshBackFromLevel60ShortSequence(c *C) { |
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.
missing separator empty line before this?
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.
Added.
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.
thank you
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.
LGTM, thanks!
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.