Skip to content

daemon, o/i/apparmorprompting: implement ShutDown for InterfacesRequestsManager - #17147

Open
natibek wants to merge 8 commits into
canonical:masterfrom
natibek:generalize-shutdown-for-prompting-handler
Open

daemon, o/i/apparmorprompting: implement ShutDown for InterfacesRequestsManager#17147
natibek wants to merge 8 commits into
canonical:masterfrom
natibek:generalize-shutdown-for-prompting-handler

Conversation

@natibek

@natibek natibek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Implement ShutDown for the InterfacesRequestsManager to replace the existing shutdown process that uses c.d.tomb.Dying() to signal snapdShuttingDown.

Tracked with: SNAPDENG-36591

@github-actions github-actions Bot added Needs Documentation -auto- Label automatically added which indicates the change needs documentation Run only one system Only runs spread tests on one system labels Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Fri Jul 24 02:14:56 UTC 2026
The following results are from: https://github.com/canonical/snapd/actions/runs/30049138295

Failures:

Preparing:

  • openstack-arm:ubuntu-24.04-arm-64:tests/main/snap-seccomp-blocks-certain-creat
  • openstack-arm:ubuntu-24.04-arm-64:tests/main/snap-seccomp-blocks-certain-mknod

Executing:

  • openstack:centos-9-64:tests/main/prepare-image-validation-sets
  • openstack:opensuse-tumbleweed-64:tests/main/manpages
  • openstack:opensuse-tumbleweed-64:tests/main/services-watchdog
  • openstack:ubuntu-22.04-64:tests/main/lxd-mount-units

Skipped tests from snapd-testing-skip

If you wish to have any of the below tests run in your PR, in your PR description, add 'unskip:' followed by a copy-and-pasted list of the below tests you wish to run (unskip plus test list must be valid yaml)

  • garden:ubuntu-25.10-64:tests/main/apparmor-prompting-support
  • openstack-arm:ubuntu-24.04-arm-64:tests/main/i18n
  • openstack-arm:ubuntu-core-24-arm-64:tests/main/i18n
  • openstack:debian-sid-64:tests/main/interfaces-network-status-classic
  • openstack:debian-sid-64:tests/main/interfaces-xdg-portal-permission-store
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-integration-tests
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-smoke
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-24.04-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-24.04-64:tests/main/i18n
  • openstack:ubuntu-24.04-64:tests/main/interfaces-requests-activates-handlers
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-25.10-64:tests/main/interfaces-requests-activates-handlers
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-26.04-64:tests/main/i18n
  • openstack:ubuntu-26.04-64:tests/main/interfaces-requests-activates-handlers

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.36364% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.87%. Comparing base (e040201) to head (89f274a).
⚠️ Report is 180 commits behind head on master.

Files with missing lines Patch % Lines
overlord/ifacestate/ifacemgr.go 75.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #17147      +/-   ##
==========================================
- Coverage   79.11%   78.87%   -0.25%     
==========================================
  Files        1386     1401      +15     
  Lines      193780   196251    +2471     
  Branches     2466     2462       -4     
