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:

The answer is GCD is most unpredictable part of process. I have similar errors while rewriting values from queues.

It appears that the cancel() function for Just's Subscription type is not thread-safe. (Same for Result.Publisher and Optional.Publisher). I have reported one particular way to crash it in FB7722681. You should also report yours.

1 Like