Skip to content

Commit 311c288

Browse files
o/snapstate, o/s/backend: restart stopped services on error in stopSnapServices (#16861)
Transactionality improvement for `"stop-snap-services"` [[SNAPDENG-36594](https://warthogs.atlassian.net/browse/SNAPDENG-36594)] * When `stopSnapServices` encounters an error mid-flight (e.g. during refresh), restart any services that were already stopped so that the snap services remain functional * Disabled services are now queried before stopping and passed to `backend.StartServices`, so that in case of error leading to undo will only start previously enabled services. * For remove/disable stop reasons the target is to have services stopped, so undo is skipped. **Note:** currently `undoStopSnapServices` still restarts all enabled services for a remove/disable if a future task in the change fails. We plan to change that as part of snap removal robustness improvements (at least for remove). Add `UndoTracker` for rollback on in-flight error [[SNAPDENG-36593](https://warthogs.atlassian.net/browse/SNAPDENG-36593)] * To better manage transactionality, add `UndoTracker` a LIFO cleanup stack that lets do handlers incrementally register undo closures for each step. On error (excluding `*state.Wait` and `*state.Retry`), the closure returned by `NewUndoTracker` executes the undos in reverse order, rolling back partial progress without knowing the undo handler's implementation details. * The do handlers decide whether the undoes need to be registered with state lock held or released by passing either the `UndoTracker.Locked()` or `UndoTracker.Unlocked()` * `NullUndoer` and `TODOUndoer` are also added to make it clear where undos are not needed or needed but not yet setup. This also helps in removing the need for non-nil `Undoer` check before registering undos. * For `backend.StopServices`, the `Unlocked` adapter is used to register undoes
1 parent e1e891f commit 311c288

11 files changed

Lines changed: 697 additions & 39 deletions

File tree

overlord/snapstate/backend.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ type managerBackend interface {
9191
LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) error
9292
LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error
9393
StartServices(svcs []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, meter progress.Meter, tm timings.Measurer) error
94-
StopServices(svcs []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error
94+
// TODO: reduce the number of arguments here, perhaps by grouping some
95+
StopServices(svcs []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, reason snap.ServiceStopReason, undoer backend.Undoer, meter progress.Meter, tm timings.Measurer) error
9596
QueryDisabledServices(info *snap.Info, pb progress.Meter) (*wrappers.DisabledServices, error)
9697
MaybeSetNextBoot(info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error)
9798

overlord/snapstate/backend/backend.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ type Backend struct {
3030
preseed bool
3131
}
3232

33+
// Undoer collects undo actions to reverse system changes on error.
34+
type Undoer interface {
35+
AddUndo(f func() error)
36+
}
37+
3338
// Candidate is a test hook.
3439
func (b Backend) Candidate(*snap.SideInfo) {}
3540

overlord/snapstate/backend/export_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/snapcore/snapd/osutil/sys"
2828
"github.com/snapcore/snapd/snap"
2929
"github.com/snapcore/snapd/testutil"
30+
"github.com/snapcore/snapd/timings"
3031
"github.com/snapcore/snapd/wrappers"
3132
)
3233

@@ -76,3 +77,11 @@ func MockKernelEnsureKernelDriversTree(f func(kMntPts kernel.MountPoints, compsM
7677
func MockCgroupKillSnapProcesses(f func(ctx context.Context, snapName string) error) func() {
7778
return testutil.Mock(&cgroupKillSnapProcesses, f)
7879
}
80+
81+
func MockWrappersStartServices(f func(apps []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, opts *wrappers.StartServicesOptions, inter wrappers.Interacter, tm timings.Measurer) error) func() {
82+
return testutil.Mock(&wrappersStartServices, f)
83+
}
84+
85+
func MockWrappersStopServices(f func(svcs []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, opts *wrappers.StopServicesOptions, reason snap.ServiceStopReason, inter wrappers.Interacter, tm timings.Measurer) error) func() {
86+
return testutil.Mock(&wrappersStopServices, f)
87+
}

overlord/snapstate/backend/link.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939
)
4040

4141
var wrappersAddSnapdSnapServices = wrappers.AddSnapdSnapServices
42+
var wrappersStartServices = wrappers.StartServices
43+
var wrappersStopServices = wrappers.StopServices
4244
var cgroupKillSnapProcesses = cgroup.KillSnapProcesses
4345

4446
// LinkContext carries additional information about the current or the previous
@@ -244,11 +246,25 @@ func (b Backend) LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revisio
244246

245247
func (b Backend) StartServices(apps []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, meter progress.Meter, tm timings.Measurer) error {
246248
opts := &wrappers.StartServicesOptions{Enable: true}
247-
return wrappers.StartServices(apps, disabledSvcs, opts, meter, tm)
249+
return wrappersStartServices(apps, disabledSvcs, opts, meter, tm)
248250
}
249251

250-
func (b Backend) StopServices(apps []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error {
251-
return wrappers.StopServices(apps, removedSvcs, nil, reason, meter, tm)
252+
func (b Backend) StopServices(apps []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, reason snap.ServiceStopReason, undoer Undoer, meter progress.Meter, tm timings.Measurer) error {
253+
// Register the undo before stopping so that services are
254+
// started again even when StopServices fails partway through
255+
// (some services stopped, then an error on a later one).
256+
undoer.AddUndo(func() error {
257+
// Services need to be sorted according to their Before
258+
// and After requirements
259+
startupOrdered, err := snap.SortServices(apps)
260+
if err != nil {
261+
return fmt.Errorf("cannot sort services for undo: %v", err)
262+
}
263+
// StartServices filters out disabled services, so only
264+
// previously enabled services will be started again.
265+
return b.StartServices(startupOrdered, disabledSvcs, meter, tm)
266+
})
267+
return wrappersStopServices(apps, removedSvcs, nil, reason, meter, tm)
252268
}
253269

254270
func (b Backend) generateWrappers(s *snap.Info, linkCtx LinkContext) error {

overlord/snapstate/backend/link_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,65 @@ func (s *linkSuite) TestKillSnapApps(c *C) {
12181218
c.Assert(called, Equals, 1)
12191219
}
12201220

1221+
func (s *linkSuite) TestStartServices(c *C) {
1222+
var called int
1223+
restore := backend.MockWrappersStartServices(func(apps []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, opts *wrappers.StartServicesOptions, inter wrappers.Interacter, tm timings.Measurer) error {
1224+
called++
1225+
c.Assert(apps, HasLen, 1)
1226+
c.Check(apps[0].Name, Equals, "svc")
1227+
return nil
1228+
})
1229+
defer restore()
1230+
1231+
apps := []*snap.AppInfo{{Name: "svc"}}
1232+
err := s.be.StartServices(apps, nil, progress.Null, s.perfTimings)
1233+
c.Assert(err, IsNil)
1234+
c.Assert(called, Equals, 1)
1235+
}
1236+
1237+
type nullUndoer struct{}
1238+
1239+
func (nu nullUndoer) AddUndo(f func() error) {}
1240+
1241+
func (s *linkSuite) TestStopServices(c *C) {
1242+
var called int
1243+
restore := backend.MockWrappersStopServices(func(svcs []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, opts *wrappers.StopServicesOptions, reason snap.ServiceStopReason, inter wrappers.Interacter, tm timings.Measurer) error {
1244+
called++
1245+
c.Assert(svcs, HasLen, 1)
1246+
c.Check(svcs[0].Name, Equals, "svc")
1247+
return nil
1248+
})
1249+
defer restore()
1250+
1251+
apps := []*snap.AppInfo{{Name: "svc"}}
1252+
err := s.be.StopServices(apps, nil, nil, snap.StopReasonRefresh, &nullUndoer{}, progress.Null, s.perfTimings)
1253+
c.Assert(err, IsNil)
1254+
c.Assert(called, Equals, 1)
1255+
}
1256+
1257+
type fakeUndoer struct {
1258+
undoFuncs []func() error
1259+
}
1260+
1261+
func (u *fakeUndoer) AddUndo(f func() error) {
1262+
u.undoFuncs = append(u.undoFuncs, f)
1263+
}
1264+
1265+
func (s *linkSuite) TestStopServicesWithNotNilUndoerRegistersUndo(c *C) {
1266+
restore := backend.MockWrappersStopServices(func(svcs []*snap.AppInfo, removedSvcs map[string]*snap.AppInfo, opts *wrappers.StopServicesOptions, reason snap.ServiceStopReason, inter wrappers.Interacter, tm timings.Measurer) error {
1267+
c.Assert(svcs, HasLen, 1)
1268+
c.Check(svcs[0].Name, Equals, "svc")
1269+
return errors.New("mock StopServices error")
1270+
})
1271+
defer restore()
1272+
1273+
undoer := &fakeUndoer{}
1274+
apps := []*snap.AppInfo{{Name: "svc"}}
1275+
err := s.be.StopServices(apps, nil, nil, snap.StopReasonRefresh, undoer, progress.Null, s.perfTimings)
1276+
c.Assert(err, ErrorMatches, "mock StopServices error")
1277+
c.Assert(undoer.undoFuncs, HasLen, 1)
1278+
}
1279+
12211280
func (s *linkSuite) TestLinkSnapNilStateUnlockerError(c *C) {
12221281
err := s.be.LinkSnap(nil, nil, backend.LinkContext{}, nil)
12231282
c.Assert(err, ErrorMatches, "internal error: LinkContext.StateUnlocker cannot be nil")

overlord/snapstate/backend_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1482,7 +1482,7 @@ func (f *fakeSnappyBackend) StartServices(svcs []*snap.AppInfo, disabledSvcs *wr
14821482
return f.maybeErrForLastOp()
14831483
}
14841484

1485-
func (f *fakeSnappyBackend) StopServices(svcs []*snap.AppInfo, rmSvcs map[string]*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error {
1485+
func (f *fakeSnappyBackend) StopServices(svcs []*snap.AppInfo, rmSvcs map[string]*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, reason snap.ServiceStopReason, undoer backend.Undoer, meter progress.Meter, tm timings.Measurer) error {
14861486
meter.Notify("stop-services")
14871487

14881488
var svcNames []string
@@ -1504,6 +1504,14 @@ func (f *fakeSnappyBackend) StopServices(svcs []*snap.AppInfo, rmSvcs map[string
15041504
sort.Strings(rmSvcNames)
15051505
}
15061506

1507+
undoer.AddUndo(func() error {
1508+
startupOrdered, err := snap.SortServices(svcs)
1509+
if err != nil {
1510+
return fmt.Errorf("cannot sort services for undo: %v", err)
1511+
}
1512+
return f.StartServices(startupOrdered, disabledSvcs, meter, tm)
1513+
})
1514+
15071515
f.appendOp(&fakeOp{
15081516
op: fmt.Sprintf("stop-snap-services:%s", reason),
15091517
path: svcSnapMountDir(svcs),

overlord/snapstate/handlers.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3547,7 +3547,7 @@ func (m *SnapManager) undoStartSnapServices(t *state.Task, _ *tomb.Tomb) error {
35473547

35483548
// stop the services
35493549
st.Unlock()
3550-
err = m.backend.StopServices(svcs, nil, stopReason, progress.Null, perfTimings)
3550+
err = m.backend.StopServices(svcs, nil, nil, stopReason, NullUndoer, progress.Null, perfTimings)
35513551
st.Lock()
35523552
if err != nil {
35533553
return err
@@ -3556,11 +3556,30 @@ func (m *SnapManager) undoStartSnapServices(t *state.Task, _ *tomb.Tomb) error {
35563556
return nil
35573557
}
35583558

3559-
func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) error {
3559+
func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) (retErr error) {
35603560
st := t.State()
35613561
st.Lock()
35623562
defer st.Unlock()
35633563

3564+
var stopReason snap.ServiceStopReason
3565+
if err := t.Get("stop-reason", &stopReason); err != nil && !errors.Is(err, state.ErrNoState) {
3566+
return err
3567+
}
3568+
3569+
// For remove/disable, the end goal is to have services stopped, so undo is skipped
3570+
// to avoid restarting the services in case of error. This aligns with making
3571+
// remove/disable best effort towards achieving their end goal.
3572+
// On the other hand, for example for refresh the end goal is to have the services
3573+
// running (just from a different revision), so undo is needed to restart the
3574+
// services in case of error.
3575+
undoerUnlocked := NullUndoer
3576+
skipUndo := stopReason == snap.StopReasonRemove || stopReason == snap.StopReasonDisable
3577+
if !skipUndo {
3578+
ut, undoOnError := NewUndoTracker(t, &retErr)
3579+
defer undoOnError()
3580+
undoerUnlocked = ut.Unlocked()
3581+
}
3582+
35643583
perfTimings := state.TimingsForTask(t)
35653584
defer perfTimings.Save(st)
35663585

@@ -3578,11 +3597,6 @@ func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) error {
35783597
return nil
35793598
}
35803599

3581-
var stopReason snap.ServiceStopReason
3582-
if err := t.Get("stop-reason", &stopReason); err != nil && !errors.Is(err, state.ErrNoState) {
3583-
return err
3584-
}
3585-
35863600
pb := NewTaskProgressAdapterUnlocked(t)
35873601
st.Unlock()
35883602
defer st.Lock()
@@ -3606,19 +3620,22 @@ func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) error {
36063620
}
36073621
}
36083622

3609-
// stop the services
3610-
err = m.backend.StopServices(svcs, rmSvcs, stopReason, pb, perfTimings)
3623+
// Query disabled services before stopping. This serves two purposes:
3624+
// 1. The undoer passes it to StartServices to skip disabled services,
3625+
// so only previously enabled services are started again on error.
3626+
// 2. The result is persisted in the task state for undoStopSnapServices
3627+
// to similarly know which services should remain disabled when starting
3628+
// services again during undo.
3629+
// backend.StopServices does not change which services are disabled as it uses
3630+
// the default StopServicesOptions.Disable = false opts, so a single
3631+
// query before the stop is sufficient for both uses.
3632+
disabledServices, err := m.queryDisabledServices(currentInfo, pb)
36113633
if err != nil {
36123634
return err
36133635
}
36143636

3615-
// get the disabled services after we stopped all the services.
3616-
// this list is not meant to save what services are disabled at any given
3617-
// time, specifically just what services are disabled while systemd loses
3618-
// track of the services. this list is also used to determine what services are enabled
3619-
// when we start services of a new revision of the snap in
3620-
// start-snap-services handler.
3621-
disabledServices, err := m.queryDisabledServices(currentInfo, pb)
3637+
// stop the services
3638+
err = m.backend.StopServices(svcs, rmSvcs, disabledServices, stopReason, undoerUnlocked, pb, perfTimings)
36223639
if err != nil {
36233640
return err
36243641
}
@@ -3800,7 +3817,8 @@ func (m *SnapManager) doKillSnapApps(t *state.Task, _ *tomb.Tomb) (retErr error)
38003817
pb := NewTaskProgressAdapterUnlocked(t)
38013818

38023819
// Make sure snap services are stopped because they may have started through snapctl
3803-
err = m.backend.StopServices(svcs, nil, snap.ServiceStopReason(reason), pb, perfTimings)
3820+
// TODO: replace TODOUndoer with an UndoTracker for non-remove reason
3821+
err = m.backend.StopServices(svcs, nil, nil, snap.ServiceStopReason(reason), TODOUndoer, pb, perfTimings)
38043822
if err != nil {
38053823
return err
38063824
}

overlord/snapstate/snapstate_test.go

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7995,15 +7995,15 @@ func (s *snapmgrTestSuite) TestStopSnapServicesUndo(c *C) {
79957995
c.Check(t.Status(), Equals, state.UndoneStatus)
79967996

79977997
expected := fakeOps{
7998+
{
7999+
op: "current-snap-service-states",
8000+
disabledServices: []string{"svc1"},
8001+
},
79988002
{
79998003
op: "stop-snap-services:",
80008004
path: filepath.Join(dirs.SnapMountDir, "hello-snap/1"),
80018005
services: []string{"svc1", "svc2"},
80028006
},
8003-
{
8004-
op: "current-snap-service-states",
8005-
disabledServices: []string{"svc1"},
8006-
},
80078007
{
80088008
op: "start-snap-services",
80098009
services: []string{"svc1", "svc2"},
@@ -8067,14 +8067,14 @@ func (s *snapmgrTestSuite) TestStopSnapServicesErrInUndo(c *C) {
80678067
c.Check(t.Status(), Equals, state.ErrorStatus)
80688068

80698069
expected := fakeOps{
8070+
{
8071+
op: "current-snap-service-states",
8072+
},
80708073
{
80718074
op: "stop-snap-services:",
80728075
path: filepath.Join(dirs.SnapMountDir, "hello-snap/1"),
80738076
services: []string{"svc1", "svc2"},
80748077
},
8075-
{
8076-
op: "current-snap-service-states",
8077-
},
80788078
{
80798079
// failed after this op
80808080
op: "start-snap-services",
@@ -8085,6 +8085,81 @@ func (s *snapmgrTestSuite) TestStopSnapServicesErrInUndo(c *C) {
80858085
c.Check(s.fakeBackend.ops, DeepEquals, expected)
80868086
}
80878087

8088+
func (s *snapmgrTestSuite) testStopSnapServicesOnStopFailure(c *C, reason snap.ServiceStopReason) {
8089+
s.state.Lock()
8090+
defer s.state.Unlock()
8091+
8092+
prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
8093+
s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1"}
8094+
8095+
defer func() {
8096+
s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
8097+
}()
8098+
8099+
si := &snap.SideInfo{RealName: "hello-snap", SnapID: "hello-snap-id", Revision: snap.R(1)}
8100+
snaptest.MockSnap(c, servicesSnap, si)
8101+
8102+
snapstate.Set(s.state, "hello-snap", &snapstate.SnapState{
8103+
Active: true,
8104+
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
8105+
Current: si.Revision,
8106+
SnapType: "app",
8107+
})
8108+
8109+
snapstate.MockSnapReadInfo(snap.ReadInfo)
8110+
8111+
chg := s.state.NewChange("services..", "")
8112+
t := s.state.NewTask("stop-snap-services", "")
8113+
sup := &snapstate.SnapSetup{SideInfo: si}
8114+
t.Set("snap-setup", sup)
8115+
t.Set("stop-reason", reason)
8116+
chg.AddTask(t)
8117+
8118+
// Mock error in StopServices
8119+
s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
8120+
if op.op == fmt.Sprintf("stop-snap-services:%s", reason) {
8121+
return fmt.Errorf("mock stop failure")
8122+
}
8123+
return nil
8124+
}
8125+
8126+
s.settle(c)
8127+
8128+
c.Check(chg.Status(), Equals, state.ErrorStatus)
8129+
c.Check(t.Status(), Equals, state.ErrorStatus)
8130+
8131+
expected := fakeOps{
8132+
{
8133+
op: "current-snap-service-states",
8134+
disabledServices: []string{"svc1"},
8135+
},
8136+
{
8137+
op: fmt.Sprintf("stop-snap-services:%s", reason),
8138+
path: filepath.Join(dirs.SnapMountDir, "hello-snap/1"),
8139+
services: []string{"svc1", "svc2"},
8140+
},
8141+
}
8142+
skipUndo := reason == snap.StopReasonRemove || reason == snap.StopReasonDisable
8143+
if !skipUndo {
8144+
expected = append(expected, fakeOp{
8145+
// Should be triggered due to mock error in StopServices
8146+
op: "start-snap-services",
8147+
services: []string{"svc1", "svc2"},
8148+
disabledServices: []string{"svc1"},
8149+
path: filepath.Join(dirs.SnapMountDir, "hello-snap/1"),
8150+
})
8151+
}
8152+
c.Check(s.fakeBackend.ops, DeepEquals, expected)
8153+
}
8154+
8155+
func (s *snapmgrTestSuite) TestStopSnapServicesStartsStoppedServicesOnStopFailureForRefresh(c *C) {
8156+
s.testStopSnapServicesOnStopFailure(c, snap.StopReasonRefresh)
8157+
}
8158+
8159+
func (s *snapmgrTestSuite) TestStopSnapServicesDoesNotStartStoppedServicesOnStopFailureForRemove(c *C) {
8160+
s.testStopSnapServicesOnStopFailure(c, snap.StopReasonRemove)
8161+
}
8162+
80888163
func (s *snapmgrTestSuite) TestEnsureAutoRefreshesAreDelayed(c *C) {
80898164
s.state.Lock()
80908165
defer s.state.Unlock()

0 commit comments

Comments
 (0)