I'm attempting to wrap some closure API which provides a completion handler to return a value asynchronously into an API which provides an async
context to provide the value instead. Unfortunately, unlike the version which uses a DispatchQueue
to control the context on which the provided closure is called, I can't seem to find a way to allow the user to provide an isolation domain for their async closure. In essence, I'd like to provide a similar capability to Task
but two layers removed. However, none of the combinations of isolation features seem to provide what I want. The closest I've gotten is this:
@discardableResult
public func onHTTPResponse<Isolation>(
isolatedTo isolation: Isolation,
perform handler: @escaping @Sendable (_ response: HTTPURLResponse) async -> ResponseDisposition
) -> Self where Isolation: Actor {
@Sendable func performHandler(
isolatedTo isolation: isolated Isolation,
response: HTTPURLResponse,
@_inheritActorContext handler: @escaping @Sendable (HTTPURLResponse) async -> ResponseDisposition
) async -> ResponseDisposition {
print("performHandler", Thread.isMainThread)
return await handler(response)
}
onHTTPResponse(on: underlyingQueue) { response, completionHandler in
Task {
print("inResponse", Thread.isMainThread)
let disposition = await performHandler(isolatedTo: isolation, response: response, handler: handler)
completionHandler(disposition)
}
}
return self
}
When called as
.onHTTPResponse(isolatedTo: MainActor.shared) { response in
print("inClosure", Thread.isMainThread)
return .allow
}
in a test, it prints
inResponse false
performHandler true
inClosure false
As I understand it, the isolation contexts work something like this:
DataRequest
, which definesonHTTPResponse
, is not actor isolated, it provides its own thread-safety, so...- When the closure is called, it occurs on the
underlyingQueue
, so... - The
Task
created has no actor isolation, so it executes its closure on the default executor, so... performHandler
, which takes anisolated
actor parameter, executes its sync context in that isolation, so...- I would expect the use of
@_inheritActorContext
on thehandler
parameter to execute that closure in the provided actor's isolation, however... - it does not, executing instead on the default executor.
I can't tell whether this is a bug in that @_inheritActorContext
isn't properly capturing the isolated
actor, @_inheritActorContext
isn't supposed to capture isolated
contexts in the first place, adding @_inheritActorContext
to an existing closure without it doesn't work, or there's a flaw in my reasoning.
(The underlying Alamofire work is tracked in this PR.)