I've been trying to ascertain the root cause of some test failures in our application at work and I'm wondering if it's related to inappropriate use of subscribe(on:)
. We use this operator to push GraphQL request construction to a background queue (DispatchQueue.global()
) as the construction can be potentially expensive on the main thread.
Something akin to this code:
Just(request)
.subscribe(on: DispatchQueue.global())
...
We haven't seen any issues in our application but have experienced sporadic test crashes/failures. After further investigation, I found that if we don't "properly" store the subscription form a sink, we can experience a EXC_BAD_ACCESS
exception. (The task is stored in local scope, but deallocates after a 0.1-second timeout using XCTExpecations)
Upon further research, I've found that the following snippet of code crashes in a playground:
var x = 0
let count = 100
for i in 0..<count {
Just(i)
.subscribe(on: DispatchQueue.global())
.sink { value in
print("sink \(value)")
x += 1
if x == count {
print("finished \(count)")
}
}
// note: subscription is not stored.
// The above code eventually crashes with `EXC_BAD_ACCESS`
}
PlaygroundPage.current.needsIndefiniteExecution = true
Note: It only crashes intermittently. The crash can be produced more reliably if you bump the count past 1000.
My question is: Is this expected if we do not retain the subscription returned from sink? Does anyone have insight into why we might be seeing crashes with this combine pipeline?
Exception: