Conversation
84783c1 to
c119d0d
Compare
| } | ||
| group.addTask { | ||
| do { | ||
| try await clock.sleep(for: timeout, tolerance: tolerance) |
There was a problem hiding this comment.
I think we've talked about this in the other PR already, but I don't remember the outcome: would it make sense to also add the withTimeout overload that takes an Instant rather than the Duration? This timeout comes with the "impreciseness" of adding the little "spinning off child task" delay on top of the timout Duration.
There was a problem hiding this comment.
Yeah this was raised in the forums thread again. Having a withDeadline method.
| nonisolated(nonsending) public func withTimeout<Return, Failure: Error, Clock: _Concurrency.Clock>( | ||
| in timeout: Clock.Instant.Duration, | ||
| tolerance: Clock.Instant.Duration? = nil, | ||
| clock: Clock = .continuous, |
There was a problem hiding this comment.
I think the Duration variant of withTimeout though could take any Clock<Duration> rather than some.
Imagine this model:
class Model {
func doSomething() async throws {
try await withTimeout(in: .seconds(5)) { ... }
}
}
If you were to unit test doSomething, you would normally try to inject a custom clock, to not make the unit test wait for 5 seconds.
Either with some sort of dependency injection, or with simply passing it along the initializer you could do this:
class Model {
let clock: any Clock<Duration>
func doSomething() async throws {
try await withTimeout(in: .seconds(5), clock: clock) { ... }
}
}
At call site:
let model1 = Model(clock: ContinuousClock())
let model2 = Model(clock: TestClock())
There was a problem hiding this comment.
I think you can do this already with the caveat that Duration cannot be used on an arbitrary clock so you have to constrain it. This works with the existing code:
@Test(arguments: [ContinuousClock()])
func dependencyInjection(clock: any Clock<Duration>) async throws {
try await self.concreteClock(clock: clock)
}
private func concreteClock(clock: some Clock<Duration>) async throws {
try await withTimeout(
in: .seconds(10),
clock: clock
) {
try await Task.sleep(for: .milliseconds(10))
}
}## Motivation Asynchronous operations in Swift can run indefinitely, which creates several problems in real-world applications. Currently, developers must implement timeout logic manually using task groups and clock sleep operations, resulting in verbose, error-prone code that's difficult to compose with surrounding async contexts. Each implementation must carefully handle cancellation, error propagation, and race conditions between the operation and timer. ## Modifications This PR introduces a `withTimeout` method. ## Result An ecosystem wide timeout solution for asynchronous operations.
c119d0d to
88f98d5
Compare
| struct Disconnected<Value>: Sendable { | ||
| // This is safe since we take the value as sending and take consumes it | ||
| // and returns it as sending. | ||
| private nonisolated(unsafe) var value: Value? | ||
|
|
||
| init(value: consuming sending Value) { | ||
| self.value = .some(value) | ||
| } | ||
|
|
||
| consuming func take() -> sending Value { | ||
| self.value.takeSending()! | ||
| } | ||
| } |
There was a problem hiding this comment.
are we sure this is safe?
since both Disconnected and Value are copyable types, I don't see what would prevent copies of this ending up in different isolations.
There was a problem hiding this comment.
That's right. This type would ideally need to be ~Copyable for this to be entirely safe. Sadly withTaskGroup doesn't support ~Copyable types right now. A way around this would be to box the Disconnected but that incurs an allocation. I wanna pitch the non-copyable Disconnected type in a follow up pitch to this package.
There was a problem hiding this comment.
maybe you could add a few "do not try this at home" comments to make sure this does not spread into use cases where bad thing will happen.
…ration failed on its own or due to a timeout
| tolerance: Clock.Instant.Duration? = nil, | ||
| clock: Clock, | ||
| body: nonisolated(nonsending) () async throws(Failure) -> Return | ||
| ) async throws(TimeoutError<Failure>) -> Return { |
There was a problem hiding this comment.
On Swift Swift version 6.2.3 (swift-6.2.3-RELEASE), I get a compilation error:
Cannot convert '@Sendable () async throws(Failure) -> Return' to 'nonisolated(nonsending) @Sendable () async throws(Failure) -> Return' because crossing of an isolation boundary requires parameter and result types to conform to 'Sendable' protocol
Do I miss something?
withTimeout methodwithDeadline method
| case .deadlineExceeded(let error): | ||
| return "\(error)" | ||
| case .operationFailed(let error): | ||
| return "\(error)" |
There was a problem hiding this comment.
Should the description indicate which case was the cause?
## Motivation
Asynchronous operations in Swift can run indefinitely, which creates several problems in real-world applications. Currently, developers must implement timeout logic manually using task groups and clock sleep operations, resulting in verbose, error-prone code that's difficult to compose with surrounding async contexts. Each implementation must carefully handle cancellation, error propagation, and race conditions between the operation and timer.
Modifications
This PR introduces a
withTimeoutmethod.Result
An ecosystem wide timeout solution for asynchronous operations.