2013-06-29 13 views
5

Ho il seguente programma F # che recupera una pagina web da internet:Gestione delle eccezioni Web correttamente?

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 

Questo funziona bene se l'URI è valida e la macchina ha una connessione internet e il server web risponde correttamente ecc. In un mondo di programmazione funzionale ideale, i risultati di una funzione non dipenderanno da variabili esterne non passate come argomenti (effetti collaterali).

Quello che vorrei sapere è ciò che è appropriato F # modello di progettazione per le operazioni che potrebbero richiedere la funzione di trattare con recuperabili errori esterni. Ad esempio, se il sito web è inattivo, è possibile attendere 5 minuti e riprovare. Dovrebbero essere passati esplicitamente parametri come quante volte riprovare e ritardi tra i tentativi o è corretto inserire queste variabili nella funzione?

risposta

6

in F #, quando si desidera gestire recuperabili errori quasi universalmente desidera utilizzare il o il tipo optionChoice<_,_>. In pratica l'unica differenza tra loro è che Choice consente di restituire alcune informazioni sull'errore mentre option no. In altre parole, option è la cosa migliore quando non è importante in che modo o perché un errore non è riuscito (solo che ha avuto esito negativo); Choice<_,_> viene utilizzato quando si dispone di informazioni su come o perché il problema di non è riuscito. Ad esempio, potresti voler scrivere le informazioni di errore in un log; o forse vuoi gestire una situazione di errore in modo diverso in base a perché il problema di non è riuscito: un ottimo caso per questo è fornire messaggi di errore precisi per aiutare gli utenti a diagnosticare un problema.

Con questo in mente, ecco come mi piacerebbe refactoring del codice per gestire gli errori in uno stile funzionale, pulito:

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 

Fondamentalmente, ho diviso il codice fuori in due funzioni, più l'originale main:

  • Una funzione che tenta di recuperare il contenuto da un URI specificato.
  • Una funzione che contiene la logica per tentativi di riprovare; questo "avvolge" la prima funzione che esegue le richieste effettive.
  • La funzione principale originale ora gestisce solo le "impostazioni" (che è possibile estrarre facilmente da un app.config o web.config) e stampare i risultati finali. In altre parole, è ignaro della logica di riprogrammazione: è possibile modificare la singola riga di codice con l'istruzione match e utilizzare la funzione di richiesta non ripetuta, se lo si desidera.

Se si vuole estrarre il contenuto da più URI e attendere una notevole quantità di tempo (ad esempio, 5 minuti) tra i tentativi, è necessario modificare la logica di ritentare di utilizzare una coda di priorità o qualcosa invece di utilizzare Thread.Sleep o Async.Sleep.

Plug vergogna: la mia libreria ExtCore contiene alcune cose per rendere la vita molto più semplice quando si costruisce qualcosa di simile, soprattutto se si desidera rendere tutto asincrono. Ancora più importante, fornisce un flusso di lavoro asyncChoice e collections functions designed to work with it.

Per quanto riguarda la domanda relativa al passaggio dei parametri (come il numero di tentativi e il numero di tentativi), non penso ci sia una regola ferrea per decidere se passarli o codificarli all'interno la funzione. Nella maggior parte dei casi, preferisco passarli, ma se hai più di alcuni parametri da passare, è meglio creare un record per tenerli tutti e passare quello. Un altro approccio che ho usato è quello di creare i valori option, dove i valori di default sono estratti da un file di configurazione (anche se vorrai estrarli dal file una volta e assegnarli ad un campo privato per evitare ri-analisi il file di configurazione ogni volta che viene chiamata la funzione); questo facilita la modifica dei valori predefiniti che hai usato nel tuo codice, ma ti dà anche la possibilità di sovrascriverli quando necessario.