使用F#-Style异步工作流在WebRequest上超时

时间:2011-10-30 21:52:33

标签: asynchronous f# httpwebrequest

对于更广泛的上下文,此处为 my code ,可下载网址列表。

在我看来,使用use! response = request.AsyncGetResponse()样式的URL提取时,没有好办法处理F#中的超时。我几乎所有工作都按照我的喜好(错误处理和异步请求和响应下载)保存了网站需要很长时间才能响应的问题。我当前的代码只是无限期挂起。我已经在我编写的等待300秒的PHP脚本上尝试过它。它一直等待着。

我找到了两种“解决方案”,这两种解决方案都是不可取的。

AwaitIAsyncResult + BeginGetResponse

this other Stack Overflow question ildjarn 的答案相同。这样做的问题是,如果您排队了许多异步请求,则会在AwaitIAsyncResult上人为阻止某些异步请求。换句话说,已经进行了发出请求的调用,但幕后的一些事情阻止了调用。这会导致在发出许多并发请求时过早触发AwaitIAsyncResult上的超时。我的猜测是对单个域的请求数量的限制,或者只是对总请求的限制。

为了支持我的怀疑,我写了一些WPF应用程序来绘制请求似乎开始和结束的时间表。在我上面链接的代码中,注意计时器的启动和停止在第49和54行(调用第10行)。这是resulting timeline image

当我将计时器开始移动到初始响应之后(所以我只计算下载内容的时间),the timeline looks a lot more realistic。注意,这是两个单独的运行,但除了启动计时器之外没有代码更改。我没有在startTime之前直接测量use! response = request.AsyncGetResponse(),而是在之后直接测量。

为了进一步支持我的主张,我制作了Fiddler2的时间表。这是the resulting timeline很明显,当我告诉他们时,请求并没有完全开始。

新帖子中的

GetResponseStream

换句话说,同步请求和下载调用是在辅助线程中进行的。此 工作,因为GetResponseStream尊重Timeout对象上的WebRequest属性。但是在这个过程中,我们失去了所有的等待时间,因为请求已经在线上并且响应还没有回来。我们不妨用C#...;)

写出来

问题

  • 这是一个已知的问题吗?
  • 是否有任何好的解决方案可以利用F#异步工作流并仍然允许超时错误处理?
  • 如果问题确实是我一次发出太多请求,那么限制请求数量的最佳方法是使用Semaphore(5, 5)或类似的东西吗?
  • 问题:如果你看过我的代码,你能看到我做过的任何愚蠢的事情吗?

如果您有任何疑惑,请告诉我。

2 个答案:

答案 0 :(得分:0)

AsyncGetResponse只是忽略发布的任何超时值...这是我们刚刚解决的解决方案:

open System
open System.IO
open System.Net

type Request = Request of WebRequest * AsyncReplyChannel<WebResponse>

let requestAgent =
    MailboxProcessor.Start <| fun inbox -> async {
            while true do
                let! (Request (req, port)) = inbox.Receive ()

                async {
                    try
                        let! resp = req.AsyncGetResponse ()
                        port.Reply resp
                    with
                    | ex -> sprintf "Exception in child %s\n%s" (ex.GetType().Name) ex.Message |> Console.WriteLine
                } |> Async.Start
        }

let getHTML url =
    async {
        try
            let req = "http://" + url |> WebRequest.Create
            try
                use! resp = requestAgent.PostAndAsyncReply ((fun chan -> Request (req, chan)), 1000)
                use str = resp.GetResponseStream ()
                use rdr = new StreamReader (str)
                return Some <| rdr.ReadToEnd ()
            with
            | :? System.TimeoutException ->
                req.Abort()
                Console.WriteLine "RequestAgent call timed out"
                return None
        with
        | ex ->
            sprintf "Exception in request %s\n\n%s" (ex.GetType().Name) ex.Message |> Console.WriteLine
            return None
    } |> Async.RunSynchronously;;

getHTML "www.grogogle.com"

即。我们委托给另一个代理并调用它提供异步超时...如果我们在指定的时间内没有得到代理的回复,我们就会中止请求并继续。

答案 1 :(得分:0)

我看到我的其他答案可能无法回答您的特定问题...这是任务限制器的另一个实现,不需要使用信号量。

open System

type IParallelLimiter =
    abstract GetToken : unit -> Async<IDisposable>

type Message= 
    | GetToken of AsyncReplyChannel<IDisposable>
    | Release

let start count =
    let agent =
        MailboxProcessor.Start(fun inbox ->
            let newToken () =
                { new IDisposable with
                    member x.Dispose () = inbox.Post Release }

            let rec loop n = async {
                    let! msg = inbox.Scan <| function
                        | GetToken _ when n = 0 -> None
                        | msg -> async.Return msg |> Some

                    return!
                        match msg with
                        | Release ->
                            loop (n + 1)
                        | GetToken port ->
                            port.Reply <| newToken ()
                            loop (n - 1)
                }
            loop count)

    { new IParallelLimiter with
        member x.GetToken () =
            agent.PostAndAsyncReply GetToken}

let limiter = start 100;;

for _ in 0..1000 do
    async {
        use! token = limiter.GetToken ()
        Console.WriteLine "Sleeping..."
        do! Async.Sleep 3000
        Console.WriteLine "Releasing..."
    } |> Async.Start