Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via either email or forums DM. When messaging me directly, please put "[SE-0504]" at the start of the subject line.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available on the evolution website.
Iâve read the proposal but havenât had a lot of time to think about it, yet.
Iâve previously just spawned unstructured tasks for cleanup. Itâs not that big a hassle. Generally, the biggest hassle is realising that you need some kind of cleanup protection in the first place â a problem that isnât addressed by this proposal.
I suppose this proposal is much better if youâre cleaning up a bunch a non-sendable data or you otherwise canât transition to a new Task for some reason. But having a Task that is cancelled but behaves like it is not cancelled is weird.
My mind is filled with questions like: what happens if I try to cancel the Task from inside the shield? If I create a new child task will it be able to self-cancel?
I am strongly in favor of solving this problem and the solution proposed here. I have written a lot of asynchronous code that manages resources which heavily relies on clean-up that must run even in cancelled tasks. While the existing workaround with using an unstructured task is applicable in some situations there are many where it can't be used.
public func withTaskCancellationShield<Value, Failure>(
_ operation: () throws(Failure) -> Value,
file: String = #fileID, line: Int = #line
) throws(Failure) -> Value
public nonisolated(nonsending) func withTaskCancellationShield<Value, Failure>(
_ operation: nonisolated(nonsending) () async throws(Failure) -> Value,
file: String = #fileID, line: Int = #line
) async throws(Failure) -> T
Weâre basically saying this isnât viable in situations where you just canât spare the scheduling delay and indeterminism of such cleanups with extra tasks. If youâre getting away with it thatâs fine, but weâve observed production use-cases which suffer because of this indeterminism.
Thereâs various aspects you could be asking about here, this is documented in the âdebugging and observing task cancellation shieldsâ section, please give it another read and happy to answer more specific questions if youâll still have any.
The proposal spells all this out as well, shields donât affect child tasks directly. Just the propagation from parent cancellation. If you have concrete examples youâd like to discuss please spell them out, itâs hard to answer vague questions on this topic.
Great feature!
May I suggest an alternative name, though? If I'm honest, withTaskCancellationShield had me guessing about the meaning until I read the proposal's introduction.
My suggestion is withTaskCancellationIgnored.
It echoes existing APIs like ObservationIgnored and AnimatableIgnored, granted these are library Macro naming conventions, but I think it works here too. The word "ignore" is also used in the very first line of the proposal intro, so it seems like the authors are very much in the same headspace.
We explicitly rejected the word âignoreâ (alternatives consideed). It is problematic because it implies the cancellation had no effect but thatâs not the case, youâre just not observing it while executing inside a shielded block of code. âignoringâ would imply the task was not getting cancelled if cancellation happened while in such a block, so thatâs a risky misunderstanding we want to avoid, and therefore are proposing coining a new term, because the exact behavior is not exactly captured by any other verb we could think of.
In other words, I think that leading people to intuitively mis-understand a feature, rather than not having to look up its semantics when they encounter it would be worse.
Thatâs two examples, yes. Thatâs why just âignoring cancellationâ isnât quite right, we didnât ignore the cancel() happening after all.
Thereâs many more cases, just any âsomeone cancels the task, while the task is executing with a cancellation shield installedâ manifests in the same behavior.
I continue to think that this is an important enough pointâthe distinction between "the effect of being canceled" and "being canceled"âthat consistent feedback wondering why it's not called "ignored" sends a signal that using "shield" as a term of art is not a good enough solution.
I think in part it is because the analogy is faulty: as we discussed during the pitch phase, if you shoot an arrow at a shield, the arrow doesn't hover in midair waiting to hit the target once the shield is dropped. By the "shield" analogy, an arrow blocked by a shield is ignored, so substituting one term for the other doesn't solve the problem.
This is a niche enough feature with a significant enough nuance thatâbased on this empiric evidence of ongoing, continued reader confusionâI would urge continued exploration as to what terminology could make the model clearer. Other terms that come into mind which do convey the sense that something is temporarily held back rather than overridden:
withTaskCancellationDeferred â I think this conveys the right idea by analogy with defer that it's a "clean-up" feature that wraps things up; in this sense and by analogy, it head-on contradicts the misunderstanding that something is "ignored" forever
I continue to think that, even only on UnsafeCurrentTask, we should not have this spelled out as an API. I guess some of this is contingent on whether the same result can already be achieved by querying task.isCancelled == Task.isCancelled, based on the next section.
Regardless, though, this situation rhymes with prior instances where we've said that adding some API but then obfuscating it with namespaces or in wordier types is not the way: either it's good API or it's not. Namespacing it under UnsafeCurrentTask might be justifiable if the API is literally concurrency-unsafe, but afaict it's not (at least not per se)âthe motivation given, at least, is to just hide it or make it seem scarier so it's less used.
I think this is the correct behavior. However, the proposal is ambiguous on a salient point. It says only that the instance method task.isCancelled queried from outside of the task will return the actual cancelled state regardless of cancellation shield. The rationale given is that it is racy to consider any other behavior.
However, what is the proposed behavior if the instance method is queried from inside of the task? It would not be racy: by that logic, it would seem that it should respect cancellation shields: in other words, Task.isCancelled == task.isCancelled always if task is the current task. However, there is also a case to be made that we should have task.isCancelled always behave as-though it is not in a cancellation shield scope. This would make the API exhibit the same behavior in every contextâmuch simpler to understand.
(It would also incidentally make Task.isCancelled == task.isCancelled a way to query if we're under a shield for debugging purposes, without vending a dedicated API that we have to then warn people not to use in productionâsee previous section.)
Either way, this proposal should spell out explicitly what the behavior is of task.isCancelled when it is invoked in the context where task is the current task.
This topic is pretty far outside of my wheelhouse, and I am very much not an expert in matters of concurrency. However, from reading the discussions about naming this API, I wonder if it might be cleaner to switch from a mental model of:
(a) âPerform these actions while pretending the task is not cancelled.â
To something more like:
(b) âPerform these actions even if the task is cancelled.â
That line of thought could perhaps lead to a new set of spellings that are potentially easier to understand.
I believe the issue with such a formulation is that any code that is called down the stack might check Task.isCancelled without the appropriate âeven if the task is cancelledâ protection. That is, there would be no way for the caller of library code to influence the observed cancellation state. We already have a mechanism for âperform these actions even if the task is cancelledâ which is simply âdonât check the cancellation state.â
I admit i have only skimmed the discussion here, so if this has been brought up before feel free to ignore me
The explanations for the precise behaviour seem to differentiate cancellation from the outside and from inside the task. Maybe it would be worth including that in the name for more clarity? Something like ignoringOutsideCancellation or ignoringParentCancellation perhaps?
withoutPropagatingTaskCancellation is a bit of a mouthful, but is less vague than "shield" in my eyes. That being said, we've been calling them "shields" for a while now.
I actually kind of feel the oppositeâin particular, this feedback:
is IMO exactly what I think a 'new term of art' should aim to achieve. The semantics here are very subtle and I don't think we're going to be able to pick a name which sufficiently communicates in three or four words how all the different edge cases will behave. It's much better for people who encounter this to scratch their heads a bit before they read the docs than to read a succinct name which implies something incorrect about the behavior.
I still think that shield is fairly evocative for the behavior described here, if one adopts the mindset that it's the scope that is shielded rather than a notional observer who moves along with the program counter. Moreover, task cancellation does not act like an arrow, but more like sunlight: cancellation hits the entire task 'at once', but the notional observer may ride the program counter between shielded regions where they can be shaded and pretend its still night.
In any case, I agree that our docs for this feature should take great pains to explain why this behavior is different from 'ignoring' cancellation, but I don't think it's the job of the name to do much more than think "huh, this has some effect on task cancellation... what does it do again?" And I am perfectly happy if we have minor qualms about how perfect the metaphor is so long as it doesn't easily lead to misinterpretations. Judging alternatives from this thread by that standard:
withTaskCancellationDeferred: I think this suggests something too similar to 'ignored', that cancellations which happen inside the block would not actually have any effect until the end. But IIUC if there were a task cancellation handler outside the shield then it would run immediately upon a call to task.cancel from within the shield.
withTaskCancellationSuspended: "suspended" as a term of art here is decent, IMO. I don't think this immediately suggests any improper semantics.
withTaskCancellationInAbeyance: doesn't suggest anything wrong, but IMO is too obscureâassume this one was half-joking.
ignoringOutsideCancellation: decent qualification of the "ignore" terminology IMO! It misses the with and the Task as part of the name, and withOutsideTaskCancellationIgnored starts to get pretty wordy IMO. "Outside" is not immediately clear, so clears the bar of "gets someone to look at the docs".
ignoringParentCancellation: the problem with this that I see is that this is really an operation that affects the execution of the current task rather than the parent taskâwe don't really talk about the current execution environment having the current task as a 'parent'.
withoutPropagatingTaskCancellation: I think this too easily suggests only the "child tasks won't get cancelled if parent task gets cancelled without hinting at all that there's something more (i.e., Task.isCancelled starts lying.
Agree that this should be the behavior if it isn't already, and that the proposal should make this clear!
While I don't see an example in the proposal that confirms this, I assumed that hasActiveTaskCancellationShield would return true even if the task is not cancelledâthat is, in:
we'd always expect it to print true. Granted, given that the stated use case is to demystify "why isn't my Task.isCancelledtrue?" I don't know that the above behavior is needed. But also, giving an API for this specific scenario is IMO more useful than relying on the impicit knowledge that Task.isCancelled != task.isCancelledalways means "you're inside a task cancellation shield". I think it's reasonable to give an UnsafeCurrentTask property for inspecting the value of this task record property directly rather than merely having a roundabout way of spelling it.
Other terms that come into mind which do convey the sense that something is temporarily held back rather than overridden:
I think the temporary nature of the applied effect is already conveyed well by the with-style API, and reinforcing this with additional verbiage isn't necessary.
For this reason, and after reading other arguments, I'm now more open to withTaskCancellationShield naming.
I think for a with-style API it's better to focus on what IS in effect, rather than on what IS NOT.
In this case, the API conveys that Cancellation Shield is in effect for a given scope. What is a Cancellation Shield? You can learn about it in the documentation. This is in contrast to an API that tries to convey what is not in effect, which is cancellation propagation via static Task methods (but not the task-instance ones).
This is definitely a very good addition and personally, I am strongly in favor of adopting the proposal mostly as is, including the "cancellation shield" term of art. I want to point out that IIRC, this has been used to describe the purpose of the "use an unstructured task" pattern before.
While it's true that in the real world, a shield "permanently" blocks an arrow (including from progressing "further down the tree"), I'd argue that this is just another example of such metaphors breaking down at a certain point in computer science anyway.[1]
In the end, the cancellation shield "shields" only the code that's in the scope of the closure that's passed to withTaskCancellationShield, and that code is indeed "permanently" shielded (excluding deeper scopes, as the proposal explains).
With the term as is, I see the best fit for progressive disclosure, "cancellation shield" is hopefully unexpected enough for developers new to the concept to realize "wait, why is it not just named ignored or so? This is a new concept I need to learn?"
What I'd also like to see added is the clarification for the isCancelledinstance method, as @xwu pointed out. This could be addressed by clarifying (in the documentation for the cancellation shields) that this method may be called from outside the task (and usually is), so it is, unlike its static counterpart, not affected by the shield in general (in effect excluding it from anything shield related, correct?).
I'm a little on the edge of the hasActiveTaskCancellationShield and want to point out that regardless of any "this is intended for debugging only" warnings in the documentation, it will be used in production. However, if there are no huge dangers (e.g. performance considerations), I'd be fine with this. Just keep in mind that this will lead to discussions (read: probably heated arguments) down the line, similar to Thread.current.
I still remember people who learned that "software" is called "soft" because the 5.25 inch floppy disks it comes on are, well, "soft and floppy"... (and the "hardware" was a "hard metal box"... don't grill me on this, I'm saying this tongue in cheek). âŠď¸
Iâll try to reply to bigger topics coming up througout the thread.
Thatâs not really right, because the problem this proposal is solving isnât code you control â it is code you donât control.
It is trivial to not check cancellation in your own code, it is impossible to do that in already existing 3rd party code that you cannot change â e.g. a networking library which will avoid making a request from cancelled context. This is why we need to give the caller of such API the ability to wrap a piece of code with a shield or however weâd like to name it.
Yeah, I very much agree with that⌠Iâve worked on and read about many actor runtimes, and sometimes thereâs those which insist to take all the words âtoo literarilyâ and it becomes an un-understandable mess â âthe actors are on a scene and have linesâ and stuff (just tell me they are kept in memory and get messages)⌠it becomes too abstract and too attached to existing words. I think itâs totally fine to make up a name and specify itâs specific meaning in a context, and not let the real world parallels exact meanings be overly overlayed onto the concepts conveyed.
Behaviors: task.isCancelled
This is the intended (and implemented) behavior. I did attempt to make that clear in the proposal, perhaps it needs yet another explanation somewhere.
Behaviors: hasActiveTaskCancellationShield
Thatâs also correct. This just checks if we have the shielg flag set, regardless of cancelled status.
Naming
Yes, thatâs why Iâm more comfortable with a new name rather than imprecise attempts to explain it using the method names. All proposed names so far that attempt to explain the behavior miss some part of it, which will lead to move misunderstandings than a new term of art.
While this may seem pretty descriptive and good, it still is misleading because this doesnât just prevent propagation but also changes the code within the scope of this block to not observe cancellation.
This isnât just preventing propagation (which is the âcause child task to be cancelled as wellâ), so the name is missing to describe half of of the behavior of the API.