Let's say I have a Task that I want to extend into the background with beginBackgroundTask(expirationHandler:)
. Furthermore, I'd like to leverage cooperative cancelation of subtasks when responding to the expiration handler. Unfortunately, the expirationHandler:
closure parameter is not async, so I'm unable to do something like:
actor MyTaskManagerOne {
var backgroundID = UIBackgroundTaskIdentifier.invalid
func start() {
Task {
let doTheWorkTask = Task {
await self.doTheWork()
}
backgroundID = await UIApplication.shared.beginBackgroundTask {
doTheWorkTask.cancel()
// next line: compile error, since not an async context
await doTheWorkTask.value // ensure work finishes up
// next line: generates MainActor compilation warnings despite docs allowing it
UIApplication.shared.endBackgroundTask(self.backgroundID)
}
await doTheWorkTask.value
}
}
func doTheWork() async {}
}
So instead, I think I have to do something like this. It, however, generates runtime warnings, since I'm not directly calling endBackgroundTask(_:)
at the end of the expirationHandler
:
actor MyTaskManagerTwo {
var backgroundID = UIBackgroundTaskIdentifier.invalid
func start() {
Task {
let doTheWorkTask = Task {
await self.doTheWork()
}
backgroundID = await UIApplication.shared.beginBackgroundTask {
doTheWorkTask.cancel()
// 1. not calling endBackgroundTask here generates runtime warnings
}
await doTheWorkTask.value
// 2. even though endBackgroundTask gets called
// here (as long as my cooperative cancellation
// implementations abort quickly in `doTheWork()`)
await UIApplication.shared.endBackgroundTask(self.backgroundID)
}
}
func doTheWork() async {}
}
As best I can tell, the MyTaskManagerTwo
actor works and does not cause a watchdog termination (as long as cancellation is sufficiently fast). It is, however, producing the following runtime warning:
Background task still not ended after expiration handlers were called: <_UIBackgroundTaskInfo: 0x302753840>: taskID = 2, taskName = Called by libswift_Concurrency.dylib, from <redacted>, creationTime = 9674 (elapsed = 28). This app will likely be terminated by the system. Call UIApplication.endBackgroundTask(_:) to avoid this.
Is the runtime warning ok to ignore in this case?
Basically, the kind of architecture you're trying to build here doesn't really work. Off the main thread, it's ENTIRELY possible that your task will start AFTER the system has already started expiring background tasks, which means you end up crashing because you failed to end the background task you just started.
More broadly, this kind of very fine grained task management makes it much harder to actual manage the work your app is doing. In practice, what typically ends up happening is something like this:
-
Your app is scheduling a larger collection of these small tasks and it works best if they "all" get done.
-
In your ininitial implementation, some other part/component of your app has it's own active background task. That task is what ACTUALLY keeps your app awake between every small task, so the work "all" gets done. Everything appears to work great, so you move on.
-
At some later point, the component from #2 changes, shortening or removing the background task. Suddenly your app starts suspending in the middle of the work that was previously finishing, even though nothing changed in the task code.
The right approach here is to manage your background task at a much higher level- not "what is the specific code I need to execute", but "what is the work may app is awake to do and have I finished all of that work".
On the specific warning here:
Background task still not ended after expiration handlers were called: <_UIBackgroundTaskInfo: 0x302753840>: taskID = 2, taskName = Called by libswift_Concurrency.dylib, from <redacted>, creationTime = 9674 (elapsed = 28). This app will likely be terminated by the system. Call UIApplication.endBackgroundTask(_:) to avoid this.
Is the runtime warning ok to ignore in this case?
Yes, but only under very specific circumstances. IF your app called "beginBackgroundTask" before task expiration time started, then it has ~10s call "endBackgroundTask" once expiration time starts.
If you KNOW your work it "short enough" AND you also know you're in a "safe" app state, then you can call "beginBackgroundTask", fire off the work, then call endBackgroundTask when the work is done.
In concrete terms, if you're saving a small amount of data in "applicationDidEnterBackground" the approach above may make more sense than trying to implement some kind of cancel. The save should finish LONG before task expiration and if SOMETHING interferes then a crash for failing to expire is probably better than expiring in the middle of the work you can't really cancel.
However, the key here is still that you need to call "beginBackgroundTask" in that safe context (main thread, before anything expires).
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware