Dispatch precondition false positive?

I'm not sure if this belongs in the Apple developer forums, rather than the Swift forums, but I think Dispatch is part of Swift, so it might fit. Please let me know if this is better asked elsewhere.

I have written a small promise type for a project I'm working on, and it allows us to specify which queue to run a completion handler on. This had a bug, so in order to test the semantics of this, I wrote a (supposedly failing) test using:

promise.onCompletion(on: .queue(expectedQueue)) { _ in
    dispatchPrecondition(condition: .onQueue(expectedQueue))
    exp.fulfill()
}

However, it produces a false positive, as far as I can tell. That is, the condition passes, even when the queue is in fact not the correct queue. If I instead change the test to

promise.onCompletion(on: .queue(expectedQueue)) { _ in
    let currentLabel = String(validatingUTF8: __dispatch_queue_get_label(nil))!
    XCTAssertEqual(expectedQueue.label, currentLabel)
    exp.fulfill()
}

… the test fails as expected.

What am I getting wrong? Is this a bug in dispatchPrecondition or am I misunderstanding the semantics of how it is supposed to work?

Do you use target queues? Do you dispatch synchronously or not? I mean, could you not be on your expected queue, after all?

Queues are not necessarily unique.

Check if anything related applies in your setup.

My Promise type has a private (serial) mutationQueue. All read and write access to result and completionHandlers are performed synchronously on this queue. This is because onCompletion(on:execute:) and resolve(with:) can be called in any order on any queue, and I want to eliminate race conditions, if a completion block is added while the promise is being resolved.

In addition my onCompletion has an ExecutionContext type that wraps a closure. It only exposes a single function execute which combines the concept of DispatchQueue.async { ... }, DispatchQueue.sync { ... }, DispatchQueue.asyncAfter(deadline: ...) { ... } and simply { ... }(), so that I can specify how I want my code to run.

The bug in my code was that when I specified .immediate as the execution context, and the promise was resolved, it got executed without dispatch (as it should), but it got executed from within the mutationQueue, and not from queue that either onCompletion or resolve was called from.

I fixed the bug, but I was confused as to why my tests didn't fail. I explicitly had a test like this:

func testOnCompletion_onDefaultContext_whenResolvedOnAQueue_shouldExecuteOnSameQueue() {
    let queue = DispatchQueue(label: "expected")
    let exp = expectation(description: "should resolve on same queue")
    Promise<Void, Error>
        // (1) this returns an unresolved promise, and causes resolve() to 
        // be called on 'queue' after 100 milliseconds
        .delayed(.milliseconds(100), on: queue, with: .success)
        // (2) this adds a completion handler that should be executed
        // as soon as the promise is resolved, either on the queue 
        // that 'onCompletion' is called from (if already resolved), or
        // the queue that 'resolve' is called from (if not yet resolved) (ie. 'queue')
        .onCompletion(on: .immediate) { result in
            dispatchPrecondition(condition: .onQueue(queue))
            exp.fulfill()
        }
    waitForExpectations(timeout: 1.0)
}

The dispatchPrecondition passed. But when I changed the test to compare queue labels instead, it failed with:

XCTAssertEqual failed: ("expected") is not equal to ("promise.mutation_sync")
1 Like