How to serializes access to broken async swift APIs that fail when called concurrently?

Viewed 23

Apple's generateAssertion(_:clientDataHash:completionHandler:) is broken and fail majority of concurrent calls with invalidKey error. This test will fail 99% of times. If instead the calls are made one after the other then all of them succeed.

func testConcurrentGenerateAssertionIsUnsafe() async throws {
    let service = DCAppAttestService.shared
    let attestationKey = try await service.generateKey()
    try await service.attestKey(attestationKey, clientDataHash: Data(SHA256.hash(data: Data())))
    
    let calls = 5
    let concurrentAssertResults = try await withThrowingTaskGroup(of: Data.self) { taskGroup -> [Data] in
        for i in 1...calls {
            taskGroup.addTask {
                do {
                    let result = try await service.generateAssertion(attestationKey, clientDataHash: Data())
                    print("Assertion result \(i) \(result)")
                    return result
                } catch {
                    print("Throwing error \(i) \(error)")
                    throw error
                }
            }
        }
        var assertResult = [Data]()
        for try await fetchKeyResult in taskGroup { assertResult.append(fetchKeyResult) }
        return assertResult
    }
}

Output:

Throwing error 5 Error Domain=com.apple.devicecheck.error Code=3 "(null)"
Assertion result 4 140 bytes
Assertion result 3 142 bytes
Throwing error 1 Error Domain=com.apple.devicecheck.error Code=3 "(null)"
Throwing error 2 Error Domain=com.apple.devicecheck.error Code=3 "(null)"

For APIs like this it would almost be better if the API could be forced to be synchronous. I have a solution in place currently to fix this that throws concurrent calls in an array and drains them one by one after waiting on each result and passing it to corresponding completion handler. But is there a better way to deal with this issue?

0 Answers
Related