Now the inner deadline is returned to the code which expects to read the outer deadline. Just using an ad-hoc ThreadLocal fixes this problem too.
And for code that expects to generically read a user-imposed deadline, maybe the new HTTPClient proposal, the situation is worse — the best it can do is
What would that actually mean for the API shape though, Task.currentDeadlineExpiration is rather weird IMHO, Task.currentDeadline reads pretty logically on the other hand.
@Philippe_Hausler I actually second the question here since I just noticed the current proposal uses any InstantProtocol?, while I thought we were going to have concrete types here, but ofc we can't anymore with the generic clocks... Do we need func deadline(in: SomeClock.Type) -> SomeClock.Instant?? That'd be unfortunate complexity but at least would save developers from writing the cast dance themselves...?
Deadlines can reach an expiration, or expire, but also can be exceeded; all are fairly reasonable terms of use. I agree that the name Task.currentDeadline is pretty reasonable and Task.currentDeadlineExpiration feels redundant.
The current deadline cannot be generic and a property - there is no way to express that in the langauge today (or at least reasonably). The problem with the deadline(in: SomeClock.Type) -> SomeClock.Instant? is that would then just push the guessing to the clock type so the developers would have no real "cast dance" savings.
Not that this is a way out of this pickle; but if the currentDeadline property was instead a structure that had both an instant AND a clock then I would likely name that instant property the expiration; e.g. Task.currentDeadline.expiration and Task.currentDeadline.clock. Which would mean that a deadline would be the composition of the clock, an instant, and likely also the tolerance - which that particular combination is the one I am exploring concurrently to the pitch per some of the other concerns raised around the conversion routines.
To note: that exploration is not really going well - it very much complicates the APIs for very little wins (if any).
My concern with the name withDeadline is that it indicates a hard stop at some point in time, by which the caller expects the function to return.
This creates friction because, in structured concurrency, the parent task cannot exit its scope until all child tasks have completed.
Therefore, if the function takes longer, the deadline is irrelevant; it's only used to execute cancellation, not to define the allowed execution time for the function.
The name is misleading - it sets an expectation that the implementation cannot fulfill, and it's not that obvious to catch.
The proposal addresses alternative namings, such as withAutomaticTaskCancellation, by saying:
This naming does not focus upon the time related qualities of the concept of deadlines, which is the primary behavioral aspect of this API. The cancellation behavior is part of the realities of how the language level concept of cooperative cancellation works and in reading the code at a potential call site it is more meaningful to convey the temporal nature of a deadline than to convey the cancellation being automatic."
I agree we don't need to convey cancellation as being automatic, but I'd argue we do need to convey that the deadline is related to cancellation.
After all, the function merely schedules a cancellation signal, rather than enforcing a hard time window for execution.
As I stated before, reading the code wrapped by the withDeadline primarily conveys the message that it's the maximum time after this function returns, and it's simply not true.
What really happens is that we always await the task until it completes, and either return its value or wrap the failure with the DeadlineError.
I'd argue that a better name reflecting this behavior is: withCancellation(atDeadline:) proposed by @Dmitriy_Ignatyev. Where the deadline is the clock's instant.
I'd add withCancellation(after duration:) for the convenient wrappers with the clock's duration param.
It's simple and indicates a few important things:
cancellation happening at the deadline (not a deadline/time-span for execution)
the body runs until normal completion or early exits via cooperative cancellation
increases awareness that the body should handle cancellation, otherwise the time limit doesn't make any sense
It's not just a naming preference - it actively improves the discoverability of a non-obvious requirement.
Another naming alternative is withSoftDeadline - it doesn't relate to cancellation, but at least it indicates the deadline is not a strict constraint.
Addressing @ktoso's requirement that the Task’s property name be considered alongside the function name:
The currentDeadline property has the same issues as the withDeadline function - it’s not a fixed time window for the task's execution.
What I found as a better approach is to go with something similar to what @KeithBauerANZ proposed with a wrapping struct: public static var currentDeadline: CurrentDeadline? { get }
Though in this case, it’s all about scheduling a cancellation, so I would go with something like currentScheduledCancellation and the ScheduledCancellation struct.
Then we can have deadline or instant properties that read well on it.
After all, the deadline is related only to cancellation. It is cancellation that happens to a task (not a deadline), and it's well aligned with other properties and functions related to verifying/triggering cancellation on a task, e.g. Task.isCancelled or Task.checkCancellation().
If you still want to go with a simple any InstantProtocol? property, an alternative is to name it currentCancellationScheduledAt, though the struct approach feels much cleaner, and it’s not that verbose.
I want to address a question that was brought to me in offline discussions around this proposal. A previous version contained the implementation of the proposed withDeadline method, which showed that it is based around two child tasks that are racing with each other. While this is the current implementation and the simplest way to get the semantics to work, this might not be the long-term implementation. In particular, spawning two child tasks can be quite expensive, and underlying executors might be able to handle this better directly. Similar to the APIs proposed in SE-0505: Delayed Enqueuing for Executors, we can expand the APIs of jobs and executors to avoid spawning any child tasks. Importantly, this can be done in a future evolution.
The first one is that Scheduled should be after Task, so it can modify Cancellation - a task's scheduled cancellation, which is exactly right.
Otherwise, Scheduled modifies Task - a scheduled task's cancellation, which isn't the intended meaning.
Similarly to withTaskCancellationHandler and withUnsafeCurrentTask.
The second issue is about including Scheduled in the function name at all.
At first glance, it seems innocent. It aligns with the currentScheduledCancellation property name as well as the struct name.
However, it implies the cancellation is already scheduled rather than being established by this call, and makes the name a bit more verbose.
On the other hand, the at argument in withTaskCancellation(at:) already implies a future point in time, and conveys that the cancellation happens at the specified time, which is equivalent to saying “it’s scheduled”.
If we want more clarity, we could use withTaskCancellation(scheduledAt:). Moving scheduled into the argument label reads naturally: "with task cancellation, scheduled at this instant", and doesn't require awkward modification of either "Task" or "Cancellation" in the function name itself.
So overall, either withTaskCancellation(at:) or withTaskCancellation(scheduledAt:).
Is this a plausible misreading? Do we use "scheduled task" as a term of art anywhere?
There are many competing demands when it comes to naming. Ruling out every possible grammatical ambiguity can lead to ludicrous results by other criteria. The priority is to rule out plausible and actively misleading readings. Remember: as humans, we deliberately use rhetorical devices such as hypallage, moving adjectives to modify words other than their semantic targets ("smart money," "proud day," "restless night")—we are good at understanding.
How would it imply that when, as you say, at clearly indicates it's taking the deadline by which it's scheduled?
But, sure, I agree that withTaskCancellation(scheduledAt:) is fine; the feedback was clarifying whether this explanation—that the function is scheduling a cancellation signal—is indeed an accurate summary that folks can agree on.
The point of insisting on the word "scheduled" is to be responsive to the authors' concern that a name like with(Automatic)TaskCancellation(at:), which they reject in the proposal, "does not focus upon the time related qualities of the concept of deadlines." They insist that the naming must cause the user to "realiz[e] without ambiguity that a concept of time is involved."
I would prefer to not include the words "schedule" anywhere here, as it inherently hints at this causing scheduling events, while we may not necessarily cause a scheduling even when entering this with... block. I'd rather state the "thing" rather then the "act of ..." in these APIs.
For example, potential optimizations we can co here include:
not schedule anything if deadline is already in the past or outer is already shorter,
don't schedule until we know there's some listener to actually observe it (i.e. isCancelled can checks against deadline, however without scheduling an "cancel event" until we install a task cancellation handler, therefore we must schedule the timer/cancel now).
And things like that... We're not really committing to those in this proposal but it's a window I'd like to leave open and I don't feel very that adding "scheduled" to the API name helps the actual expressiveness but just tries to explain the specific implementation technique.
If we have to steer towards cancellation "at" then some form of withTaskCancellation(in: .seconds(20)) and withTaskCancellation(at /*deadline*/someInstant) would be workable IMHO.
Yeah, I don't think we should imply this is scheduling anything, it might not. And granted one can write that off as an optimization, but imho the core idea is that of a point in time at which a the task is cancelled -- and less so about how that is achieved (scheduling anything).
This is a good addition that works with the already established semantics of swift concurrency.
Like others, I don’t love the name, but my objection is from a different direction. I have no desire to state the semantics of concurrency in the function name; at that point we might as well state them every time we use the keyword await. Clarifying the semantics doesn’t belong at every call site, but in the function’s documentation – as long as the semantics aren’t particularly different from the usual (they aren’t.)
Rather, I would point out that usually in the standard library we pass an instance of Something as a parameter to the closure of a function called withSomething. In the case of the deadline, it is not found as a parameter, but as a condition of the runtime environment of the closure. I’d rather have a name that makes clear the relationship between the deadline and the return value, such as beforeDeadline(_:). This would read pretty well to me: try await beforeDeadline(t) { doSomething() }
However I want to voice that we might want to make the DeadlineError more generic by also adding the used Clock as a generic parameter.
Looking at this usage example:
let clock = ContinuousClock()
do {
try await withDeadline(clock.now.advanced(by: .seconds(5)), clock: clock) {
// ... do something
}
} catch {
switch error.cause {
case .deadlineExceeded(let deadlineError):
// 🚨 why do I need to do this cast here?
let deadline = deadlineError.expiration as? ContiniousClock.Instant
case .operationFailed(let operationError):
// ...
}
}
it would be much nicer, if we don't need to cast deadlineError.expiration. To allow this we will need to add the clock to the signature here:
public struct DeadlineError<OperationError: Error, C: Clock> {
public var expiration: C.Instant
}
The word “deadline” makes me think the task could change its behavior and plan things in order try to meet the deadline. For instance, a task with a close deadline could split itself into more subtasks to increase parallelism (at the cost of less efficiency) in order to meat the goal. Or maybe it could bail out early when it knows it’s already too late to meet an impossible deadline.
If the sole meaning is that the task should be cancelled after a specific date/time, then calling it “task cancellation at [instant]” seems better, as it does not evoke planning or doing clever things to meet the deadline. But if a task is expected (at least in some cases) to plan how it does things according to the deadline, then I think deadline is the correct word.
I see, you’re saying that "schedule" in the API name would imply that something is always set up, ruling out that it may actually be more lazy. Since the task needs to finish, checking against the deadline on isCancelled calls would be enough, and only adding the handler would require scheduling the cancellation, because a handler needs to be triggered at a specific moment rather than just checked lazily?
Did I understand it correctly? Really interesting!
I see now how this could be a bit misleading. Just for clarity, I didn’t make any assumptions about implementation. I simply used the term "schedule" because I find it to be a general term for saying things are time-based, and something is going to happen at a specified point in time.
But I see it may not always be true, and it would be better not to impose anything.
Either way, withTaskCancellation(at:) sounds great to me! Hope this stays
What about the Task property, though? “scheduled” is ruled out, and currentDeadline has issues I mentioned.
What about currentCancellationDeadline? It scopes the deadline to cancellation rather than the task’s execution time, and it doesn't require a struct if this is preferred.
-1 on the current proposal because of two one issues
### Problem 1: No ability to query the current deadline
When you call a remote service, you would typically attach the deadline to the request. The obvious deadline to attach to a request is the current deadline -- but in order to do so, you'd have to be able to query it. Unless I'm missing something, the proposal is missing it
(Just found Task.currentDeadline in the proposal which addresses this .)
Problem 2: Repeats the huge tolerance = nil mistake from Task.sleep
When one builds robust systems, it's usually important to spread out firing deadlines. Typically, one calculates a deadline and optionally adds a bit of jitter.
The tolerance = nil is bad in two important ways:
It reads like "no tolerance" (but that's not the case)
The "tolerance" gets interpreted as a way of doing timer coalescing(!!)
The default of doing timer coalescing has caused so much trouble that we're avoiding Task.sleep now. Instead, we have an API called Task.sleepProperly(for duration: .seconds(30), maxJitter: .seconds(1)) which essentially calls Task.sleep(.seconds(30) + randomJitter(maxJitter), tolerance: .zero). The crucial bit here being tolerance: .zero.
Unless you pass tolerance: .zero, a lot of your deadlines will be coalesced together, which often has the effect of thousands of them firing at the same instant which then has really explosive potential: If you're lucky, you're just get a momentary stutter of bad latencies. If you're less lucky, you accumulate so much work triggered by expiring deadlines that you might even OOM.
In some cases this can even be exploited by an attacker. If you have a system that has a timeout of (say) 60 seconds, with the default timer coalescing, the attacker has ample time to trigger lots and lots of timers to fire at the exact same instant triggering work. Even if you do apply some jitter, the timer coalescing often undoes this protection.
Let's please not repeat this mistake and default to switching timer coalescing off. It sounds like a good idea but it really isn't.
In reality you will be adding jitter to deadlines anyway, but your request would be specifically to default the tolerance to 0 right? Coalescing is an idea to save on timer events, but I totally see your example of it achieving the inverse effect by firing at the same time and needing to be serviced then.
I wonder though if just adding jitter with the existing API is sufficient or not, in your experience with sleep?
This very much is one possible use of this API I've been thinking about, and a reason to not focus this API spelling about cancellation entirely.
It not only matters for cancelling work but also for "the deadline is so far ahead, that there's no need to spin up resource intensive things to complete this quickly". This is very much an use case of deadlines as well -- to not rush executing things if we know we have a lot of time remaining. You could totally inform scheduling and task priority information in tasks you're creating using this information.
If I get it right, you want to view "deadline" as "cooperatively plan your work". However, "deadline" on its own risks misleading callers into expecting guaranteed execution time, rather than cooperative planning, and I think names should minimize doc-reading.
From the caller's perspective, the end result is still the same; the deadline expiration causes cancellation - I think we agree on this one.
Which misuse is worse?
devs expecting a guaranteed return
or
devs optimizing task's work for the deadline but ignoring cancellation
I lean towards the first one as it sets wrong expectations. withTaskCancellation(at:) paired with currentCancellationDeadline clearly signal cancellation happens at the specified instant, and I believe the name should reflect the observable behavior.