Swift: Have a timeout for async/await function - swift

Is there a way to attach a timeout to stop an async function if it takes too long?
Here's a simplified version of my code:
func search() async -> ResultEnum {
// Call multiple Apis one after the other
}
Task.init {
let searchResult = await search()
switch searchResult {
// Handle all result scenarios
}
}
I would like to have a deadline for the search() async function to provide a result, otherwise it should terminate and return ResultEnum.timeout.

Thank you Rob for your comments, and for the link you provided.
I had to make some changes though, For some reason the initial task fetchTask kept going even after cancellation, until I added Task.checkCancellation() to it.
Here's what the code looks like now, if anyone is facing a similar issue:
func search() async throws -> ResultEnum {
// This is the existing method as per my initial question.
// It calls multiple Apis one after the other, then returns a result.
}
// Added the below method to introduce a deadline for search()
func search(withTimeoutSecs: Int) async {
let fetchTask = Task {
let taskResult = try await search()
try Task.checkCancellation()
// without the above line, search() kept going until server responded long after deadline.
return taskResult
}
let timeoutTask = Task {
try await Task.sleep(nanoseconds: UInt64(withTimeoutSecs) * NSEC_PER_SEC)
fetchTask.cancel()
}
do {
let result = try await fetchTask.value
timeoutTask.cancel()
return result
} catch {
return ResultEnum.failed(NetworkError.timeout)
}
}
// Call site: Using the function (withTimeout:) instead of ()
Task.init {
let searchResult = await search(withTimeoutSecs: 6)
switch searchResult {
// Handle all result scenarios
}
}

Related

How to ignore async let throws error when save response in tuple?

i have a code like this:
Task {
async let configsReq = self.interactor.getConfigs()
async let optionsReq = self.interactor.getOptions()
async let updateStateReq = self.interactor.getAppUpdateState()
async let contactsReq = self.interactor.getContactOptions()
var config: Config?
var options: AvailableOptions?
var updateState: UpdateType?
var contacts: ContactOptions?
do {
config = try await configsReq
} catch {
config = nil
}
do {
options = try await optionsReq
} catch {
options = nil
}
do {
updateState = try await updateStateReq
} catch {
updateState = nil
}
do {
contacts = try await contactsReq
} catch {
contacts = nil
}
self.goToNextPage()
}
in this case it does not matter for me that the requests get correct response or throws error. i don't want to block user to get correct response.
And also I want to make sure that all my requests are answered (correct or error response) to take the user to the next page
how can i write these codes cleaner and better with new swift concurrency?
i tried like this (but i could not get match error to each related request):
Task {
async let configs = self.interactor.getConfigs()
async let options = self.interactor.getOptions()
async let updateState = self.interactor.getAppUpdateState()
async let contacts = self.interactor.getContactOptions()
do {
let array = try await [configs, options, updateState, contacts]
} catch {
print(error)
}
}
If I understand the question correctly, you want to:
“match error to each related request”, but that
you want to proceed regardless of success or failure, as it “does not matter for me that the requests get correct response or throws error”.
If that is the pattern you are looking for, I might suggest using Task result:
async let configsReq = Task { try await interactor.getConfigs() }
async let optionsReq = Task { try await interactor.getOptions() }
async let stateReq = Task { try await interactor.getAppUpdateState() }
async let contactsReq = Task { try await interactor.getContactOptions() }
let config = await configsReq.result
let options = await optionsReq.result
let state = await stateReq.result
let contacts = await contactsReq.result
goToNextPage(config: config, options: options, state: state, contacts: contacts)
Or, more concisely:
async let configs = Task { try await interactor.getConfigs() }
async let options = Task { try await interactor.getOptions() }
async let state = Task { try await interactor.getAppUpdateState() }
async let contacts = Task { try await interactor.getContactOptions() }
await goToNextPage(config: configs.result, options: options.result, state: state.result, contacts: contacts.result)
Where goToNextPage might be defined as:
func goToNextPage(
config: Result<Config, Error>,
options: Result<AvailableOptions, Error>,
state: Result<UpdateType, Error>,
contacts: Result<ContactOptions, Error>
) { … }
That way, goToNextPage can look at the .success or .failure for each, to retrieve either the value or error associated with each of the four requests.
Needless to say, you also could have four properties for these four requests, and then goToNextPage could refer to those, rather than taking them as parameters to the method. It’s functionally the same thing, but you have to decide either local vars that are passed to the next method or update properties that are accessed by the next method.
You asked:
… if we don't want to use Result anymore, how can do that?
Yes, we do not use Result very much, anymore, as that was historically a pattern for returning either value or error in traditional asynchronous patterns, and nowadays we try a series of tasks, catch thrown errors, but generally early exit once one of them fails.
But if you really want to capture the success and failure for each of the four concurrent requests, then Result encapsulates that quite well.
I would make a little helper that helps wrap the error into a Result:
extension Result {
init(asyncCatching block: () async throws -> Success) async where Failure == Error {
do {
self = .success(try await block())
} catch {
self = .failure(error)
}
}
}
In case of errors, you even get the Error object for each getXXX method, rather than just a nil. Of course, if you really just want a nil, you can write a helper that returns optionals instead.
// this is essentially like refactoring out the repeated parts of your first code
func asyncCatchWithNil<Result>(function: () async throws -> Result) async -> Result? {
do {
return try await function()
} catch {
return nil
}
}
Then you could do:
Task {
async let configs = Result(asyncCatching: self.interactor.getConfigs)
async let options = Result(asyncCatching: self.interactor.getOptions)
async let updateState = Result(asyncCatching: self.interactor.getAppUpdateState)
async let contacts = Result(asyncCatching: self.interactor.getContactOptions)
/* or
async let configs = asyncCatchWithNil(function: self.interactor.getConfigs)
async let options = asyncCatchWithNil(function: self.interactor.getOptions)
async let updateState = asyncCatchWithNil(function: self.interactor.getAppUpdateState)
async let contacts = asyncCatchWithNil(function: self.interactor.getContactOptions)
*/
let (configsResult, optionsResult, updateStateResult, contactsResult)
= await (configs, options, updateState, contacts)
// you can inspect each result here if you'd like
self.goToNextPage()
}
The idea here is that you get a type that can contain both the response and error at the point of async let, rather than catching the error later.

