548 Stimmen

Wie schreibe ich die Wiederholungslogik am saubersten?

Gelegentlich muss ich einen Vorgang mehrmals wiederholen, bevor ich aufgeben kann. Mein Code ist wie:

int retries = 3;
while(true) {
  try {
    DoSomething();
    break; // success!
  } catch {
    if(--retries == 0) throw;
    else Thread.Sleep(1000);
  }
}

Ich würde dies gerne in eine allgemeine Wiederholungsfunktion umschreiben:

TryThreeTimes(DoSomething);

Ist dies in C# möglich? Wie würde der Code für die TryThreeTimes() Methode?

646voto

LBushkin Punkte 124894

Blanket catch-Anweisungen, die einfach den gleichen Aufruf wiederholen, können gefährlich sein, wenn sie als allgemeiner Mechanismus zur Behandlung von Ausnahmen verwendet werden. Daher hier ein Lambda-basierter Retry-Wrapper, den Sie mit jeder Methode verwenden können. Ich habe mich dafür entschieden, die Anzahl der Wiederholungsversuche und die Wiederholungszeitüberschreitung als Parameter anzugeben, um etwas mehr Flexibilität zu erreichen:

public static class Retry
{
    public static void Do(
        Action action,
        TimeSpan retryInterval,
        int maxAttemptCount = 3)
    {
        Do<object>(() =>
        {
            action();
            return null;
        }, retryInterval, maxAttemptCount);
    }

    public static T Do<T>(
        Func<T> action,
        TimeSpan retryInterval,
        int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();

        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    Thread.Sleep(retryInterval);
                }
                return action();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }
}

Mit dieser Utility-Methode können Sie nun eine Wiederholungslogik durchführen:

Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

oder:

Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));

oder:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);

Oder Sie könnten sogar eine async Überlastung.

283voto

Michael Wolfenden Punkte 2791

Sie sollten versuchen Polly . Es ist eine von mir geschriebene .NET-Bibliothek, die es Entwicklern ermöglicht, Richtlinien für die Behandlung transienter Ausnahmen wie Retry, Retry Forever, Wait and Retry oder Circuit Breaker fließend auszudrücken.

Beispiel

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(3))
    .Execute(() => DoSomething());

78voto

Drew Noakes Punkte 282438
public void TryThreeTimes(Action action)
{
    var tries = 3;
    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tries == 0)
                throw;
            Thread.Sleep(1000);
        }
    }
}

Dann würden Sie anrufen:

TryThreeTimes(DoSomething);

...oder alternativ...

TryThreeTimes(() => DoSomethingElse(withLocalVariable));

Eine flexiblere Option:

public void DoWithRetry(Action action, TimeSpan sleepPeriod, int tryCount = 3)
{
    if (tryCount <= 0)
        throw new ArgumentOutOfRangeException(nameof(tryCount));

    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tryCount == 0)
                throw;
            Thread.Sleep(sleepPeriod);
        }
   }
}

Zu verwenden als:

DoWithRetry(DoSomething, TimeSpan.FromSeconds(2), tryCount: 10);

Eine modernere Version mit Unterstützung für async/await:

public async Task DoWithRetryAsync(Func<Task> action, TimeSpan sleepPeriod, int tryCount = 3)
{
    if (tryCount <= 0)
        throw new ArgumentOutOfRangeException(nameof(tryCount));

    while (true) {
        try {
            await action();
            return; // success!
        } catch {
            if (--tryCount == 0)
                throw;
            await Task.Delay(sleepPeriod);
        }
   }
}

Zu verwenden als:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);

58voto

Eric Lippert Punkte 628543

Dies ist möglicherweise eine schlechte Idee. Erstens ist es sinnbildlich für die Maxime "Die Definition von Wahnsinn ist, dass man dieselbe Sache zweimal macht und jedes Mal andere Ergebnisse erwartet". Zweitens lässt sich dieses Kodierungsmuster nicht gut mit sich selbst vereinbaren. Ein Beispiel:

Nehmen wir an, die Hardware-Schicht Ihres Netzes sendet ein Paket bei einem Fehler dreimal erneut, wobei zwischen den Fehlern etwa eine Sekunde gewartet wird.

Nehmen wir nun an, die Softwareschicht sendet bei einem Paketfehler dreimal eine Benachrichtigung über einen Fehler zurück.

Nehmen wir nun an, dass die Benachrichtigungsschicht die Benachrichtigung dreimal reaktiviert, wenn die Zustellung einer Benachrichtigung fehlgeschlagen ist.

Nehmen wir nun an, die Fehlermeldeschicht reaktiviert die Meldeschicht dreimal bei einem Meldefehler.

Und nun nehmen wir an, der Webserver reaktiviert die Fehlermeldung dreimal bei Fehlern.

Und nun nehmen wir an, der Web-Client sendet die Anfrage dreimal erneut, wenn er einen Fehler vom Server erhält.

Nehmen wir nun an, die Leitung am Netzwerk-Switch, die die Benachrichtigung an den Administrator weiterleiten soll, ist nicht angeschlossen. Wann erhält der Benutzer des Webclients endlich seine Fehlermeldung? Ich rechne mit etwa zwölf Minuten später.

Damit Sie nicht denken, dass dies nur ein dummes Beispiel ist: Wir haben diesen Fehler in Kundencode gesehen, allerdings viel, viel schlimmer als ich hier beschrieben habe. In dem speziellen Kundencode betrug die Zeitspanne zwischen dem Auftreten des Fehlers und seiner Meldung an den Benutzer mehrere Wochen weil so viele Schichten automatisch einen neuen Versuch mit Wartezeiten unternahmen. Stellen Sie sich nur vor, was passieren würde, wenn es zehn Wiederholungen anstelle von drei .

Normalerweise ist es richtig, bei einer Fehlerbedingung Folgendes zu tun sofort melden und den Nutzer entscheiden lassen, was zu tun ist. Wenn der Benutzer eine Richtlinie für automatische Wiederholungsversuche erstellen möchte, kann er diese Richtlinie auf der entsprechenden Ebene der Softwareabstraktion erstellen.

18voto

Martin R-L Punkte 3989

Ich bin ein Fan von Rekursion und Erweiterungsmethoden, also hier sind meine zwei Cents:

public static void InvokeWithRetries(this Action @this, ushort numberOfRetries)
{
    try
    {
        @this();
    }
    catch
    {
        if (numberOfRetries == 0)
            throw;

        InvokeWithRetries(@this, --numberOfRetries);
    }
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X