AFAIK the minimum supported compiler from swift-async-algorithms is 5.7:
But typed throws I believe requires a 6.0 compiler… how would the withTimeout function be able to compile from 5.7?
AFAIK the minimum supported compiler from swift-async-algorithms is 5.7:
But typed throws I believe requires a 6.0 compiler… how would the withTimeout function be able to compile from 5.7?
I would imagine that this would actually be gated with a newer availability than 5.7; it would likely be available from a given tag of swift-async-algorithms instead
+1. Standard library is the most logical place for this fundamental feature.
Waiting for the original task to complete seems to considerably limit the usefulness of this function. Since any async work can block on a task that doesn’t pay attention to cancellation, is there actually a practical advantage of withTimeout compared to just checking the current time after awaiting the original task?
I think a much more useful version of this would be to cancel and detach the child task when the timeout expires. The child task can then clean itself up without impeding the task that asked for the timeout in the first place. TimeoutError could still offer a Future<UnderlyingError> in case the caller wants to report any errors that happen post timeout.
Just because it is marked as async does not mean it is concurrent. The other runtime functions are built upon the idea of avoiding fully unstructured tasks - this API should do the same; it is up to the callee to determine if an early return is possible/warrented. It might be worthwhile to instead of throwing have a version that calls a callback; ala withTaskCancellationHandler so perhaps it would be withDeadlineHandler or something of the sorts.
Future in Swift concurrency is spelled Task. In general that concept is not ideal and causes some level of possibility of performance hits. I don't think we should be offering that many APIs that create them unless they are either funneled into a singular task and multiplexed via something like withTaskGroup or they are somehow intended to be a root level, top of the app structure, type thing. I don't see temporal termination as part of that root.
A useful withTimeout API requires some form of concurrency so that the timer can fire while the task is being awaited. While devolution into executing the entire original task is a reasonable failure mode in a non-concurrent or otherwise degraded system, I would expect this API to be designed for the case where the timeout can fire concurrently with the original task.
As pitched, the behavior of withTimeout is useless if the task might block for long periods of time. The prohibition on blocking only applies to the default cooperative threadpool; a custom executor could be designed specifically for blocking operations, such as an epoll-driven server or a façade around a web service. A “timeout” API that doesn’t actually time out when my HTTP request stalls isn’t a very useful timeout API.
Even in the case of pure compute tasks, the pitched API depends on the entire task tree checking for cancellation. Furthermore, the task tree must check frequently enough to respond to the timeout cancellation within a tolerable window, but not so frequently as to wipe out the benefits of parallelism. This is extremely difficult to fine-tune, especially since it will vary based on the hardware.
I have always wanted something like this function, but I second @ksluder. The implication of "once the operation completed" would diminish the usefulness of this function in real life scenarios.
In my head I actually translate withTimeout to "please mark my task as canceled after this amount of time". So not only is it not useless, it's exactly what I'd expect in structured concurrency. The comment sounds to me to be related to the concept of cooperative cancelation rather than to specifically triggering cancelation after a timeout. Wouldn't this argument apply to manual cancelation too?
That interpretation makes much less of an implication that the caller can use this to reliably reassert execution within a short window around the timeout.
Would folks consider a name like withAutomaticCancellation(after:) for the pitched semantics?
I’m so happy to see something along these lines proposed! I also kinda agree that the standard library feels like a nicer spot for this to land, even though it has downsides.
Now this is an interesting idea. Because while I imagine this name will be very unappealing to experts, I think it will save a lot of non-experts confusion and frustration.
Thanks for all the great feedback so far. I updated the pitch with the following changes:
withDeadline and the error to DeadlineErrorClock and the deadline InstantCurious to hear what everyone thinks about the above changes.
Waiting for the original task to complete seems to considerably limit the usefulness of this function. Since any async work can block on a task that doesn’t pay attention to cancellation, is there actually a practical advantage of
withTimeoutcompared to just checking the current time after awaiting the original task?
I am strongly against detaching the task that runs the operation and returning from the method early. This is going to break all the guarantees of structured concurrency where you can reason about the state of your program by looking at the linear flow of your code. If the task were to be detached, then anything running with a deadline becomes unpredictable. While I understand that in bad scenarios the operation might run significantly longer than the deadline, this is strictly a bug in the code inside the operation. If a developer wants to detach the task, they can do that manually inside their operation closure by setting up a detached task and handling cancellation and awaiting the result manually.
Not checking for cancellation is not a bug. In fact, checking for cancellation too frequently could be a bug. And there are some operations which are simply not interruptible from the CPU side, such as blocking on GPU work.
Disagree, while it isn’t a bug in Swift’s concurrency model, it would certainly be an implementation bug. Also, executing long-running synchronous work in Swift’s default execution context is, in most cases, not a good idea.
I categorically disagree with this, and I would actually implore the designers of Swift Concurrency to back me up on this: proper adoption of Swift Concurrency never requires the client to check for cancellation. In other words, omission of a cancellation check may be a missed opportunity for a performance gain, but it is not a bug.
Swift concurrency is not limited to the default cooperative thread pool. There is nothing wrong with blocking for long periods on an executor that is specifically designed for such. I would expect an operation called withTimeout to preemptively return execution to my awaiting caller while disposing of the blocked task. This is how all existing timeout APIs have worked for blocking I/O and GPU operations since preemptive multitasking became widespread on consumer machines in the mid-1990s.
Swift concurrency is not limited to the default cooperative thread pool.
I never said otherwise, but that’s exactly what I was talking about with “Swift’s default execution context” (default actors, GCE).
I would expect an operation called
withTimeoutto preemptively return execution to my awaiting caller while disposing of the blocked task.
Swift concurrency is not preemptive but cooperative, and I strongly believe we should stay true to this.
For the record, I would like to specifically acknowledge this. It’s a significant but extremely useful tradeoff. Timeouts are very often used as a safety mechanism to catch accidentally deadlocked or stalled operations. If withTimeout results in a new concurrent task that’s stuck waiting for the first one to complete, then it’s actually counterproductive, and an attractive nuisance that will be deployed in the exact scenario it will make worse.
The name withAutomaticCancellation eschews the implied utility that comes with the word “timeout.”
Happy to discuss and push this topic forward a bit, but sadly I’ve gotten super sick in the middle of travel so will need a few days to get better and formulate thoughts more clearly here.
Short version though for now: We could experiment with something in that package. Whatever we come up there does not dictate how such API will look like inside the Concurrency library. The concurrency library must be the one offering this – this is a fundamental piece – and if we’re introducing a temporary thing somewhere, I want everyone to understand that this is very likely to be very quickly deprecated and replaced by a Concurrency library impl.
Specific implementation requirements I had from last time I was investigating this:
I need a few days to get better and will give this proposal a proper review then. Other than the “this might just get deprecated immediately” I don’t mind experimenting in packages for impls, even if the Swift runtime can do these things better. We may gain some experience prototyping before we commit the API to the stdlib this way.
This has always been my interpretation of the swift-async-algorithms package: a place that serves as an extension of Swift’s concurrency implementation, where we might move some APIs to the concurrency library in the future.
P.S. Wishing you a speedy recovery.
Super happy with this proposal, huge +1 from me, but I wish macOS target was not as high, are typed throws the main reason for macOS 15.0+?
They are the primary reason; yes.