I am trying to understand how (or if) cooperative task cancellation behavior can interact with a closure being executed on the queue associated with an instance of NSManagedObjectContext
. I crafted a relatively simple test case that captures what I thought would work. On both iOS 14 and 15, however, Task.isCancelled
always returns false
in the closure supplied to either perform(_:)
or perform(schedule:_:)
.
Have I misunderstood something here, or is checking task cancellation state via Task.isCancelled
not available in this circumstance? I have crafted a workaround for this scenario using withTaskCancellationHandler(operation:onCancel:)
that basically mimics Task.isCancelled
and Task.checkCancellation()
, but it's not as nice.
import XCTest
@testable import TaskCancellation
import CoreData
class TaskCancellationTests: XCTestCase {
func testCancellation() {
let expectation = expectation(description: "...")
let task = Task {
do {
let context = PersistenceController.shared.container.newBackgroundContext()
try await doWork(context: context)
}
catch is CancellationError {
}
catch {
XCTFail("Unexpected error: \(error)")
}
expectation.fulfill()
}
task.cancel()
waitForExpectations(timeout: 2)
}
@discardableResult
private func doWork(context: NSManagedObjectContext) async throws -> Bool {
// Artificial delay to ensure that the parent task is cancelled before trying to use the NSManagedObjectContext
Thread.sleep(forTimeInterval: 0.5)
XCTAssertTrue(Task.isCancelled)
await anotherMethod()
return try await context.performAsync {
if Task.isCancelled {
return true
}
XCTFail("Should not get here")
throw NSError(domain: "Test", code: 0, userInfo: [NSLocalizedDescriptionKey: "Task.isCancelled returned false"])
}
}
private func anotherMethod() async {
XCTAssertTrue(Task.isCancelled)
}
}
extension NSManagedObjectContext {
func performAsync<T>(_ block: @escaping () throws -> T) async throws -> T {
if #available(iOS 15.0, *) {
return try await perform(block)
}
else {
return try await withCheckedThrowingContinuation { continuation in
perform {
do {
let result = try block()
continuation.resume(returning: result)
}
catch {
continuation.resume(throwing: error)
}
}
}
}
}
}