insertOrUpdateAssets(_:) is async throws and returns an AsyncSequence, so by the Swift
Concurrency contract I expected it to observe cancellation of the enclosing Task and throw
CancellationError. It does not. The call runs to completion and returns successfully, with
the full result set, as though the cancellation had never happened.
In my measurement the cancel was delivered at 2.7 s and the call returned successfully at 19.0 s, having processed all 150 assets and reported 550 faces.
The practical consequence for an app is that a "Stop" button cannot stop work that is already in
flight. The only way to get responsive cancellation is to slice the work into many small calls and
check Task.checkCancellation() between them — which means the batch size, which should be tuned
for throughput, ends up being dictated by how long a user is willing to wait after pressing Stop.
For me that is the difference between a batch of 150 (≈19 s to react) and a batch of 25 (≈3 s).
The store is left in a consistent state, which is good: the assets processed before cancellation
remain, and state correctly becomes .stale. So this is specifically about the cancellation
signal being ignored, not about data integrity.
One detail worth knowing when reproducing this
insertOrUpdateAssets is declared nonisolated(nonsending), so it executes on the caller's
executor. My first attempt at this reproducer used a plain Task { } created from @MainActor
top-level code — which inherits main-actor isolation — and the detection therefore ran on the
main actor and starved the very code that was supposed to cancel it: a Task.sleep(2.5s) on the
main actor did not resume until the 19-second call had already finished, so the cancel was not
even delivered until 19.0 s.
The attached reproducer uses Task.detached to avoid that confound, and the cancel is correctly
delivered at 2.7 s. I mention it because anyone reproducing this from a @MainActor context will
see a different and misleading timeline.
Feedback: FB24174707