Description
Description
I can honestly see this going either way as a bug, working as expected, or even "kinda a bug, but not much we can do".
Anyways, I'm trying to get a reliable timeout behavior by leveraging a task group.
Reproduction
This is a simplistic reproduction of an issue that can be experienced in our live production code. The usleep
is just an issue reproducing standin for another long running task.
@Test func failingExample() async throws {
try await withThrowingTaskGroup { group in
group.addTask {
let startTime = Date()
while .now < startTime.addingTimeInterval(10) {
// I couldn't get usleep to ever return on a single 10 second call, so I'm just doing this.
usleep(1_000)
}
print("🤬🤬🤬")
throw TimeoutError.failure
}
group.addTask {
try await Task.sleep(for: .seconds(1))
print("💨 now!")
throw TimeoutError.timedOut
}
defer { group.cancelAll() }
guard
let success = try await group.next()
else {
throw TimeoutError.noResult
}
}
}
public enum TimeoutError: Swift.Error {
case timedOut
case noResult
case failure
}
Expected behavior
Concise/TL;DR
I expect the usleep
task to continue in the background and any returns/results/etc discarded. Nothing can be done to stop it from completion, just the completion is into the void.
I expect the task group to bubble up the thrown error immediately after print("💨 now!")
and not wait for the rest of the task group to complete.
Long, but will read
We should see 💨 now!
print out on the console after about a second.
If the program is still executing, I'd expect to see 🤬🤬🤬
about 9 seconds afterwards.
While I know there's no mechanism in the long running task to stop what it's doing (it should just silently discard its results in the background, tho that wouldn't affect any side effects it could affect while it's still running), I would expect the task group to exit immediately upon the thrown error.
I DO also understand the complication where if the usleep
task is running on the main thread and the task group is also on the main actor, that would cause a dead lock until usleep
is done, but I don't think that's what's happening here. I'm not constraining any isolation on any scope, and while some might come by default, I think the only thing that SHOULD have any default isolation is the top scope of the test. The usleep scope should be on a background thread/actor/isolation/whatever.
And, the solution can't be "change the usleep operation scope to periodically check for cancellations" because A. this should be a general, generic timeout solution B. I don't think that's possible on cross language code (specifically in our app this is interacting with a Rust library) and C. the long running code is from an SDK we don't have control over, even if we wanted to change that side of things.
Environment
swift-driver version: 1.120.5 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
Target: arm64-apple-macosx15.0
MacBook Pro M4 Pro
Additional information
It DOES work as expected if you periodically run Task.checkCancellation()
in the long running task. And when the long running task is async aware in general, the task group works great!