Skip to content

Discussion on async/cancellation and exceptions #78

@dsyme

Description

@dsyme

@TIHan and I have been discussing using FSharp.Data.Adaptive in future work on making parts of the F# compiler and FSharp.Compiler.Service more incremental.

We've some questions about async, cancellation and exceptions.

As context, in our current incremental model the operations to evaluate final nodes in the dependency graph are always cancellable. In the language of FSharp.Data.Adaptive I believe this means as follows. Consider these operations:

ASet.map: ('T -> 'U) -> aset<'T> -> aset<'T>
Aset.contains: aval<'T> -> aset<'T> -> aval<'T>
Aset.elements: aset<'T> -> aval<Set<'T>>
Aval.force: aval<'T> -> 'T

In these cases, the 'T -> 'U functions are not cancellable and might throw exceptions. My understanding is that

  1. if any such function throws an exception then FSharp.Data.Adaptive never "saves" the exception as a result, it is just always propagated all the way up to the routine that requested the result.

  2. some parts of the dependency graph calculation may have been committed, and this is done in an atomic way (there is a related question about concurrent update requests)

  3. there is no special treatment if OperationCancelledException is thrown, and so throwing that exception is a valid way to represent cancellation.

  4. however there is no cancellation token propagated, so these functions have to capture an exterior cancellation token.

Now one could imagine a variation on FSharp.Data.Adaptive that either

  1. propagates a cancellation token

  2. uses a computation expression like cancellable { ... } to represent synchronous cancellable code

  3. uses an existing computation like async { ... } that has cancellation built in (but is richer, as it represents asynchronous code)

Which would lead to signatures like this (option 1)

ASet.mapCancellable: (CancellationToken -> 'T -> 'U) -> aset<'T> -> aset<'T>
Aval.force: aval<'T> * ?cancellationToken: CancellationToken -> 'T

or this (option 2)

ASet.mapCancellable: ('T -> Cancellable<'U>) -> aset<'T> -> aset<'T>
Aval.force: aval<'T> -> Cancellable<'T>
Cancellable.run: Cancellable<'T> * ?cancellationToken: CancellationToken -> 'T

or this (option 3)

ASet.mapAsync: ('T -> Async<'U>) -> aset<'T> -> aset<'T>
Aval.force: aval<'T> -> Async<'T>

or this (combo of option 2 and 3 for tasks)

ASet.mapAsync: (CancellationToken -> 'T -> Task<'U>) -> aset<'T> -> aset<'T>
Aval.force: aval<'T> * ?cancellationToken: CancellationToken -> Task<'T>

I'm curious about your thinking about this. Obviously complicates things.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featurea feature to be implemented

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions