Yes, it's within a single actor.
After rereading the continuations and structured concurrency proposal, I'm still not sure what the problem might be. As I understand it, this is the sequence of events.
- My actor,
DataRequest, is created bySession, another actor.Sessionthen creates a partial task toresumethe request and then synchronously returns theDataRequest.
@discardableResult
nonisolated
func request(_ urlRequest: URLRequest) -> DataRequest {
let request = DataRequest(.init(urlRequestConvertible: urlRequest, dataTaskProvider: self, requestModifiers: []))
async {
await request.resume()
}
return request
}
- Outside the
DataRequest(in this case in a test), my continuation method is called toawaitthe request'sData.
func data() async -> Data {
await withCheckedContinuation { continuation in
storage.waiters.append({
continuation.resume(returning: self.storage.data)
})
}
}
- Concurrently, the partial task created in the
Session.requestmethod will executeDataRequest.resume(). This method attempts to stay synchronous by creating an unstructured task for the work needed to start a request. (Aside: It's probably an issue that I can create a partial task without handling the errors produced like I would without the partial task.) This work is executed but immediately suspends toawaittheurlRequestvalue.
func resume() {
guard state.canTransition(to: .resumed) else { return }
if state == .initialized {
async {
let urlRequest = try await configuration.urlRequestConvertible.urlRequest
let dataTask = await configuration.dataTaskProvider.dataTask(with: urlRequest, for: self)
storage.tasks.append(dataTask)
dataTask.resume()
state = .resumed
}
} else {
storage.tasks.last?.resume()
state = .resumed
}
}
- The unstructured task started in 3 eventually completes, appending the task to storage, resuming the task, and updating the
DataRequest.stateto.resumed. At this point the task is executing and I'm getting delegate callbacks. I'll skip to the completion event. - My
URLSessionTaskDelegatereceivesdidCompleteWithErrorand I create an unstructured task to call back to theDataRequestfor the completedtask. Since this is anonisolateddelegate method, it executes on the global concurrent executor.
nonisolated func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
async {
await self.session?.dataTaskMap[task]?.didComplete(producingError: error)
}
}
-
DataRequest.didCompleteis called, which is where the ultimate issue occurs. I store any passed error, transition to thefinishingstate, and call all of the enqueued waiters. This case, it's just the one created in the test. Then the state is set tofinished.
func didComplete(producingError error: Error?) {
storage.underlyingError = error
guard state.canTransition(to: .finishing) else { return }
state = .finishing
// Start response handling.
//async {
storage.waiters.forEach { $0() }
//}
guard state.canTransition(to: .finished) else { return }
state = .finished
}
- In the synchronous case, the closure for the continuation is called, resuming it with the stored
Data. The continuation transitions from the suspended to the scheduled state but isn't immediately executed since thedidCompletemethod is still executing. OncedidCompletefinishes, the continuation is executed,data()executes, and the waiter receives itsData. - In the async case, according to the proposal, "[t]he initializer creates a new task that begins executing the provided closure". Now, this seems to be saying the closure should execute immediately. If that's the case, the execution should match the sync case since there are no suspensions within the closure. However, that's not what I observe. Adding
printstatements within theasynccall indicates it's actually executed afterdidCompletehas completed and the state has been set tofinished. This is actually what I'd expect, but doesn't match what I interpret the proposal to mean. In any case, the continuation closure is called and then the hang occurs.
In the course of writing this all up, I actually fixed the issue by adding a print statement at the end of didComplete. Simply adding that statement got both the sync and async versions to work. This is definitely a bug. As far as I can tell, the didComplete method actually finishes executing, as the execution of the unstructured task picks up the .finished state.