I cribbed this rough pattern from WWDC23's Beyond Structured Concurrency talk, but I'm concerned that I'm mis-using it or that it's not safe to do what I'm after.
My goal is to call an async method that returns a result, and intentionally "race" the time it takes that method to return a value against a specific timeout, throwing an error if the timeout is exceeded, or returning the value if it returns before the timeout is triggered.
The code from my project:
let msg = try await withThrowingTaskGroup(of: SyncV1Msg.self) { group in
group.addTask {
// retrieve the next message
try await self.receiveSingleMessage()
}
group.addTask {
// Race against the receive call with a continuous timer
try await Task.sleep(for: explicitTimeout)
throw SyncV1Msg.Errors.Timeout()
}
guard let msg = try await group.next() else {
throw CancellationError()
}
// cancel all ongoing tasks (the websocket receive request, in this case)
group.cancelAll()
return msg
}
This (and some similar patterns that don't involve returning a value) are all throwing the same compiler warning:
warning: passing argument of non-sendable type 'inout ThrowingTaskGroup<SyncV1Msg, any Error>' outside of global actor 'AutomergeRepo'-isolated context may introduce data races:
guard let msg = try await group.next() else {
^
I thought I'd read elsewhere on the forums that this was a "known issue", but I was looking for the relevant reference today, and was unable to search back to find the relevant conversation, if it exists.
Is this a legit pattern with an overly cautious warning, or is this not a pattern that works. In practice, it seems to be operating as I'd expect - but I'm afraid the specifics of task groups and calling group.next()
are eluding me.
Is there a better way to express this pattern?
(@mattie - have you run into this one in your recipes?)