How to wait for a bunch of async calls to finish to return a result?

I understand the basic usage of async/await but I'm a bit confused of what I should do in this specific example. I have an async function called save(url: URL) which mimics a function that would take a local URL as its parameter, and asynchronously return a String which would be, say the new remote URL of this file:
struct FileSaver {
// In this example I'll simulate a network request
// with a random async time and return the original file URL
static func save(_ url: URL) async throws -> String {
try await Task.sleep(
seconds: Double(arc4random_uniform(10)) / 10
)
return url.absoluteString
}
}
extension Task where Success == Never, Failure == Never {
public static func sleep(
seconds: Double
) async throws {
return try await sleep(
nanoseconds: UInt64(seconds) * 1_000_000_000
)
}
}
Now say I have 4 local files, and I want to save these files in parallel, but only return when they are all done saving. I read the documentation but still I'm a bit confused if I should use an array or a TaskGroup.
I would like to do something like this:
// in FileSaver
static func save(
files: [URL]
) async throws -> [String] {
// how to call save(url) for each file in `files`
// in parallel and only return when every file is saved?
}
Thank you for your help
We use task group to perform the requests in parallel and then await the whole group.
The trick, though, is that they will not finish in the same order that we started them. So, if you want to preserve the order of the results, we can return every result as a tuple of the input (the URL) and the output (the string). We then collect the group result into a dictionary, and the map the results back to the original order:
static func save(files: [URL]) async throws -> [String] {
try await withThrowingTaskGroup(of: (URL, String).self) { group in
for file in files {
group.addTask { (file, try await save(file)) }
}
let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
return files.compactMap { dictionary[$0] }
}
}
There are other techniques to preserve the order of the results, but hopefully this illustrates the basic idea.
I think withThrowingTaskGroup is what you are looking for:
static func save(
files: [URL]
) async throws -> [String] {
try await withThrowingTaskGroup(of: String.self) { group in
for file in files {
group.addTask { try await save(file) }
}
var strings = [String]()
for try await string in group {
strings.append(string)
}
return strings
}
}

Swift async method call for a command line app

