Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 589
Rustam Avatar asked Mar 07 '14 08:03

Rustam


3 Answers

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.

like image 172
Rustam Avatar answered Nov 15 '22 15:11

Rustam


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
  }
like image 8
Siro Mateos Avatar answered Nov 15 '22 16:11

Siro Mateos


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.

like image 7
MisterMetaphor Avatar answered Nov 15 '22 15:11

MisterMetaphor