-
Notifications
You must be signed in to change notification settings - Fork 596
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
o/snapstate: move auxinfo functions to snapstate backend with metadata helpers #15051
base: master
Are you sure you want to change the base?
o/snapstate: move auxinfo functions to snapstate backend with metadata helpers #15051
Conversation
…a helpers Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Since store metadata is not specific to any particular revision or instance of a snap, it should not be removed if there are other instances of the snap still present. By making `hasOtherInstances` a parameter to `DiscardStoreMetadata`, callers are encouraged to compute and check `hasOtherInstances` beforehand, thus avoiding accidentally removing metadata which is expected for another instance of the same snap. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
…talling metadata The `keepAuxStoreInfo` helper was already a no-op if `snapID == ""`, so remove the additional check in the calling handler. Additionally, make the backend store metadata helpers explicitly return `nil` early if `snapID` is `""`, rather than relying on all functions called within to guarantee this. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
c5b7b15
to
c21b939
Compare
Thu Feb 13 20:34:34 UTC 2025 Failures:Executing:
Restoring:
|
overlord/snapstate/handlers.go
Outdated
// Write the revision-agnostic store metadata for this snap. If snap ID is | ||
// empty, InstallStoreMetadata is a no-op, so no need to check beforehand. | ||
// XXX: Previously, err := ... was used and didn't error, indicating that | ||
// the err checked in IsErrAndNotWait was the local err (which we know to | ||
// be non-nil at time of the check), not the err returned from the | ||
// function. Is this analysis correct, and was this intentional? | ||
// XXX: Also, why is cand.Snap.SnapID used instead of snapsup.SnapID? | ||
// Previously, all of this was inside `if cand.Snap.SnapID != "" {`, and | ||
// AFAICT, snapsup.SnapID should never be "". | ||
err = backend.InstallStoreMetadata(cand.Snap.SnapID, aux) | ||
if err != nil { | ||
return err | ||
} | ||
if len(snapst.Sequence.Revisions) == 1 { | ||
defer func() { | ||
if IsErrAndNotWait(err) { | ||
// the install is getting undone, and there are no more of this snap | ||
// try to remove the metadata we just installed | ||
backend.DiscardStoreMetadata(cand.Snap.SnapID, otherInstances) | ||
} | ||
}() |
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.
@bboozzoo please advise on this, I'm not sure if there's something special about cand.Snap.SnapID
vs simply using snapsup.SnapID
, and if/when one would expect either/both to be ""
.
// InstallStoreMetadata saves revision-agnostic metadata to disk for the snap | ||
// with the given snap ID. At the moment, this metadata includes auxiliary | ||
// store information. | ||
func InstallStoreMetadata(snapID string, aux *AuxStoreInfo) error { | ||
if snapID == "" { | ||
return nil | ||
} | ||
if err := keepAuxStoreInfo(snapID, aux); err != nil { | ||
return err | ||
} | ||
// TODO: install other types of revision-agnostic metadata | ||
return nil | ||
} | ||
|
||
// DiscardStoreMetadata removes revision-agnostic metadata to disk for the snap | ||
// with the given snap ID. At the moment, this metadata includes auxiliary | ||
// store information. If hasOtherInstances is true, does nothing. | ||
func DiscardStoreMetadata(snapID string, hasOtherInstances bool) error { | ||
if hasOtherInstances || snapID == "" { | ||
return nil | ||
} | ||
if err := discardAuxStoreInfo(snapID); err != nil { | ||
return err | ||
} | ||
// TODO: discard other types of revision-agnostic metadata | ||
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.
It would be nice if these could be called directly from within other backend methods (e.g. backend.LinkSnap
and backend.UnlinkSnap
), as that would mean nearly/all of the complexity around saving and reverting auxinfo could be avoided. However, that would require those methods to be able to compute hasOtherInstances
, which is not possible at the moment, unless we want to start passing snapstate into the backend.
Notably, the error handling for doLinkSnap
after backend.LinkSnap(newInfo, ...)
has succeeded already does exactly what we want: if there's another revision, simply call backend.LinkSnap(oldInfo, ...)
, and if there's no other revision, call backend.UnlinkSnap
. If InstallStoreMetadata
and DiscardStoreMetadata
were called from within these, respectively, the result would be as desired.
There are lots of other backend functions which are called from the handlers though, so I'm not sure if it would be consistent to place calls to these helpers within backend.LinkSnap
and backend.UnlinkSnap
, for example, else why are so many backend helpers called from within
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.
overlord/snapstate/handlers.go
Outdated
err = backend.InstallStoreMetadata(cand.Snap.SnapID, aux) | ||
if err != nil { | ||
return err | ||
} | ||
if len(snapst.Sequence.Revisions) == 1 { | ||
defer func() { | ||
if IsErrAndNotWait(err) { | ||
// the install is getting undone, and there are no more of this snap | ||
// try to remove the metadata we just installed | ||
backend.DiscardStoreMetadata(cand.Snap.SnapID, otherInstances) | ||
} |
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 still feels a bit messy, it feels like maybe we should pass cand and SnapSetup and an isInstall flag to the backend and get back a something to do inside the if of the defer in any case
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 could pass cand
in, but I don't think we can pass SnapSetup
in, since it's part of the snapstate
package and that would cause a circular dependency. Also, if we were to pass SnapSetup
in, we'd have all we need from cand
, since all we need is SnapID
, and SnapSetup
has that.
But even if we passed in SnapSetup
, we'd also need to pass in State
so that we can compute otherInstances
. Would we want to do that?
For now, I've added hasOtherInstances
and isInstall
parameters to InstallStoreMetadata
, and made it return an undo callback which takes into account those parameters. Please let me know if this is what you had in mind, or if you'd prefer something else.
Thanks!
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
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, just some small things.
overlord/snapstate/handlers.go
Outdated
// Previously, all of this was inside `if cand.Snap.SnapID != "" {`, and | ||
// AFAICT, snapsup.SnapID should never be "". | ||
isInstall := len(snapst.Sequence.Revisions) == 1 | ||
restore, err := backend.InstallStoreMetadata(cand.Snap.SnapID, aux, otherInstances, isInstall) |
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.
Is it worth accepting isInstall
as a param here, only to use it to change how restore
works?
I wonder if this closure is really needed, the indirection is maybe harder to follow than doing things on this side here. I understand for testability, but the task itself probably needs to test those cases anyways.
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.
Eh yeah I'm not sure either way, I understand wanting to have the undo logic right next to where we have the do side of things.
Note that we wouldn't need otherInstances
either if we didn't use the closure style undo.
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, and I'm also torn. Prior to the previous commit (f7fefa1) the caller was expected to know how to undo things properly, but that undo handling can be rather messy, and Samuele suggested here to just pass in isInstall
as a parameter and have the returned undo closure handle the complexity. I think it's nice to have a simple undo closure, but it does push a bit more complexity to the call site of InstallStoreMetadata
.
Samuele actually suggested passing something more generic (cand
and snapsup
) into InstallStoreMetadata
, which would ease some of the complexity burden in the handler at the cost of more complexity in the metadata function, but I'm worried about circular dependencies, and I think we'd need to pass all of state
in in order to compute otherInstances
.
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 is not the only backend function that is provided such information though, both sides don't seem overly complicated to me, also in the next step the backend side will take care of icons so the trade off should be a bit more valuable
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #15051 +/- ##
=========================================
Coverage ? 78.03%
=========================================
Files ? 1162
Lines ? 156918
Branches ? 0
=========================================
Hits ? 122450
Misses ? 26856
Partials ? 7612
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
…all handler Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
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 for the changes, did another pass
overlord/snapstate/handlers.go
Outdated
// Previously, all of this was inside `if cand.Snap.SnapID != "" {`, and | ||
// AFAICT, snapsup.SnapID should never be "". | ||
isInstall := len(snapst.Sequence.Revisions) == 1 | ||
restore, err := backend.InstallStoreMetadata(cand.Snap.SnapID, aux, otherInstances, isInstall) |
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 is not the only backend function that is provided such information though, both sides don't seem overly complicated to me, also in the next step the backend side will take care of icons so the trade off should be a bit more valuable
…idual flags Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Move `installStoreMetadata` call from the snapstate handler into the backend `LinkSnap` method call, and move `uninstallStoreMetadata` into `UnlinkSnap`. This reduces the complexity of the snapstate handlers. Since `doDiscardSnap` still needs to discard store metadata if there are no other revisions/instances of the snap present, and it is not inherently associated with a particular install/uninstall operation, there is an additional new metadata helper which, instead of taking a `LinkContext`, simply takes a `hasOtherInstances` bool, and uses it to construct a `LinkContext` and then call `uninstallStoreMetadata`. Auxiliary store info is removed regardless of whether `UnlinkSnap` or `doDiscardSnap` are the callers, but other store metadata (such as cached snap icons) may behave differently, so this wrapper will enable this distinction in the future. One important note: auxinfo for a snap should not be removed unless there are no other revisions of the snap present, because in case the unlink snap is reverted, `undoUnlinkSnap` needs to read the auxinfo from disk in order to populate `oldInfo`, so it can be re-linked. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
I've just pushed a commit which moves the calls to This simplifies the snapstate handler code but does require widespread changes to testing, and |
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.
@olivercalder given that LinkSnap is about a revision but the metadata is across revisions and as you said you still need to keep things as they are in doDiscardSnap, I think is better to revert the last change
…ods" This reverts commit e590adc. Since `backend.LinkSnap` is about a particular revision but the metadata is revision-agnostic, and the requirement of discarding metadata manually in `doDiscardSnap` means it's probably clearer to leave the metadata calls all within the snapstate handlers, not backend methods. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Some store metadata should only be removed if there are no other revisions of the snap present, but others may need to be removed regardless. By passing a `LinkContext` including both `FirstInstall` and `HasOtherInstances`, the function can decide for itself what metadata should be discarded, and what should be preserved, rather than the caller being responsible for this. As such, moved calls to `DiscardStoreMetadata` out of some checks for the presence of other revisions. This also means that the undo closure returned by `InstallStoreMetadata` can simply call `DiscardStoreMetadata`, without any additional checks. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
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.
small new remark
overlord/snapstate/handlers.go
Outdated
// link or unlink, so we can discard store metadata appropriately. | ||
linkCtx := backend.LinkContext{ | ||
// there are no revisions of the snap present, so we're discarding the "first" | ||
FirstInstall: true, |
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 looks definitely a bit confusing, maybe we should introduce a (Last)Discard flag in LinkContext as well? and DiscardStoreMetadata should treat it accordingly?
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.
or DiscardMetadataContext structure?
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.
doubtful that the clarity win of adding a new structure is enough vs not being able to just pass LinkContext where it makes sense
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.
Aha, this is actually addressed in the follow-up PR I opened yesterday, which adds the icon calls to the metadata helpers: 7643baf#diff-00d6af4a298e5274422799eac8a04e66a2087322bcb3515a1814623ecafe3ab3L3814-R3815
Icons need to be unlinked in undoLink
/doLink
, but they additionally need to be discarded from the snap icons pool in this place, so we need to split the DiscardStoreMetadata
helper into two, with the one here only requiring hasOtherInstances
, so we don't need this extra LinkContext
here (though one is still used in the metadata helper itself, but I think it's better contained there).
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.
So I can pull in a similar change to this PR now.
overlord/snapstate/handlers.go
Outdated
// XXX: Previously, err := ... was used and didn't error, indicating that | ||
// the err checked in IsErrAndNotWait was the local err (which we know to | ||
// be non-nil at time of the check), not the err returned from the | ||
// function. Is this analysis correct, and was this intentional? |
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 previous code err
in the scope of the deferred function refers to the named return result from the enclosing function. Even if you shadow the variable names, the values are assigned when returning from the function. See https://go.dev/ref/spec#Defer_statements and https://go.dev/play/p/Zs2RTwh2Yg7
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, that makes sense. I think I ended up in some intermediate situation where the defer
call was within the else
of that error handling case --- I think what it was was that I'd added something like:
if otherInstances, err := hasOtherInstances(); err == nil && len(snapst.Sequence.Revisions) == 1 {
defer func() {
if IsErrAndNotWait(err) {
// the install is getting undone, and there are no more of this snap
// try to remove the aux info we just created
discardAuxStoreInfo(cand.Snap.SnapID)
}
}()
and that err
was actually local. This was before moving things into the metadata helpers in the backend, and I was trying to debug why tests weren't covering the discardAuxStoreInfo
call within.
Anyway, I don't see how the original code would have resulted in this problem, so I've removed the comment.
Thanks!
overlord/snapstate/handlers.go
Outdated
// link or unlink, so we can discard store metadata appropriately. | ||
linkCtx := backend.LinkContext{ | ||
// there are no revisions of the snap present, so we're discarding the "first" | ||
FirstInstall: true, |
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.
or DiscardMetadataContext structure?
…and discard When a snap is being unlinked or a link is being undone, the snapstate handler should call `UnlinkStoreMetadata`, which takes a `LinkContext` and checks whether there are other revisions of the snap present. When the final revision of a snap is being discarded, the snapstate handler should call `DiscardStoreMetadata`, which takes a `hasOtherInstances bool` and assumes that there are no other revisions of that instance of the snap present. At the moment, the latter simply calls the former, but in the future, other store metadata which should be handled differently during unlink and discard can be handled appropriately. Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
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.
remark
linkCtx := LinkContext{ | ||
// since the final revision is being discarded, we're effectively | ||
// removing the "first" install of the snap | ||
FirstInstall: true, | ||
HasOtherInstances: hasOtherInstances, | ||
} | ||
if err := UninstallStoreMetadata(snapID, linkCtx); err != 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.
the reuse here doesn't seem very valuable to me, better to just call discardAuxStoreInfo(snapID)
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.
Sure, good point, probably easier to unconditionally remove auxinfo (and icon in the future) than construct a special LinkContext
to get UninstallStoreMetadata
to do what we want.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Move actions which are not revision- or instance-specific into dedicated functions within the snapstate backend.
In preparation for the management of snap icons from the store within
snapstate
handler functions (coming in#15003#15070), move the existing functions related toauxStoreInfo
into the backend package, and wrap them in more generic{Install,Discard}StoreMetadata
backend helper functions. In the future, functions to install/discard snap icons will be called from within these helpers as well. Thus, the snapstate handlers will only have to worry about calling one function to setup and one function to teardown all snap metadata which is not tied to a specific revision or instance of the snap.Additionally, make some other drive-by improvements, such as not discarding auxinfo if there are other instances of the snap present.
This work is tracked internally by https://warthogs.atlassian.net/browse/SNAPDENG-34436