2013-06-29 4 views
5

Ich habe folgendes Fis-Programm, das eine Webseite aus dem Internet abruft:WebExceptions richtig behandeln?

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 

Dies funktioniert wenn die URI gültig ist und die Maschine über eine Internetverbindung hat und der Webserver richtig reagiert usw. In einer idealen funktionalen Programmierwelt wären die Ergebnisse einer Funktion nicht von externen Variablen abhängig, die nicht als Argumente übergeben wurden (Nebeneffekte).

Was Ich mag würde, wissen, was das entsprechende Muster F # Designs ist mit Operationen zu behandeln, die die Funktion erforderlich machen könnten mit erzielbare externen Fehlern zu behandeln. Zum Beispiel, wenn die Website down ist, möchte man 5 Minuten warten und es erneut versuchen. Sollen Parameter wie die Anzahl der Wiederholungen und Verzögerungen zwischen Wiederholungen explizit übergeben werden oder ist es in Ordnung, diese Variablen in die Funktion einzubetten?

Antwort

6

In F #, wenn Sie erzielbare Fehler behandeln wollen Sie fast überall die option oder Choice<_,_> Typ verwenden möchten. In der Praxis ist der einzige Unterschied zwischen ihnen, dass Sie Choice können Sie einige Informationen über den Fehler zurückgeben, während option nicht. Mit anderen Worten, option ist am besten, wenn es egal ist wie oder warum etwas fehlgeschlagen (nur, dass es fehlgeschlagen ist); Choice<_,_> wird verwendet, wenn Sie Informationen über wie oder haben, warum etwas fehlgeschlagen ist wichtig. Zum Beispiel möchten Sie möglicherweise die Fehlerinformationen in ein Protokoll schreiben; oder vielleicht möchten Sie eine Fehlersituation anders behandeln, basierend auf , warum etwas fehlgeschlagen ist - ein großer Anwendungsfall dafür ist die Bereitstellung genauer Fehlermeldungen, die Benutzern helfen, ein Problem zu diagnostizieren.

, die mit im Auge, hier ist, wie ich Ihren Code Refactoring würde Ausfälle in einem sauberen, funktionalen Stil zu handhaben: out in zwei Funktionen, plus die ursprünglichen main

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 

Grundsätzlich ist teilten Sie Ihren Code:

  • Eine Funktion, die versucht, den Inhalt von einem angegebenen URI abzurufen.
  • Eine Funktion, die die Logik für Versuche zum erneuten Versuch enthält; Dies "wickelt" die erste Funktion ein, die die tatsächlichen Anforderungen ausführt.
  • Die ursprüngliche Hauptfunktion behandelt jetzt nur 'Einstellungen' (die Sie leicht aus einem app.config oder web.config ziehen können) und die endgültigen Ergebnisse drucken. Mit anderen Worten, die Wiederholungslogik wird ignoriert - Sie können die einzelne Codezeile mit der match-Anweisung ändern und stattdessen die Anforderungsfunktion verwenden, die nicht erneut versucht, wenn Sie möchten.

Wenn Sie Inhalte aus mehrer URIs ziehen wollen und für eine erhebliche Menge an Zeit warten (zB 5 Minuten) zwischen den Wiederholungen, sollten Sie die retrying Logik ändern, um eine Prioritätswarteschlange oder etwas zu verwenden, anstatt Thread.Sleep der Verwendung oder Async.Sleep.

Shameless Stecker: meine ExtCore Bibliothek enthält einige Dinge, um Ihr Leben wesentlich einfacher zu machen, wenn Sie so etwas bauen, vor allem, wenn Sie alles asynchron machen wollen. Vor allem bietet es einen asyncChoice Workflow und collections functions designed to work with it.

Wie für Ihre Frage über die Übergabe von Parametern (wie das Wiederholungs-Timeout und die Anzahl der Wiederholungen) - Ich glaube nicht, dass es eine hart-und-schnell-Regel für die Entscheidung, ob sie in ihnen übergeben oder fest codiert die Funktion. In den meisten Fällen übergebe ich sie lieber. Wenn Sie jedoch mehr als ein paar Parameter eingeben müssen, erstellen Sie am besten einen Datensatz, um sie alle zu speichern und stattdessen zu übergeben. Ein anderer Ansatz, den ich verwendet habe, ist die Parameter option Werte, wo die Standardwerte aus einer Konfigurationsdatei gezogen werden (obwohl Sie sie aus der Datei einmal ziehen und sie zu einem privaten Feld zuweisen, um erneute Analyse zu vermeiden die Konfigurationsdatei jedes Mal, wenn Ihre Funktion aufgerufen wird); Auf diese Weise können Sie die Standardwerte, die Sie in Ihrem Code verwendet haben, leicht ändern, und Sie können diese bei Bedarf auch überschreiben.