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

o/snapstate: account for remodeling when installing prereqs as well as updating them #14137

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions overlord/snapstate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,19 +324,28 @@ func (m *SnapManager) installOneBaseOrRequired(t *state.Task, snapName string, c
return nil, err
}

inProgress := func(snapName string) (bool, error) {
shouldAttemptInstall := func(snapName string) (bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe then this name can go back to the old one? it's really a bit confusing for the usage inside if isInstalled

linkTask, err := findLinkSnapTaskForSnap(st, snapName)
if err != nil {
return false, err
}

if linkTask == nil {
// snap is not being installed
return true, nil
}

// if we are remodeling, then we should return early due to the way that
// tasks are ordered by the remodeling code. specifically, all snap
// downloads during a remodel happen prior to snap installation. thus,
// we cannot wait for snaps to be installed here. see remodelTasks for
// more information on how the tasks are ordered.
Copy link
Collaborator

Choose a reason for hiding this comment

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

wont' this be a problem also for updatePrereqIfOutdated code? doesn't it mean we should simply check this before all other checks and return early? instead of doing the check here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I spoke to Miguel about this earlier, because I was also confused about the ordering of the original code:

Those are mutually exclusive, unless I'm misunderstanding the question. The first if runs when the prerequisite is a default provider and the rest runs if it's another type of dependency (base, snapd). So that checks doesn't run after updating. Also the updatePrereqIfOutdated has code to check if its already being updated so it would catch a conflict

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to clarify, that comment was about the inProgress check vs the updatePrereqIfOutdated call which was the code snippet in the link. It wasn't about the remodel check

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm I see I was conflating them.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then I misunderstood the question, my bad

if deviceCtx.ForRemodeling() {
return false, nil
}

// snap is being installed, retry later
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this comment making sense in all cases now?

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to account for the refresh case

return true, nil
return false, onInFlight
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure that return directly onInFlight is helpful here

Copy link
Member Author

Choose a reason for hiding this comment

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

I return onInFlight here to distinguish between when we are remodeling or when we aren't.

Copy link
Member Author

@andrewphelpsj andrewphelpsj Jul 2, 2024

Choose a reason for hiding this comment

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

I reworked how this looks in the most recent commit. Since we don't actually want to go into updatePrereqIfOutdated during a remodel since it tries to reach out to the store (we should have already downloaded all the snaps needed for the remodel, cc @MiguelPires to make sure I'm not lying), I think we can just move the check for remodeling earlier in the function.

}

if isInstalled {
Expand All @@ -346,28 +355,17 @@ func (m *SnapManager) installOneBaseOrRequired(t *state.Task, snapName string, c
}

// other kind of dependency, check if it's in progress
if ok, err := inProgress(snapName); err != nil {
if _, err := shouldAttemptInstall(snapName); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks a bit weird now. It's inside a isInstalled case but we run a shouldAttemptInstall check? It's also inconsistent with the comment above it.
Tbh the original is also not great, since it has more logic to it than than just checking if the snap is running

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree that the name isn't good. Unfortunately I haven't been able to come up with something better.

Copy link
Collaborator

Choose a reason for hiding this comment

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

do we actually need this code inside the "if" now? wouldn't the code outside the "if" do the same in practice?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it can be dropped here to just always return err, regardless if nil or not.

return nil, err
} else if ok {
return nil, onInFlight
}
return nil, nil
}

// not installed, wait for it if it is. If not, we'll install it
if ok, err := inProgress(snapName); err != nil {
if ok, err := shouldAttemptInstall(snapName); err != nil {
return nil, err
} else if ok {
// if we are remodeling, then we should return early due to the way that
// tasks are ordered by the remodeling code. specifically, all snap
// downloads during a remodel happen prior to snap installation. thus,
// we cannot wait for snaps to be installed here. see remodelTasks for
// more information on how the tasks are ordered.
if deviceCtx.ForRemodeling() {
return nil, nil
}

return nil, onInFlight
} else if !ok {
return nil, nil
}

// not installed, nor queued for install -> install it
Expand Down
70 changes: 70 additions & 0 deletions overlord/snapstate/handlers_prereq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,76 @@ func (s *prereqSuite) TestDoPrereqRetryWhenBaseInFlight(c *C) {
c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *prereqSuite) TestDoPrereqNoRetryWhenBaseInFlightDuringRemodel(c *C) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could have other variants (skips when there's a install in progress, a possible update of a prereq, a missing base, etc) but maybe it's not worth it to add those right now since we want this merged soon

restore := snapstate.MockPrerequisitesRetryTimeout(1 * time.Millisecond)
defer restore()

s.runner.AddHandler("link-snap", func(task *state.Task, _ *tomb.Tomb) error {
st := task.State()
st.Lock()
defer st.Unlock()

snapsup, err := snapstate.TaskSnapSetup(task)
c.Assert(err, IsNil)
fmt.Println(snapsup.InstanceName())

return nil
}, nil)

s.state.Lock()
defer s.state.Unlock()

restore = snapstatetest.MockDeviceContext(&snapstatetest.TrivialDeviceContext{
Remodeling: true,
})
defer restore()

// make it look like core is already installed
snapstate.Set(s.state, "core", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
{RealName: "core", Revision: snap.R(1)},
}),
Current: snap.R(1),
SnapType: "os",
})

// pretend foo gets installed and needs core (which we will make it look
// like the install is in progress)
prereqTask := s.state.NewTask("prerequisites", "foo")
prereqTask.Set("snap-setup", &snapstate.SnapSetup{
SideInfo: &snap.SideInfo{
RealName: "foo",
},
})

tCore := s.state.NewTask("link-snap", "Pretend core gets installed")
tCore.Set("snap-setup", &snapstate.SnapSetup{
SideInfo: &snap.SideInfo{
RealName: "core",
Revision: snap.R(11),
},
})

// this makes sure that the prereq task runs first, but the link-snap task
// for core will be found by the prereq task handler
tCore.WaitFor(prereqTask)

chg := s.state.NewChange("sample", "...")
chg.AddTask(prereqTask)
chg.AddTask(tCore)

s.state.Unlock()
s.se.Ensure()
s.se.Wait()
s.state.Lock()

// prereq task is done, but the core task isn't done. this means that we
// didn't wait for the core task to finish, since we are remodeling
c.Check(prereqTask.Status(), Equals, state.DoneStatus)
c.Check(tCore.Status(), Equals, state.DoStatus)
}

func (s *prereqSuite) TestDoPrereqChannelEnvvars(c *C) {
os.Setenv("SNAPD_BASES_CHANNEL", "edge")
defer os.Unsetenv("SNAPD_BASES_CHANNEL")
Expand Down
Loading