==========================================
+ Hits       153315   154789    +1474     
- Misses      31273    32176     +903     
- Partials     9192     9286      +94     
Flag Coverage Δ
unittests 78.87% <86.36%> (-0.25%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@natibek
natibek force-pushed the generalize-shutdown-for-prompting-handler branch from 4bc5889 to 284fd8a Compare June 18, 2026 16:17
@natibek
natibek marked this pull request as ready for review June 18, 2026 16:21
@github-actions github-actions Bot removed the Run only one system Only runs spread tests on one system label Jun 18, 2026
@natibek
natibek force-pushed the generalize-shutdown-for-prompting-handler branch from 284fd8a to 3801a39 Compare June 22, 2026 18:35

@pedronis pedronis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question

// ShutDown stops the listener, prompt DB, and rule DB from receiving new
// requests.
func (m *InterfacesRequestsManager) ShutDown() {
if m.shutdown {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seems we might need a lock around this flag? cc @andrewphelpsj

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I don't think we need a dedicated bool to mark whether this has already been done, this would be equivalent:

select {
case <-m.snapdShuttingDown:
	return
default:
	// we're about to shut down for the first time
}

But either way Samuele's right and we need some sort of synchronization. I think a sync.Once may be the most semantically clear approach, something like:

m.shutdownOnce.Do(func() {
	close(m.snapdShuttingDown)
})

then there's no need for a check at all, we can just unconditionally do this and the sync.Once guarantees the function is only invoked once. Atomics under the hood I believe.

@Rnfudge02 Rnfudge02 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me, but I agree with Samuele that we probably want a lock inside of the ShutDown function.

@natibek
natibek requested a review from olivercalder June 29, 2026 18:55

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, nice work! Two main things:

  1. I think we need to close snapdShuttingDown via a sync.Once, rather than a dedicated bool (which would need to be atomic) -- sync.Once is basically a nice wrapper around an atomic compare-and-exchange which ensures you only call a closure once.

  2. I think we need a new case in the prompting manager run loop, so it knows to stop waiting for requests once ShutDown has been called. I'm not absolutely certain this is correct, since this will cause disconnect() to be called, and thus be equivalent to simply calling m.Stop(). And Ask() already waits on m.tomb.Dying().

I think the big question is whether we want keep ShutDown and Stop distinct from the POV of the prompting manager. If not, then we can have m.ShutDown()simply callm.Stop(), which accomplishes what we want since everything waits on m.tomb.Dying(). Otherwise, some of the old comments in the test are wrong, any my comment that we should add a select case for m.snapdShuttingDown` to the run loop is also wrong.

My gut says we want these to be distinct, and ShutDown to purely affect the Ask method, not prevent requests from the listener or other API requests to e.g. reply to a prompt. So in that case we don't want to listen on m.snapdShuttingDown in the run loop.

// ShutDown stops the listener, prompt DB, and rule DB from receiving new
// requests.
func (m *InterfacesRequestsManager) ShutDown() {
if m.shutdown {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I don't think we need a dedicated bool to mark whether this has already been done, this would be equivalent:

select {
case <-m.snapdShuttingDown:
	return
default:
	// we're about to shut down for the first time
}

But either way Samuele's right and we need some sort of synchronization. I think a sync.Once may be the most semantically clear approach, something like:

m.shutdownOnce.Do(func() {
	close(m.snapdShuttingDown)
})

then there's no need for a check at all, we can just unconditionally do this and the sync.Once guarantees the function is only invoked once. Atomics under the hood I believe.

// InterfacesRequestsManager needs to stop receiving requests and
// finish handling existing requests.
snapdShuttingDown chan struct{}
shutdown bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
shutdown bool
shutdownOnce sync.Once

// favor of a manager-level shutdown triggered by the daemon stopping, most
// of this comment can be removed.
mgr.ShutDown()
c.Check(mgr.Stop(), IsNil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we don't need to call mgr.Stop() here anymore, we can do it at the end of the test now instead, like the other tests. That's what the comment was talking about iirc.

We can change this now after adding another case to the run loop which breaks out if m.snapdShuttingDown closes.

Comment thread overlord/ifacestate/apparmorprompting/prompting.go
Comment on lines 794 to 798
// Now Ask should be waiting for a reply. Stop the manager instead.
stopResultChan := make(chan error)
go func() {
select {
case stopResultChan <- mgr.Stop():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we call ShutDown now instead of Stop? Test needs some tweaks perhaps.

Edit: indeed, this test and the next one should be combined. There's no need to do this in a goroutine at all.

}

// Proceed with closing the manager
close(proceedWithClose)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then perhaps this whole MockListenerWithDelayedClose is unnecessary and can just be MockListener?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test should be removed/merged with the one above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or perhaps we want to keep them both, one for if Stop is called without having previously called ShutDown, for some reason.

…hannel and remove test for previous shutdown implementation
@natibek
natibek requested review from olivercalder and pedronis July 6, 2026 19:30

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, I think the implementation of the snapdShuttingDown channel now looks good.

The inclusion of the check for <-m.snapdShuttingDown in the run loop select statement now means that Stop() and ShutDown() have basically the same effect: stopping the run loop and closing all the backends. But they do so through parallel means (<-m.snapdShuttingDown vs m.tomb.Dying()).

I think we have two options:

  1. Make ShutDown() just stop Ask() method calls, but not stop the whole manager and backends --- in this case, we'd want to remove the new case from the run loop's select
  2. Make ShutDown() and Stop() have the same effect --- in this case, I think we don't really need two parallel concepts of shutdown/stop signalling: everywhere that <-m.snapdShuttingDown is checked, we also already check <-m.tomb.Dying(), and the effect is the same (albeit with slightly different messages in some cases), so I think we can remove the new m.snapdShuttingDown channel entirely and just use m.tomb.Dying(), or better yet, just have ShutDown() call Stop() or vice versa (probably move existing logic from Stop() into ShutDown() and then have Stop() call Shutdown()).

I don't have a strong preference, I'm curious what @pedronis thinks. There's probably not precedent for stopping a manager via the ShutDown() method instead of Stop() (since the former didn't exist until the previous PR), but perhaps that would be the simplest and most consistent approach, and we could merge the two and just use the tomb? Otherwise I think reducing the scope of ShutDown() to just affect the Ask() method would be good too.

}
case <-m.snapdShuttingDown:
logger.Debugf("InterfacesRequestsManager is shutting down")
break run_loop

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This causes the loop to break, which causes disconnect() to be called. So the effect of this is that calling ShutDown() causes all the backends to be closed, just like Stop() does.

So basically, ShutDown() has the same effect as Stop() if this case <-m.snapdShuttingDown is added to this select statement in the run loop.

My gut says this is probably not what we want? At least, it will cause Stop() and ShutDown() to have basically the same effect through parallel means. m.snapdShuttingDown and m.tomb.Dying() are checked together everywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that doesn't sound consistent with how ShutDown is used for the HookManager either. Would just be more confusing to conflate the ShutDown and Stop methods.

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! Generally +1 from me. Question about the removed test but otherwise looks good.

The fact that we select on both m.snapdShuttingDown and m.tomb.Dying() in the Ask() method select statements suggests that we should perhaps be testing both cases?

And one last thing: I don't know if it's the case, but we might want to ensure that Ask always returns after the manager run loop exits, so that e.g. there's no reply which has been received successfully but not handed off to the Ask() method call which originated it (I think we already handle this case correctly), or no other change in rules which happens to apply to an outstanding Ask() call but the manager is stopped (tomb killed) causing the Ask() call to abort prior to receiving the ensuing response, which is still in flight because the manager handler is still operating and has not got back to the run loop to observe the m.tomb.Dying() yet.

The way we could implement that is removing the m.tomb.Dying() checks from the Ask() method and instead firing off something like this at the start of the run method:

m.tomb.Go(func() error {
	<-m.tomb.Dying()
	m.shutdownOnce.Do(func() { close(m.snapdShuttingDown) })
})

What do you think about this?

c.Check(err, Equals, prompting_errors.ErrPromptingClosed)
}

func (s *apparmorpromptingSuite) TestAskShutdownBeforeReply(c *C) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's probably fine to remove this test, but does it decrease coverage to do so? I think we end up testing the snapdShuttingDown cases but now missing the m.tomb.Dying() cases in the select statements, yes?

}

// XXX: this test only exists since there are currently two ways to tell Ask to
// stop waiting: the manager closing, and the snapdShuttingDown channel closing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That is, there are still two ways to tell Ask to stop waiting. And I think there will always be two ways if we want Stop() and ShutDown() to coexist and be different.

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I discussed this a bit with Samuele today. Here are some key takeaways:

  • Conceptually, ShutDown() means "get into a state such that we don't expect requests from the snaps anymore"
    • This is very clear for the hook manager: snap hooks can do many things, such as install a component, which is a call to the snapd API. If the snapd-snap.socket were closed, then that hook's call to the API would fail, so the hook would fail, so we need to ensure that hooks are done making any calls to the API before we shut down the daemon socket.
    • For the prompting manager, it's a bit different: the daemon socket can't close until all outstanding connections close, and if one of those connections has called Ask(), that call would block until Stop() is called. But Stop() isn't called until after the daemon has closed the socket. So there would be a deadlock without a way to stop the Ask() calls prior to closing the daemon socket.
    • Other API calls and calls into the prompting backends are more short-lived, and don't come from something running within snapd which needs to be done before closing the socket. So Ask() is different and makes sense to be stopped early without closing the rest of the prompting backends.
  • There may be a long delay between ShutDown() being called and Stop(), and we want to ensure that the prompting manager doesn't receive the ShutDown() call until as late in that process as possible
    • The hook manager may wait up to 10 minutes for hooks to finish
    • Blocking Ask() requests for 10 minutes is not good, as it will block services like wireplumber from connecting streams for snaps due to retrying repeatedly after the 503 response
    • Manager methods (e.g. ShutDown()) are IIRC called sequentially on each of the managers, so we want to make sure ShutDown() is called on the hook manager before the prompting manager -- this is probably already the case, but want to confirm

And regarding this:

And one last thing: I don't know if it's the case, but we might want to ensure that Ask always returns after the manager run loop exits, so that e.g. there's no reply which has been received successfully but not handed off to the Ask() method call which originated it (I think we already handle this case correctly), or no other change in rules which happens to apply to an outstanding Ask() call but the manager is stopped (tomb killed) causing the Ask() call to abort prior to receiving the ensuing response, which is still in flight because the manager handler is still operating and has not got back to the run loop to observe the m.tomb.Dying() yet.

I was wrong, I think this is already handled correctly. If a reply call is received by the API, it needs to successfully call the Reply() method on the request, which calls sendResponse(), which contains a select on m.snapdShuttingDown to return an error. So the Reply() call will error out and that is passed all the way back up to the API, which results in a 503 response to the reply API request. So everything is fine here.

Your latest change around making m.snapdShuttingDown only affect Ask() is the correct decision.

@natibek
natibek requested a review from olivercalder July 8, 2026 19:12

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! Looks good, just one last comment about code organization in the interface manager.

Comment on lines +326 to +331
m.interfacesRequestsManagerMu.Lock()
defer m.interfacesRequestsManagerMu.Unlock()
if m.interfacesRequestsManager == nil {
return
}
interfacesRequestsManagerShutDown(m.interfacesRequestsManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It may be worth moving this into a dedicated shutDownInterfacesRequestsManager() method, like we have for stopInterfacesRequestsManager(), since the InterfacesRequestsManager is one of several managers managed by the InterfaceManager.

…nterfaceManager to shutDownInterfacesRequestsManger
@natibek
natibek requested a review from olivercalder July 9, 2026 20:48

@olivercalder olivercalder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks for all your work on this!

@olivercalder
olivercalder requested a review from Rnfudge02 July 9, 2026 20:54
@github-actions github-actions Bot added the Auto rerun spread Auto reruns spread up to 4 times in non-draft PRs w/ >=1 approval and <20 fails in any fund. system label Jul 9, 2026

@pedronis pedronis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looking good, question about the API we are defining though

c.Assert(s.se.Ensure(), IsNil)
}
s.se.ShutDown()
s.se.Stop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@olivercalder this poses an interesting question, should Stop imply ShutDown if ShutDown was not called yet, or we don't strictly needs this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't strictly need it, as everything which select on m.snapdShuttingDown still selects on m.tomb.Dying(), which is killed by Stop().

But conceptually this could be a nice thing to implement at the overlord/StateEngine level, I think. Or we could leave it up to each manager to ensure it is implemented correctly, as needed. I'm not sure, but we should decide on one of the choices and document that choice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@olivercalder @natibek I think the way to look at this is that Stop should work correctly even if ShutDown was not called and stop any (remaining) manager activity, so ShutDown is just a way to phase things (that's not too dissimilar for how shutdown and close work for sockets)

@Rnfudge02 Rnfudge02 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good!

@natibek
natibek requested a review from pedronis July 24, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto rerun spread Auto reruns spread up to 4 times in non-draft PRs w/ >=1 approval and <20 fails in any fund. system Needs Documentation -auto- Label automatically added which indicates the change needs documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants