2013-06-29 2 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 

Это прекрасно работает если Ури действует и машина имеет подключение к Интернету и веб-сервер отвечает правильно и т. д. В идеальном мире функционального программирования результаты функции не будут зависеть от внешних переменных, которые не передаются как аргументы (побочные эффекты).

То, что я хотел бы знать, это то, что соответствующий F # шаблон проектирования, чтобы иметь дело с операциями, которые могут потребовать функции, чтобы иметь дело с извлекаемых внешних ошибок. Например, если веб-сайт отключен, вы можете подождать 5 минут и повторите попытку. Должны ли передаваться параметры, например, сколько раз повторять попытку и задержки между попытками, или можно ли вставить эти переменные в функцию?

ответ

6

В F #, когда вы хотите обрабатывать извлекаемые ошибки вы почти повсеместно хотите использовать option или Choice<_,_> типа. На практике единственная разница между ними заключается в том, что 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.config или web.config) и распечатать окончательные результаты. Другими словами, он не обращает внимания на логику повторной попытки - вы можете изменить одну строку кода с помощью оператора match и вместо этого использовать функцию запроса на повторную попытку, если хотите.

Если вы хотите, чтобы вытащить содержимое из нескольких URI, и ждать в течение значительного периода времени (например, 5 минут) между повторами, вы должны изменить логику Повторная попытка использовать очереди приоритета или что-то вместо того, чтобы использовать Thread.Sleep или Async.Sleep.

Бесстыдная плагин: моя библиотека ExtCore содержит некоторые вещи, которые значительно облегчают вашу жизнь при создании чего-то подобного, особенно если вы хотите сделать все асинхронным. Самое главное, он обеспечивает рабочий процесс asyncChoice и collections functions designed to work with it.

Что касается вопроса о передаче параметров (например, тайм-аута повтора и количества повторных попыток) - я не думаю, что существует твердое правило для принятия решения о том, следует ли передавать или жестко кодировать их внутри функция. В большинстве случаев я предпочитаю передавать их, хотя, если у вас есть более чем несколько параметров для перехода, вам лучше создать запись, чтобы держать их все и передавать это вместо этого. Другой подход, который я использовал, - это сделать значения параметров option, где значения по умолчанию извлекаются из файла конфигурации (хотя вы хотите вытащить их из файла один раз и назначить их в какое-то частное поле, чтобы избежать повторного разбора файл конфигурации каждый раз, когда вы вызываете свою функцию); это упрощает изменение значений по умолчанию, которые вы использовали в вашем коде, но также дает вам возможность переопределять их, когда это необходимо.

Смежные вопросы