Rustam
Rustam

Reputation: 1776

F# async workflow with timeout

I really like F#'s async workflow, but as for me, it has one serious issue: it does not allow creating workflows which should execute no longer than some specific timespan.

To make it clearer, here's a simple function I wrote for myself:

let withTimeout operation timeout = async {
    try
        return Some <| Async.RunSynchronously (operation, timeout)
    with :? TimeoutException -> return None
}

I.e. signature is

val withTimeout : operation:Async<'a> -> timeout:int -> Async<'a option>

Example usage here:

let op = async { 
    do! Async.Sleep(1000) 
    return 1
}
#time
withTimeout op 2000 |> Async.RunSynchronously;;
// Real: 00:00:01.116, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
// val it : unit option = Some 1
withTimeout op 2000 |> Async.RunSynchronously;;
// Real: 00:00:01.004, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
// val it : unit option = Some 1
withTimeout op 500 |> Async.RunSynchronously;;
// Real: 00:00:00.569, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
// val it : unit option = None    

You can see, it works as expected. And it's very nice, but it is also a bit awkward and I'm not sure of it's safety and other issues which might arise. Maybe I am reinventing the wheel, and there's nice and concise way to write such workflows?

Upvotes: 10

Views: 3180

Answers (3)

Siro Mateos
Siro Mateos

Reputation: 408

Just use Async.StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>:

let with_timeout timeout action =
  async {
    let! child = Async.StartChild( action, timeout )
    return! child
  }

Upvotes: 9

Rustam
Rustam

Reputation: 1776

UPD: Best option at the moment was proposed by Vesa A.J.K here: https://stackoverflow.com/a/26230245/1554463. With my edits its like that:

type Async with
    static member WithTimeout (timeout : int option) operation = 
        match timeout with
        | Some time  when time > 0 -> 
            async { 
                let! child = Async.StartChild (operation, time) 
                try 
                    let! result = child 
                    return Some result
                with :? TimeoutException -> return None 
            }
        | _ -> 
            async { 
                let! result = operation
                return Some result
            }

Here's another option:

type Async with
    static member WithCancellation (token:CancellationToken) operation = 
        async {
            try
                let task = Async.StartAsTask (operation, cancellationToken = token)
                task.Wait ()
                return Some task.Result
            with 
                | :? TaskCanceledException -> return None
                | :? AggregateException -> return None
        }

    static member WithTimeout (timeout:int option) operation = 
        match timeout with
        | Some(time) -> 
            async {
                use tokenSource = new CancellationTokenSource (time)
                return! operation |> Async.WithCancellation tokenSource.Token
            }

        | _ -> 
            async { 
                let! res = operation
                return Some res
            }

Here I use .Net tasks and CancellationToken.

Upvotes: 11

MisterMetaphor
MisterMetaphor

Reputation: 6018

Please see this implementation of Async.WhenAny, which is supposed to behave similar to Task.WhenAny.

By using it, you can implement withTimeout similarly to how it is implemented for Task here:

let withTimeout dueTime comp =
    let success = async {
        let! x = comp
        return (Some x)
    }
    let timeout = async {
        do! Async.Delay(dueTime)
        return None
    }
    Async.WhenAny(success, timeout)

I'm not confident that it will cancel the other computation when the first one finishes, but at least this implementation won't block a thread unnecessarily.

Upvotes: 7

Related Questions