2013-06-29 124 views
5

我有一个检索来自互联网网页下面的F#程序:正确处理WebExceptions?

open System.Net 

[<EntryPoint>] 
let main argv = 
    let mutable pageData : byte[] = [| |] 
    let fullURI = "http://www.badaddress.xyz" 
    let wc = new WebClient() 
    try 
     pageData <- wc.DownloadData(fullURI) 
     () 
    with 
    | :? System.Net.WebException as err -> printfn "Web error: \n%s" err.Message 
    | exn -> printfn "Unknown exception:\n%s" exn.Message 

    0 // return an integer exit code 

这工作得很好如果的URI是有效的该机拥有互联网连接 Web服务器正确响应等等。在理想的函数式编程世界中,函数的结果不依赖于不作为参数传递的外部变量(副作用)。

我想知道什么是什么是适当的F#的设计模式,以应付可能需要的功能来处理可恢复外部错误操作。例如,如果网站出现故障,可能需要等待5分钟,然后重试。参数应该多少次重试和重试之间的延迟显式传递,还是可以将这些变量嵌入到函数中?

回答

6

在F#中,当你要处理,你几乎普遍希望使用optionChoice<_,_>可恢复错误。实际上,它们之间的唯一区别是,Choice允许您返回有关错误的一些信息,而option则不允许。换句话说,option是最好的时候它并不重要如何为什么东西失败(只有它确实失败); Choice<_,_>当有关于如何为什么信息失败是重要的。例如,您可能想要将错误信息写入日志;或者您想要根据以不同的方式处理错误情况,为什么某些事情失败 - 这是一个很好的用例,它提供了准确的错误消息来帮助用户诊断问题。

考虑到这一点,这里就是我会重构代码来处理干净,实用的风格故障:

open System 
open System.Net 

/// Retrieves the content at the given URI. 
let retrievePage (client : WebClient) (uri : Uri) = 
    // Preconditions 
    checkNonNull "uri" uri 
    if not <| uri.IsAbsoluteUri then 
     invalidArg "uri" "The URI must be an absolute URI." 

    try 
     // If the data is retrieved successfully, return it. 
     client.DownloadData uri 
     |> Choice1Of2 
    with 
    | :? System.Net.WebException as webExn -> 
     // Return the URI and WebException so they can be used to diagnose the problem. 
     Choice2Of2 (uri, webExn) 
    | _ -> 
     // Reraise any other exceptions -- we don't want to handle them here. 
     reraise() 

/// Retrieves the content at the given URI. 
/// If a WebException is raised when retrieving the content, the request 
/// will be retried up to a specified number of times. 
let rec retrievePageRetry (retryWaitTime : TimeSpan) remainingRetries (client : WebClient) (uri : Uri) = 
    // Preconditions 
    checkNonNull "uri" uri 
    if not <| uri.IsAbsoluteUri then 
     invalidArg "uri" "The URI must be an absolute URI." 
    elif remainingRetries = 0u then 
     invalidArg "remainingRetries" "The number of retries must be greater than zero (0)." 

    // Try to retrieve the page. 
    match retrievePage client uri with 
    | Choice1Of2 _ as result -> 
     // Successfully retrieved the page. Return the result. 
     result 
    | Choice2Of2 _ as error -> 
     // Decrement the number of retries. 
     let retries = remainingRetries - 1u 

     // If there are no retries left, return the error along with the URI 
     // for diagnostic purposes; otherwise, wait a bit and try again. 
     if retries = 0u then error 
     else 
      // NOTE : If this is modified to use 'async', you MUST 
      // change this to use 'Async.Sleep' here instead! 
      System.Threading.Thread.Sleep retryWaitTime 

      // Try retrieving the page again. 
      retrievePageRetry retryWaitTime retries client uri 

[<EntryPoint>] 
let main argv = 
    /// WebClient used for retrieving content. 
    use wc = new WebClient() 

    /// The amount of time to wait before re-attempting to fetch a page. 
    let retryWaitTime = TimeSpan.FromSeconds 2.0 

    /// The maximum number of times we'll try to fetch each page. 
    let maxPageRetries = 3u 

    /// The URI to fetch. 
    let fullURI = Uri ("http://www.badaddress.xyz", UriKind.Absolute) 

    // Fetch the page data. 
    match retrievePageRetry retryWaitTime maxPageRetries wc fullURI with 
    | Choice1Of2 pageData -> 
     printfn "Retrieved %u bytes from: %O" (Array.length pageData) fullURI 

     0 // Success 
    | Choice2Of2 (uri, error) -> 
     printfn "Unable to retrieve the content from: %O" uri 
     printfn "HTTP Status: (%i) %O" (int error.Status) error.Status 
     printfn "Message: %s" error.Message 

     1 // Failure 

基本上,我拆你的代码伸到两个功能,加上原来main

  • 一个函数,试图从指定的URI检索内容。
  • 一个包含重试尝试逻辑的函数;这'包装'执行实际请求的第一个函数。
  • 原始主要功能现在只处理'设置'(您可以轻松从app.configweb.config中提取)并打印最终结果。换句话说,它忽略了重试逻辑 - 您可以使用match语句修改单行代码,并根据需要使用非重试请求功能。

如果你想从多个URI拉内容,等待的时间显著量(例如5分钟)重试之间,你应该修改重试逻辑使用优先级队列或东西,而不是使用Thread.SleepAsync.Sleep

无耻的插件:我的ExtCore库包含一些东西,当你构建这样的事情时,你的生活将变得更加容易,特别是如果你想让它变得异步。最重要的是,它提供了一个asyncChoice工作流程和collections functions designed to work with it

至于你关于传入参数的问题(比如重试超时和重试次数) - 我不认为有一个确定是否通过它们或硬编码它们的硬性规则功能。在大多数情况下,我更愿意将它们传入,但如果您有超过几个参数传入,您最好创建一个记录来保存它们并传递它们。我使用的另一种方法是创建参数option的值,其中默认值是从配置文件中提取的(尽管您想从文件中将它们拉出一次,并将它们分配给某个专用字段以避免重新解析每次你的函数被调用时,配置文件);这可以很容易地修改您在代码中使用的默认值,但也可以在必要时灵活地覆盖它们。