I'm still concerned that withDeadline doesn't communicate the (reasonable, correct, but unintuitive) behavior that the task is cooperatively cancelled at the deadline. The discussion threads had several alternative names for this API, but withAutomaticCancellation(at deadline:...) seemed to have the most adherents. I see this is addressed in the "alternatives considered", but I still don't consider the issue settled.
DeadlineError doesn't capture the case that the operation is cancelled but subsequently succeeds (or fails in some manner undetectable to withDeadline's convention other than by throwing an error, e.g. returning false). So at least one additional case for Cause is required. I think that DeadlineError should be extended with the Return generic parameter, underlyingError should be removed, and Cause should be extended to cover all the cases:
enum Cause {
case failureBeforeDeadline(OperationError)
case successAfterDeadline(OperationReturn)
case failureAfterDeadline(OperationError)
}
Note that this would imply that Return needs to be constrained to Sendable, in order to appear in the error type. I think that's necessary, but it's not very elegant. If the associated value of my proposed successAfterDeadline were omitted, the user would lose their ability to distinguish whether the operation had completed or not.
The API to return the current deadline as any InstantProtocol isn't actually useful, since there's no way to call either of InstantProtocol's APIs in that situation, without first downcasting to a known concrete type. That means that the deadline is effectively only useful to the code which set the deadline, which could more easily use a TaskLocal to pass itself a concretely-typed deadline. Using any InstantProtocol<Swift.Duration> is clearly better; that would at least allow advanced(by:) to be called, but I think that's still not actually useful. For anyone to do anything general-purpose with this value, they need to be able to cast it to the instant of a well-known clock.
Internally, this has to be implemented by calling sleep(until:tolerance:) on the user-provided clock. I expect that most user-provided clocks have to translate that to a call to ContinuousClock.sleep(until:tolerance:) anyway? But I guess it's theoretically possible that they use some external implementation such as a runloop timer or pthread_cond_timedwait or whateverā¦
An out-of-the-box idea is to provide an access method for the current deadline that does a kind of reopening of the user's clock existential (would have to be magic, but _openExistential exists as precedent); something like
Task.withCurrentDeadline { clock, instant in
// this is actually a generic function, not a regular closure
// it effectively has a signature like
// f<C: Clock>(_ clock: Clock, _ instant: C.Instant)
}
But perhaps the right solution is just to provide the option to get the deadline in multiple ways:
public struct CurrentDeadline {
// always available, but only useful to the calling code
var instant: any InstantProtocol<Swift.Duration> { get }
// non-nil if `withDeadline` was called with `ContinuousClock`
var continuousClockInstant: ContinuousClock.Instant? { get }
// or alternatively, just one accessor API:
func instant<C: Clock>(_ clock: C) -> C.Instant?
}
extension Task where Success == Never, Failure == Never {
// nil if we're not (dynamically) within a `withDeadline` call
public static var currentDeadline: CurrentDeadline? { get }
}
making a CurrentDeadline type might make it easier to evolve the API if we add a hypothetical future protocol like RealtimeClock which provides functionality to convert to & from ContinuousClock; then if the user calls withDeadline with a RealtimeClock instance then the deadline can be available in multiple timebases simultaneously.