Skip to content

fix(js/ts): align tasks and cancellation with .NET - #5199

Open
leonitousconforti wants to merge 6 commits into
fable-compiler:mainfrom
leonitousconforti:fix-task-cancellation
Open

leonitousconforti wants to merge 6 commits into
fable-compiler:mainfrom
leonitousconforti:fix-task-cancellation

Conversation

@leonitousconforti

Copy link
Copy Markdown
Contributor

Tasks and cancellation in JS/TS now behave like .NET where they crashed, failed to compile or differed. One commit per concern, each with tests.

  • TaskCompletionSource: SetCanceled works (the runtime spelt it SetCancelled, kept as an alias). TrySetResult/TrySetException/TrySetCanceled are supported, a second Set* raises InvalidOperationException, and the non-generic TaskCompletionSource compiles. SetException(seq<exn>) faulted the task with the F# list itself and now faults with the first exception.
  • Task.Run compiled to an import of a missing run, so the whole module failed to load. Its overloads are supported, including an already-cancelled token.
  • A faulted or cancelled task that nothing awaits (from TaskCompletionSource, Async.StartAsTask, Task.Run or Task.Delay) no longer raises an unhandled promise rejection, as fix(js/ts): async cancellation propagation and continuation double-resolve #4746 did for StartChild. Awaiters still see the failure.
  • ThrowIfCancellationRequested raised a plain Exception, and OperationCanceledException() in user code compiled to new Exception(), so :? OperationCanceledException matched neither. Both now create the library's OperationCanceledException, with .NET's message.
  • CancellationTokenSource.CreateLinkedTokenSource and direct calls to CancellationTokenRegistration.Dispose()/Unregister() are supported (they didn't compile).
  • Behaviour change: Async.AwaitTask of a cancelled task silently cancelled the awaiting workflow, skipping any try/with. Like .NET, and like fix(python): settle cancelled tasks awaited by async workflows #5064 did for Python, it now raises TaskCanceledException, a new OperationCanceledException subclass. Cancelled library tasks reject with it too, so let! in task { } sees .NET's type. Async.AwaitPromise, Parallel and StartChild are unchanged.

Notes:

  • A rejection can't tell a cancelled task from one faulted with an OperationCanceledException, so AwaitTask raises TaskCanceledException for both, where .NET raises AggregateException for the second.
  • Not covered: Task.IsCompleted/Status, Task.Result, wrapping faults in AggregateException, unobserved failures of task { } itself, and CancellationTokenRegistration.Token.

TaskCompletionSource differed from .NET in several ways:

- SetCanceled works. The runtime spelt it SetCancelled, so every call
  raised "SetCanceled is not a function". SetCancelled is kept for JS
  code that calls it.
- TrySetResult, TrySetException and TrySetCanceled return whether they
  completed the task.
- A second SetResult, SetException or SetCanceled raises
  InvalidOperationException with the .NET message instead of being
  ignored.
- SetException and TrySetException with a sequence fault the task with
  the first exception, which is what awaiting it throws on .NET. They
  rejected the task with the F# list itself. An empty sequence or a
  null element raises ArgumentException.
- The non-generic TaskCompletionSource compiles to the same class
  instead of failing with "not supported by Fable".
Task.Run compiled to an import of a run function that the library did
not export, so the module that used it failed to load. Task.Run now
runs the function after the caller continues, for the Func<T>,
Func<Task<T>>, Action and Func<Task> overloads, and unwraps a returned
task. With a CancellationToken that is cancelled by the time it would
run, the function does not run and the task is cancelled.
A faulted or cancelled task that nothing awaits is silent on .NET, but
in JS its rejected promise was reported as an unhandled rejection,
which ends a Node process by default. This happened for the tasks of
TaskCompletionSource, Async.StartAsTask, Async.StartImmediateAsTask,
Task.Run and a cancelled Task.Delay. They now get a rejection handler
when they are created, as Async.StartChild children already do, and
awaiters still see the failure.
CancellationToken.ThrowIfCancellationRequested raised a plain
Exception("Operation is cancelled"), and OperationCanceledException
constructed in user code compiled to a plain Exception, so
:? OperationCanceledException did not match either of them. Both now
create the library's OperationCanceledException, which the type test
already used. The constructor overloads that take a CancellationToken
drop it, the inner exception is kept, and the default message is .NET's
"The operation was canceled.".
- CancellationTokenSource.CreateLinkedTokenSource with one token, two
  tokens or an array of tokens was not supported by Fable. The linked
  source is cancelled when any parent is, at once if a parent is already
  cancelled, and cancelling it leaves the parents alone. Like .NET, a
  default token is skipped, no tokens raise ArgumentException, and
  disposing the source unlinks it from its parents. Dispose on a
  CancellationTokenSource compiled to nothing, so it now calls the
  source's Dispose, which does nothing for a source that is not linked.
- CancellationTokenRegistration.Dispose and Unregister called directly
  were not supported by Fable. They remove the callback, and Unregister
  returns whether it was still registered. Like disposing through
  IDisposable, which already worked, Dispose does nothing for a default
  registration. CancellationTokenRegistration.Token is still not
  supported.
Async.AwaitTask of a cancelled task cancelled the awaiting workflow, so
try/with did not catch it and the whole workflow was cancelled. Like
.NET, it now raises TaskCanceledException through the error
continuation, as fable-compiler#5064 did for Python. Async.AwaitPromise,
Async.Parallel, Async.Sequential and Async.StartChild still treat an
OperationCanceledException rejection as a cancellation.

TaskCanceledException is a new OperationCanceledException subclass with
.NET's "A task was canceled." message, and it can be constructed and
type tested. The tasks of TaskCompletionSource.SetCanceled, Task.Run
and Task.Delay with a cancelled token, and a cancelled
Async.StartAsTask or Async.StartImmediateAsTask, reject with it, so
awaiting them in task { } also raises TaskCanceledException as on .NET.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant