Правильно обрабатывать WebExceptions?

У меня есть следующая программа F#, которая извлекает веб-страницу из Интернета:

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

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


person JonnyBoats    schedule 29.06.2013    source источник


Ответы (1)


В 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 рабочий процесс и функции коллекций, предназначенные для работы с ним.

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

person Jack P.    schedule 29.06.2013