I'm trying to reuse some async marked code that works great in a SwiftUI application in a simple Swift-Command line tool.
Lets assume for simplicity that I'd like to reuse a function
func fetchData(base : String) async throws -> SomeDate
{
let request = createURLRequest(forBase: base)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw FetchError.urlResponse
}
let returnData = try! JSONDecoder().decode(SomeData.self, from: data)
return returnData
}
in my command line application.
A call like
let allInfo = try clerk.fetchData("base")
in my "main-function" gives the error message 'async' call in a function that does not support concurrency.
What is the correct way to handle this case.
Thanks
Patrick
To call an async method the call must take place inside an async method or wrapped in a Task.
Further the method must be called wirh await
Task {
do {
let allInfo = try await clerk.fetchData("base")
} catch {
print(error)
}
}
You can make the entry point of the CLI async with this syntax
#main
struct CLI {
static func main() async throws {
let args = CommandLine.arguments
...
}
The name of the struct is arbitrary.
If you are using Argument Parser framework then from version 1.0 it supports async context. You have to make your struct conforms to protocol AsyncParsableCommand. It creates context in which async function can be run safely.

What is the correct way to await the completion of two Tasks in Swift 5.5 in a function that does not support concurrency?

I have an app that does some processing given a string, this is done in 2 Tasks. During this time i'm displaying an animation. When these Tasks complete i need to hide the animation. The below code works, but is not very nice to look at. I believe there is a better way to do this?
let firTask = Task {
/* Slow-running code */
}
let airportTask = Task {
/* Even more slow-running code */
}
Task {
_ = await firTask.result
_ = await airportTask.result
self.isVerifyingRoute = false
}
Isn't the real problem that this is a misuse of Task? A Task, as you've discovered, is not really of itself a thing you can await. If the goal is to run slow code in the background, use an actor. Then you can cleanly call an actor method with await.
let myActor = MyActor()
await myActor.doFirStuff()
await myActor.doAirportStuff()
self.isVerifyingRoute = false
However, we also need to make sure we're on the main thread when we talk to self — something that your code omits to do. Here's an example:
actor MyActor {
func doFirStuff() async {
print("starting", #function)
await Task.sleep(2 * 1_000_000_000)
print("finished", #function)
}
func doAirportStuff() async {
print("starting", #function)
await Task.sleep(2 * 1_000_000_000)
print("finished", #function)
}
}
func test() {
let myActor = MyActor()
Task {
await myActor.doFirStuff()
await myActor.doAirportStuff()
Task { #MainActor in
self.isVerifyingRoute = false
}
}
}
Everything happens in the right mode: the time-consuming stuff happens on background threads, and the call to self happens on the main thread. A cleaner-looking way to take care of the main thread call, in my opinion, would be to have a #MainActor method:
func test() {
let myActor = MyActor()
Task {
await myActor.doFirStuff()
await myActor.doAirportStuff()
self.finish()
}
}
#MainActor func finish() {
self.isVerifyingRoute = false
}
I regard that as elegant and clear.
I would make the tasks discardable with an extension. Perhaps something like this:
extension Task {
#discardableResult
func finish() async -> Result<Success, Failure> {
await self.result
}
}
Then you could change your loading task to:
Task {
defer { self.isVerifyingRoute = false }
await firTask.finish()
await airportTask.finish()
}

'async' call in a function that does not support concurrency

I'm trying to use async/await with Swift 5.5. I have my async function, but whenever I try to call it, I get this error:
'async' call in a function that does not support concurrency
Here's the code sample:
class TryThis {
func getSomethingLater(_ number: Double) async -> String {
// test - sleep for 3 seconds, then return
Thread.sleep(forTimeInterval: 3)
return String(format: ">>>%8.2f<<<", number)
}
}
let tryThis = TryThis()
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
What's the solution for this??
The answer here is that the "await getSomethingLater" must be called from an async context. Literally that means changing this:
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
into this:
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
So the whole thing becomes:
class TryThis {
func getSomethingLater(_ number: Double) async -> String {
// test - sleep for 3 seconds, then return
Thread.sleep(forTimeInterval: 3)
return String(format: ">>>%8.2f<<<", number)
}
}
let tryThis = TryThis()
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
Here's the output:
result: >>> 3.14<<<
There's great info in the Meet async/await in Swift video from WWDC21.
When calling async code (including actor fields) from non-async code, you have to wrap it in a Task:
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
If you need it to escape that async context, you can use traditional callbacks:
func awaitedResult(callback: (String) -> Void) {
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
callback(result)
}
}
awaitedResult { result in
print("result: \(result)")
}
Swift by Sundell also offers some alternatives involving Combine
Further reading: Concurrency — The Swift Programming Language (Swift 5.5)
The original error is produced because "top-level" await is not yet supported in Swift (that is, code at the global scope).
The error just means you need to provide the async function with an asynchronous context, like using Task, Task.detached or a TaskGroup, depending on the behaviour you want.
Task.detached {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
However, your code snippet should work in the future when top-level await is eventually proposed, implemented and supported.
Top-level await support was mentioned as part of the original proposal for async/await SE-0296.
In my case I just refactored the:
async { some-code-which-can-take-some-time }
with:
Task { some-code-which-can-take-some-time }
This error occurs when you’ve tried to call an async function from a synchronous function, which is not allowed in Swift – asynchronous functions must be able to suspend themselves and their callers, and synchronous functions simply don’t know how to do that.
func doAsyncWork() async {
print("Doing async work")
}
func doRegularWork() {
Task {
await doAsyncWork()
}
}
doRegularWork()
Reference here