Skip to content

Dispose via use in async workflows has a race condition with cancellation #1436

@polytypic

Description

@polytypic

The current implementation of the use construct of the asynchronous workflow expressions has a race condition with respect to cancellation: if cancellation is requested at the right time, a disposable may be constructed, but dispose will not be called.

Consider the implementation of use in terms of try-finally. Due to cancellation, the implementation of try-finally does not translate to an implementation of use that guarantees disposal.

Here is a pair of examples that demonstrates the issue:

open System
open System.Threading

do let source = new CancellationTokenSource ()
   let token = source.Token
   let example = async {
     printfn "A: Here we go"
     use! h = Async.OnCancel <| fun () -> printfn "A: We got cancelled"
     use x = source.Cancel ()
             printfn "A: Creating disposable"
             {new IDisposable with
               override this.Dispose () = printfn "A: Disposed properly"}
     do printfn "A: Never getting here"
   }
   Async.Start (example, token)

do let source = new CancellationTokenSource ()
   let token = source.Token
   let example = async {
     printfn "B: Here we go"
     use! h = Async.OnCancel <| fun () -> printfn "B: We got cancelled"
     use x = printfn "B: Creating disposable"
             {new IDisposable with
               override this.Dispose () = printfn "B: Disposed properly"}
     do source.Cancel ()
     do! async { return () }
     do printfn "B: Never getting here"
   }
   Async.Start (example, token)

Here is sample output obtained by running the above with F# Interactive for F# 4.0 (Open Source Edition):

B: Here we go
A: Here we go
B: Creating disposable
A: We got cancelled
B: We got cancelled
A: Creating disposable
B: Disposed properly

As can be seen, in both cases disposables are created, but only in case B the disposable is disposed. Note that the example uses synchronous code to trigger the behavior. In actual use scenarios cancellation is triggered by code on other threads and the result is that disposal will be non-deterministically cancelled.

I would expect use to guarantee disposal in both cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-LibraryIssues for FSharp.Core not covered elsewhereBugImpact-Medium(Internal MS Team use only) Describes an issue with moderate impact on existing code.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions