MuniekMg
(Tomasz)
1
Hello,
Third party API I'm using sometimes calls completion callback multiple times causing fatal error in withCheckedThrowingContinuation
Fatal error: SWIFT TASK CONTINUATION MISUSE: clear() tried to resume its continuation more than once, returning ()!
Are there any options for preventing this crash besides fixing third party library?
I tried setting continuation to nil but there is still some space for resuming twice or more:
func waitForDataStoreReady() async throws {
return try await withCheckedThrowingContinuation { continuation in
var nillableContinuation: CheckedContinuation<Void, Error>? = continuation
doSomething { result in
switch result {
case .success:
nillableContinuation?.resume()
nillableContinuation = nil
case .failure(let error):
nillableContinuation?.resume(throwing: error)
nillableContinuation = nil
}
}
}
}
Thanks :)
1 Like
You should probably nil out the variable before resuming the continuing, to reduce the window in which a second invocation of doSomething can happen:
switch result {
case .success:
if let continuation = nillableContinuation {
nillableContinuation = nil
continuation.resume()
}
case .failure(let error):
if let continuation = nillableContinuation {
nillableContinuation = nil
continuation.resume(throwing: error)
}
}
However, if doSomething can be invoked simultaneously from multiple threads, that still won't avoid race conditions. You'd need to add some additional synchronization around the check of the continuation.
1 Like
MuniekMg
(Tomasz)
3
I end up using simple Lock:
func waitForDataStoreReady() async throws {
let lock = NSLock()
return try await withCheckedThrowingContinuation { continuation in
var nillableContinuation: CheckedContinuation<Void, Error>? = continuation
doSomething { result in
lock.lock()
defer { lock.unlock() }
switch result {
case .success:
nillableContinuation?.resume()
nillableContinuation = nil
case .failure(let error):
nillableContinuation?.resume(throwing: error)
nillableContinuation = nil
}
}
}
}
Sorry to hijack the thread, but I don’t understand what’s wrong with the original code. Is the continuation called multiple times because of re-entrancy? Where? I can see in general how resuming a continuation while the reference is around can result in subsequent invocations but here the reference is inside a function and resumption happens inside a closure so for the life of me I cannot explain how this code fails.
MuniekMg
(Tomasz)
5
let's imagine function doSomething looks like this:
func doSomething(callback: (Result<Void, Error>) -> Void) {
callback(.success(()))
callback(.success(()))
}
in this case everything is ok, first callback will resume continuation and set it to nil, so second one will do nothing. But function doSomething may call this callback concurrently, like so:
func doSomething(callback: @escaping (Result<Void, Error>) -> Void) {
for _ in 0..<10 {
DispatchQueue.global().async {
callback(.success(()))
}
}
}
in this case it may happen that two or more execution of callback will resume continuation before any of them set it to nil
Ah, that makes sense. I did not think much about doSomething, so I though the issue was in the surrounding code, and missed the point. Thanks for clarifying!