Najczystszy sposób na napisanie logiki ponownej próby?


455

Czasami muszę poddać się operacji kilka razy, zanim się poddam. Mój kod jest jak:

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

Chciałbym przepisać to w ogólnej funkcji ponownej próby, takiej jak:

TryThreeTimes(DoSomething);

Czy jest to możliwe w C #? Jaki byłby kod dla tej TryThreeTimes()metody?


1
Prosty cykl nie wystarczy? Dlaczego po prostu nie powtarzać i wykonywać logiki kilka razy?
Restuta

13
Osobiście byłbym bardzo ostrożny wobec takich metod pomocniczych. Z pewnością możliwe jest wdrożenie za pomocą lambd, ale sam wzór jest wyjątkowo śmierdzący, więc wprowadzenie do niego pomocnika (co oznacza, że ​​często się powtarza) jest samo w sobie bardzo podejrzane i mocno sugeruje zły ogólny projekt.
Pavel Minaev

12
W moim przypadku moje DoSomething () robią rzeczy na zdalnych komputerach, takie jak usuwanie plików lub próby trafienia do portu sieciowego. W obu przypadkach występują poważne problemy z czasem, w którym DoSomething odniesie sukces, a ze względu na oddalenie nie ma zdarzenia, na którym można by słuchać. Więc tak, śmierdzi. Sugestie mile widziane.
noctonura

13
@PavelMinaev dlaczego używanie ponownych prób wskazywałoby na zły ogólny projekt? Jeśli piszesz dużo kodu łączącego punkty integracji, to ponowne próby są zdecydowanie wzorcem, który powinieneś poważnie rozważyć.
bytedev

Odpowiedzi:


569

Ogólne instrukcje catch, które po prostu ponawiają to samo wywołanie, mogą być niebezpieczne, jeśli zostaną użyte jako ogólny mechanizm obsługi wyjątków. To powiedziawszy, oto opakowanie próbne oparte na lambda, którego można użyć z dowolną metodą. Zdecydowałem się wziąć pod uwagę liczbę ponownych prób i limit czasu ponownych prób jako parametry, aby uzyskać nieco większą elastyczność:

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);
    }
}

Możesz teraz użyć tej metody narzędzia do wykonania logiki ponownej próby:

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

lub:

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

lub:

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

Lub możesz nawet zrobić asyncprzeciążenie.


7
+1, szczególnie w przypadku ostrzeżeń i sprawdzania błędów. Byłbym jednak bardziej komfortowy, gdyby przekazał to jako typ wyjątku, który mógłby zostać przechwycony jako parametr ogólny (gdzie T: wyjątek).
TrueWill,

1
Moim zamiarem było, aby „ponawianie” w rzeczywistości oznaczało ponawianie. Ale nie jest zbyt trudno zmienić to na „próby”. Tak długo, jak nazwa ma znaczenie. Istnieją inne możliwości ulepszenia kodu, na przykład sprawdzanie negatywnych prób lub negatywnych limitów czasu - na przykład. Pominąłem je głównie, aby uprościć przykład ... ale znowu, w praktyce byłyby to prawdopodobnie dobre ulepszenia implementacji.
LBushkin

40
Używamy podobnego wzorca dla naszego dostępu do bazy danych w dużej aplikacji Biztalk, ale z dwoma ulepszeniami: Mamy czarne listy wyjątków, które nie powinny być ponawiane, i przechowujemy pierwszy występujący wyjątek i rzucamy go, gdy ponowna próba ostatecznie się nie powiedzie. Powodem jest to, że drugi i kolejne wyjątki często różnią się od pierwszego. W takim przypadku ukrywasz początkowy problem podczas ponownego zgłaszania tylko ostatniego wyjątku.
TToni

2
@Dexters Zgłaszamy nowy wyjątek z oryginalnym wyjątkiem jako wyjątek wewnętrzny. Oryginalny ślad stosu jest dostępny jako atrybut z wewnętrznych wyjątków.
TToni

7
Możesz także spróbować skorzystać z biblioteki typu open source, takiej jak Polly . Istnieje znacznie większa elastyczność w zakresie oczekiwania między kolejnymi próbami i zostało to potwierdzone przez wielu innych, którzy korzystali z projektu. Przykład: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen

220

Powinieneś spróbować Polly . Jest to napisana przeze mnie biblioteka .NET, która pozwala programistom płynnie wyrażać przejściowe zasady obsługi wyjątków, takie jak Ponów, Ponów próbę na zawsze, Czekaj i ponów próbę lub Wyłącznik.

Przykład

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

1
Czym właściwie jest delegat OnRetry? Zakładam, że musimy to zrobić, gdy wystąpi wyjątek. Kiedy wystąpi wyjątek, delegat OnRetry zadzwoni, a następnie Wykonaj delegata. Czy tak jest
user6395764

60

To prawdopodobnie zły pomysł. Po pierwsze, symbolizuje maksymę „definicja szaleństwa robi to samo dwa razy i za każdym razem oczekuje różnych rezultatów”. Po drugie, ten wzór kodowania nie komponuje się dobrze z samym sobą. Na przykład:

Załóżmy, że warstwa sprzętowa sieci ponownie wysyła pakiet trzy razy w przypadku awarii, czekając, powiedzmy, sekundę między awariami.

Załóżmy teraz, że warstwa oprogramowania trzykrotnie wysyła powiadomienie o awarii w przypadku awarii pakietu.

Załóżmy teraz, że warstwa powiadomień aktywuje powiadomienie trzykrotnie w przypadku niepowodzenia dostarczenia powiadomienia.

Załóżmy teraz, że warstwa zgłaszania błędów ponownie aktywuje warstwę powiadomienia trzykrotnie w przypadku niepowodzenia powiadomienia.

A teraz załóżmy, że serwer WWW reaktywuje raportowanie błędów trzykrotnie w przypadku niepowodzenia błędu.

A teraz załóżmy, że klient WWW trzykrotnie wysyła żądanie po otrzymaniu błędu z serwera.

Załóżmy teraz, że linia na przełączniku sieciowym, która ma kierować powiadomienie do administratora, jest odłączona. Kiedy użytkownik klienta WWW w końcu dostaje komunikat o błędzie? Robię to około 12 minut później.

Żeby nie pomyśleć, że to tylko głupi przykład: widzieliśmy ten błąd w kodzie klienta, choć daleko, znacznie gorzej, niż tutaj opisałem. W konkretnym kodzie klienta przerwa między wystąpieniem błędu a zgłoszeniem go użytkownikowi wynosiła kilka tygodni, ponieważ tak wiele warstw automatycznie ponawiało próbę oczekiwania. Wyobraź sobie, co by się stało, gdyby było dziesięć prób zamiast trzech .

Zwykle właściwą rzeczą w przypadku błędu jest zgłoszenie go natychmiast i pozwolenie użytkownikowi zdecydować, co należy zrobić. Jeśli użytkownik chce utworzyć zasadę automatycznych prób, pozwól mu utworzyć tę zasadę na odpowiednim poziomie w abstrakcji oprogramowania.


19
+1. Raymond udostępnia prawdziwy przykład z życia tutaj, blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspx
SolutionYogi

210
-1 Ta rada jest bezużyteczna w przypadku przejściowych awarii sieci napotykanych przez systemy automatycznego przetwarzania wsadowego.
nohat

15
Nie jestem pewien, czy to mówi „nie rób”, a następnie „rób to”. Większość osób zadających to pytanie to prawdopodobnie osoby zajmujące się abstrakcją oprogramowania.
Jim L,

44
W przypadku długotrwałych zadań wsadowych korzystających z zasobów sieciowych, takich jak usługi sieciowe, nie można oczekiwać, że sieć będzie w 100% niezawodna. Od czasu do czasu będą występować przerwy, rozłączanie gniazd, być może nawet fałszywe usterki routingu lub przerwy w działaniu serwera, które występują podczas korzystania z niego. Jedną z opcji jest niepowodzenie, ale może to oznaczać ponowne uruchomienie długiego zadania później. Inną opcją jest ponowienie próby kilka razy z odpowiednim opóźnieniem, aby sprawdzić, czy jest to tymczasowy problem, a następnie błąd. Zgadzam się co do kompozycji, o której musisz wiedzieć ... ale czasami jest to najlepszy wybór.
Erik Funkenbusch

18
Myślę, że cytat użyty na początku odpowiedzi jest interesujący. „Oczekiwanie różnych rezultatów” to szaleństwo tylko wtedy, gdy wcześniejsze doświadczenia regularnie dają te same wyniki. Chociaż oprogramowanie opiera się na obietnicy spójności, z pewnością istnieją okoliczności, w których jesteśmy zobowiązani do interakcji z niewiarygodnymi siłami poza naszą kontrolą.
Michael Richardson

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

Wtedy zadzwoniłbyś:

TryThreeTimes(DoSomething);

... lub alternatywnie ...

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

Bardziej elastyczna opcja:

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);
        }
   }
}

Do wykorzystania jako:

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

Bardziej nowoczesna wersja z obsługą asynchronizacji / czekania:

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);
        }
   }
}

Do wykorzystania jako:

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

2
najlepiej zmień opcję if na: --retryCount <= 0ponieważ będzie to trwało wiecznie, jeśli chcesz wyłączyć ponawianie prób, ustawiając ją na 0. Technicznie termin retryCountten nie jest zbyt dobrą nazwą, ponieważ nie spróbuje ponownie, jeśli ustawisz na 1. albo zmień nazwę to tryCountlub odłóż.
Stefanvds

2
@saille Zgadzam się. Jednak OP (i wszystkie inne odpowiedzi) korzystają Thread.Sleep. Alternatywą jest użycie liczników czasu, lub bardziej prawdopodobne, aby w dzisiejszych czasach użyć asyncponownie Task.Delay.
Drew Noakes

2
Dodałem wersję asynchroniczną.
Drew Noakes,

Tylko przełamać jeśli akcja returns true? Func<bool>
Kiquenet,

32

Obchodzenia Transient Fault Zastosowanie Blok zapewnia rozszerzalny zbiór strategii ponawiania w tym:

  • Przyrostowe
  • Naprawiono interwał
  • Wykładniczy zwrot

Zawiera także zbiór strategii wykrywania błędów dla usług w chmurze.

Więcej informacji znajduje się w tym rozdziale Przewodnika dla programistów.

Dostępne za pośrednictwem NuGet (wyszukaj „ topaz ”).


1
Ciekawy. Czy możesz używać tego poza Windows Azure, powiedzmy w aplikacji Winforms?
Matthew Lock

6
Absolutnie. Skorzystaj z podstawowego mechanizmu ponownych prób i podaj własne strategie wykrywania. Celowo je rozdzieliliśmy. Znajdź pakiet podstawowy nuget
Grigori Melnik

2
Ponadto projekt jest obecnie objęty programem Apache 2.0 i przyjmuje wkłady społeczności. aka.ms/entlibopen
Grigori Melnik

1
@Alex. Kawałki tego robią się na platformie.
Grigori Melnik

2
To jest teraz przestarzała, a ostatni raz użył go zawierał kilka błędów, które o ile wiem, nie były i nigdy nie będą stałe: github.com/MicrosoftArchive/... .
Ohad Schneider

15

Zezwalanie na funkcje i ponawianie wiadomości

public static T RetryMethod<T>(Func<T> method, int numRetries, int retryTimeout, Action onFailureAction)
{
 Guard.IsNotNull(method, "method");            
 T retval = default(T);
 do
 {
   try
   {
     retval = method();
     return retval;
   }
   catch
   {
     onFailureAction();
      if (numRetries <= 0) throw; // improved to avoid silent failure
      Thread.Sleep(retryTimeout);
   }
} while (numRetries-- > 0);
  return retval;
}

RetryMethod to retval to True lubmax retries?
Kiquenet

14

Możesz również rozważyć dodanie typu wyjątku, dla którego chcesz spróbować ponownie. Na przykład czy jest to wyjątek przekroczenia limitu czasu, który chcesz ponowić? Wyjątek bazy danych?

RetryForExcpetionType(DoSomething, typeof(TimeoutException), 5, 1000);

public static void RetryForExcpetionType(Action action, Type retryOnExceptionType, int numRetries, int retryTimeout)
{
    if (action == null)
        throw new ArgumentNullException("action");
    if (retryOnExceptionType == null)
        throw new ArgumentNullException("retryOnExceptionType");
    while (true)
    {
        try
        {
            action();
            return;
        }
        catch(Exception e)
        {
            if (--numRetries <= 0 || !retryOnExceptionType.IsAssignableFrom(e.GetType()))
                throw;

            if (retryTimeout > 0)
                System.Threading.Thread.Sleep(retryTimeout);
        }
    }
}

Możesz również zauważyć, że wszystkie inne przykłady mają podobny problem z testowaniem pod kątem ponownych prób == 0 i albo ponów próbę nieskończoności lub nie wygeneruj wyjątków, gdy otrzyma wartość ujemną. Również Sleep (-1000) zawiedzie w blokach catch powyżej. Zależy od tego, jak „głupie” oczekujesz od ludzi, ale programowanie obronne nigdy nie boli.


9
+1, ale dlaczego nie zrobić RetryForException <T> (...) gdzie T: Wyjątek, a następnie złapać (T e)? Właśnie wypróbowałem i działa idealnie.
TrueWill,

Zarówno tutaj, jak i tutaj, ponieważ nie muszę nic robić z typem, pod warunkiem, że wymyśliłem prosty stary parametr.
csharptest.net

@TrueWill najwyraźniej catch (T ex) ma kilka błędów zgodnie z tym postem stackoverflow.com/questions/1577760/…
csharptest.net

3
Aktualizacja: Właściwie lepsza implementacja, z której korzystałem, wymaga delegata Predicate <Exception>, który zwraca true, jeśli ponowna próba jest odpowiednia. Umożliwia to użycie natywnych kodów błędów lub innych właściwości wyjątku w celu ustalenia, czy ponowna próba ma zastosowanie. Na przykład kody HTTP 503.
csharptest.net

1
„Również Sleep (-1000) nie powiedzie się w blokach catch powyżej” ... użyj TimeSpan, a nie dostaniesz tego problemu. Plus TimeSpan jest znacznie bardziej elastyczny i samoopisujący. Z twojego podpisu „int retryTimeout” skąd mam wiedzieć, czy retryTimeout to MS, sekundy, minuty, lata? ;-)
bytedev

13

Jestem fanem metod rekurencyjnych i rozszerzających, więc oto moje dwa centy:

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

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

7

Opierając się na poprzedniej pracy, pomyślałem o ulepszeniu logiki ponownych prób na trzy sposoby:

  1. Określanie, jaki typ wyjątku ma zostać złapany / ponowiony. Jest to podstawowe ulepszenie, ponieważ ponawianie prób dla każdego wyjątku jest po prostu błędne.
  2. Nie zagnieżdżanie ostatniej próby w try / catch, osiągając nieco lepszą wydajność
  3. Czyniąc to Actionmetodą rozszerzenia

    static class ActionExtensions
    {
      public static void InvokeAndRetryOnException<T> (this Action action, int retries, TimeSpan retryDelay) where T : Exception
      {
        if (action == null)
          throw new ArgumentNullException("action");
    
        while( retries-- > 0 )
        {
          try
          {
            action( );
            return;
          }
          catch (T)
          {
            Thread.Sleep( retryDelay );
          }
        }
    
        action( );
      }
    }

Metodę można następnie wywołać w taki sposób (oczywiście można również użyć metod anonimowych):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );

1
To jest doskonałe. Ale osobiście nie nazwałbym tego „retryTimeout”, ponieważ tak naprawdę nie jest to limit czasu. Może „RetryDelay”?
Holf,

7

Uprość to dzięki C # 6.0

public async Task<T> Retry<T>(Func<T> action, TimeSpan retryInterval, int retryCount)
{
    try
    {
        return action();
    }
    catch when (retryCount != 0)
    {
        await Task.Delay(retryInterval);
        return await Retry(action, retryInterval, --retryCount);
    }
}

2
Jestem trochę ciekawy, czy to odrodziłoby szaloną liczbę wątków z wysoką liczbą powtórzeń i interwałem z powodu zwrócenia tej samej oczekiwanej metody?
HuntK24

6

Użyj Polly

https://github.com/App-vNext/Polly-Samples

Oto ogólna próba, której używam z Polly

public T Retry<T>(Func<T> action, int retryCount = 0)
{
    PolicyResult<T> policyResult = Policy
     .Handle<Exception>()
     .Retry(retryCount)
     .ExecuteAndCapture<T>(action);

    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}

Użyj tego w ten sposób

var result = Retry(() => MyFunction()), 3);

5

Wdrożono odpowiedź LBushkin w najnowszy sposób:

    public static async Task Do(Func<Task> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }

                await task();
                return;
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }

    public static async Task<T> Do<T>(Func<Task<T>> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }
                return await task();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }  

i użyć go:

await Retry.Do([TaskFunction], retryInterval, retryAttempts);

podczas gdy funkcja [TaskFunction]może być Task<T>albo tylko Task.


1
Dziękuję, Fabian! Należy to docenić aż do szczytu!
JamesHoux

1
@ MarkLauter krótka odpowiedź brzmi: tak. ;-)
Fabian Bigler,

4

Zaimplementowałbym to:

public static bool Retry(int maxRetries, Func<bool, bool> method)
{
    while (maxRetries > 0)
    {
        if (method(maxRetries == 1))
        {
            return true;
        }
        maxRetries--;
    }
    return false;        
}

Nie użyłbym wyjątków w sposób, w jaki zostały użyte w innych przykładach. Wydaje mi się, że jeśli spodziewamy się możliwości, że metoda się nie powiedzie, jej niepowodzenie nie jest wyjątkiem. Tak więc metoda, którą wywołuję, powinna zwrócić wartość true, jeśli się powiedzie, i false, jeśli się nie powiedzie.

Dlaczego to jest Func<bool, bool>a nie tylko Func<bool>? Jeśli więc chcę , aby metoda mogła zgłosić wyjątek w przypadku niepowodzenia, mam sposób poinformowania go, że jest to ostatnia próba.

Mogę więc użyć go z kodem takim jak:

Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       if (!succeeded && lastIteration)
       {
          throw new InvalidOperationException(...)
       }
       return succeeded;
   });

lub

if (!Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       return succeeded;
   }))
{
   Console.WriteLine("Well, that didn't work.");
}

Jeśli przekazanie parametru, którego nie używa metoda, okaże się niewygodne, wdrożenie przeciążenia Retrytego zajmuje tylko trywialność Func<bool>.


1
+1 za unikanie wyjątku. Chociaż zrobiłbym pustkę Spróbuj ponownie (...) i coś rzucić? Zwroty logiczne i / lub kody powrotu są zbyt często pomijane.
csharptest.net

1
„jeśli spodziewamy się, że metoda się nie powiedzie, jej niepowodzenie nie jest wyjątkiem” - choć w niektórych przypadkach tak jest, wyjątek nie musi oznaczać wyjątku. Służy do obsługi błędów. Nie ma gwarancji, że osoba dzwoniąca sprawdzi wynik logiczny. Jest to gwarancja, że wyjątek będą obsługiwane (przez program zamykania aplikacji, jeśli nic innego nie robi).
TrueWill,

Nie mogę znaleźć odniesienia, ale wierzę, że .NET definiuje wyjątek jako „metoda nie zrobiła tego, co powiedziała”. 1 celem jest użycie wyjątków do wskazania problemu zamiast wzorca Win32 wymagającego od osoby dzwoniącej sprawdzenia wartości zwracanej, jeśli funkcja się powiedzie, czy nie.
noctonura

Ale wyjątki nie tylko „wskazują na problem”. Obejmują one także masę informacji diagnostycznych, których skompilowanie kosztuje czas i pamięć. Oczywiście są sytuacje, w których nie ma to najmniejszego znaczenia. Ale jest tam wiele rzeczy. .NET nie używa wyjątków dla przepływu sterowania (porównaj, powiedzmy, z użyciem StopIterationwyjątku w Pythonie ), i jest powód.
Robert Rossney

TryDoSposób wzorzec jest nachylenie śliskie. Zanim się zorientujesz, cały stos wywołań będzie się składał z TryDometod. Wymyślono wyjątki, aby uniknąć takiego bałaganu.
HappyNomad


2

Dla tych, którzy chcą mieć zarówno opcję ponownej próby dowolnego wyjątku, jak i jawnego ustawienia typu wyjątku, użyj tego:

public class RetryManager 
{
    public void Do(Action action, 
                    TimeSpan interval, 
                    int retries = 3)
    {
        Try<object, Exception>(() => {
            action();
            return null;
        }, interval, retries);
    }

    public T Do<T>(Func<T> action, 
                    TimeSpan interval, 
                    int retries = 3)
    {
        return Try<T, Exception>(
              action
            , interval
            , retries);
    }

    public T Do<E, T>(Func<T> action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        return Try<T, E>(
              action
            , interval
            , retries);
    }

    public void Do<E>(Action action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        Try<object, E>(() => {
            action();
            return null;
        }, interval, retries);
    }

    private T Try<T, E>(Func<T> action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        var exceptions = new List<E>();

        for (int retry = 0; retry < retries; retry++)
        {
            try
            {
                if (retry > 0)
                    Thread.Sleep(interval);
                return action();
            }
            catch (E ex)
            {
                exceptions.Add(ex);
            }
        }

        throw new AggregateException(exceptions);
    }
}

2

Potrzebowałem metody, która obsługuje anulowanie, a gdy już tam byłem, dodałem wsparcie dla zwracania pośrednich awarii.

public static class ThreadUtils
{
    public static RetryResult Retry(
        Action target,
        CancellationToken cancellationToken,
        int timeout = 5000,
        int retries = 0)
    {
        CheckRetryParameters(timeout, retries)
        var failures = new List<Exception>();
        while(!cancellationToken.IsCancellationRequested)
        {
            try
            {
                target();
                return new RetryResult(failures);
            }
            catch (Exception ex)
            {
                failures.Add(ex);
            }

            if (retries > 0)
            {
                retries--;
                if (retries == 0)
                {
                    throw new AggregateException(
                     "Retry limit reached, see InnerExceptions for details.",
                     failures);
                }
            }

            if (cancellationToken.WaitHandle.WaitOne(timeout))
            {
                break;
            }
        }

        failures.Add(new OperationCancelledException(
            "The Retry Operation was cancelled."));
        throw new AggregateException("Retry was cancelled.", failures);
    }

    private static void CheckRetryParameters(int timeout, int retries)
    {
        if (timeout < 1)
        {
            throw new ArgumentOutOfRangeException(...
        }

        if (retries < 0)
        {
            throw new ArgumentOutOfRangeException(...

        }
    }

    public class RetryResult : IEnumerable<Exception>
    {
        private readonly IEnumerable<Exception> failureExceptions;
        private readonly int failureCount;

         protected internal RetryResult(
             ICollection<Exception> failureExceptions)
         {
             this.failureExceptions = failureExceptions;
             this.failureCount = failureExceptions.Count;
         }
    }

    public int FailureCount
    {
        get { return this.failureCount; }
    }

    public IEnumerator<Exception> GetEnumerator()
    {
        return this.failureExceptions.GetEnumerator();
    }

    System.Collections.IEnumerator 
        System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

Możesz użyć takiej Retryfunkcji, spróbuj 3 razy z 10-sekundowym opóźnieniem, ale bez anulowania.

try
{
    var result = ThreadUtils.Retry(
        SomeAction, 
        CancellationToken.None,
        10000,
        3);

    // it worked
    result.FailureCount // but failed this many times first.
}
catch (AggregationException ex)
{
   // oops, 3 retries wasn't enough.
}

Lub spróbuj ponownie wiecznie co pięć sekund, chyba że zostanie anulowane.

try
{
    var result = ThreadUtils.Retry(
        SomeAction, 
        someTokenSource.Token);

    // it worked
    result.FailureCount // but failed this many times first.
}
catch (AggregationException ex)
{
   // operation was cancelled before success.
}

Jak można się domyślić, w moim kodzie źródłowym przeciążyłem Retryfunkcję, aby obsługiwać różne typy delgate, których chcę używać.


2

Ta metoda umożliwia ponawianie niektórych typów wyjątków (inne natychmiast wyrzuca).

public static void DoRetry(
    List<Type> retryOnExceptionTypes,
    Action actionToTry,
    int retryCount = 5,
    int msWaitBeforeEachRety = 300)
{
    for (var i = 0; i < retryCount; ++i)
    {
        try
        {
            actionToTry();
            break;
        }
        catch (Exception ex)
        {
            // Retries exceeded
            // Throws on last iteration of loop
            if (i == retryCount - 1) throw;

            // Is type retryable?
            var exceptionType = ex.GetType();
            if (!retryOnExceptionTypes.Contains(exceptionType))
            {
                throw;
            }

            // Wait before retry
            Thread.Sleep(msWaitBeforeEachRety);
        }
    }
}
public static void DoRetry(
    Type retryOnExceptionType,
    Action actionToTry,
    int retryCount = 5,
    int msWaitBeforeEachRety = 300)
        => DoRetry(new List<Type> {retryOnExceptionType}, actionToTry, retryCount, msWaitBeforeEachRety);

Przykładowe użycie:

DoRetry(typeof(IOException), () => {
    using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
    {
        fs.Write(entryBytes, 0, entryBytes.Length);
    }
});

1

Aktualizacja po 6 latach: teraz uważam, że poniższe podejście jest dość złe. Aby stworzyć logikę ponownych prób, powinniśmy rozważyć użycie biblioteki takiej jak Polly.


Moja asyncimplementacja metody ponownej próby:

public static async Task<T> DoAsync<T>(Func<dynamic> action, TimeSpan retryInterval, int retryCount = 3)
    {
        var exceptions = new List<Exception>();

        for (int retry = 0; retry < retryCount; retry++)
        {
            try
            {
                return await action().ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }

            await Task.Delay(retryInterval).ConfigureAwait(false);
        }
        throw new AggregateException(exceptions);
    }

Kluczowe punkty: użyłem .ConfigureAwait(false);i Func<dynamic>zamiast tegoFunc<T>


To nie daje odpowiedzi na pytanie. Rozważ opublikowanie swojej odpowiedzi jako nowego pytania, korzystając z przycisku „Zadaj pytanie” u góry strony, a następnie opublikuj własną odpowiedź na pytanie, aby podzielić się tym, czego nauczyłeś się ze społecznością.
elixenide

Znacznie prostsze z C # 5.0 niż codereview.stackexchange.com/q/55983/54000, ale może należy wstrzyknąć CansellactionToken.
SerG,

Wystąpił problem z tą implementacją. Po ostatniej próbie, tuż przed poddaniem się, Task.Delayzostaje wezwany bez powodu.
HappyNomad

@HappyNomad to jest 6-letnia odpowiedź i teraz uważam, że jest to dość złe podejście do stworzenia logiki ponownej próby :)) dzięki za powiadomienie. Zaktualizuję swoją odpowiedź zgodnie z tym rozważaniem.
Cihan Uygun

0

A może zrobisz to trochę lepiej ...

int retries = 3;
while (retries > 0)
{
  if (DoSomething())
  {
    retries = 0;
  }
  else
  {
    retries--;
  }
}

Uważam, że generowanie wyjątków powinno być zasadniczo unikane jako mechanizm, chyba że przekazanie ich między granicami (np. Budowa biblioteki, z której mogą korzystać inni ludzie). Dlaczego po prostu nie DoSomething()zwróci polecenia, truejeśli się powiedzie false?

EDYCJA: I to może być zawarte w funkcji, jak sugerowali inni. Jedynym problemem jest to, że nie piszesz DoSomething()funkcji samodzielnie


7
„Uważam, że generowanie wyjątków powinno być unikane jako mechanizm, chyba że przełożenie ich między granicami” - całkowicie się nie zgadzam. Skąd wiesz, że dzwoniący sprawdził twój fałszywy (lub gorzej, zerowy) zwrot? DLACZEGO kod zawiódł? Fałsz nie mówi nic więcej. Co jeśli osoba dzwoniąca musi przekazać awarię do stosu? Przeczytaj msdn.microsoft.com/en-us/library/ms229014.aspx - te są dla bibliotek, ale mają taki sam sens dla kodu wewnętrznego. A w zespole inne osoby mogą wywołać Twój kod.
TrueWill,

0

Musiałem przekazać jakiś parametr do mojej metody, aby spróbować ponownie i uzyskać wartość wynikową; więc potrzebuję wyrażenia. Buduję tę klasę, która wykonuje pracę (jest inspirowana klasą LBushkina), możesz użyć jej w następujący sposób:

static void Main(string[] args)
{
    // one shot
    var res = Retry<string>.Do(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);

    // delayed execute
    var retry = new Retry<string>(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);
    var res2 = retry.Execute();
}

static void fix()
{
    Console.WriteLine("oh, no! Fix and retry!!!");
}

static string retryThis(string tryThis)
{
    Console.WriteLine("Let's try!!!");
    throw new Exception(tryThis);
}

public class Retry<TResult>
{
    Expression<Func<TResult>> _Method;
    int _NumRetries;
    TimeSpan _RetryTimeout;
    Action _OnFailureAction;

    public Retry(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        _Method = method;
        _NumRetries = numRetries;
        _OnFailureAction = onFailureAction;
        _RetryTimeout = retryTimeout;
    }

    public TResult Execute()
    {
        TResult result = default(TResult);
        while (_NumRetries > 0)
        {
            try
            {
                result = _Method.Compile()();
                break;
            }
            catch
            {
                _OnFailureAction();
                _NumRetries--;
                if (_NumRetries <= 0) throw; // improved to avoid silent failure
                Thread.Sleep(_RetryTimeout);
            }
        }
        return result;
    }

    public static TResult Do(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        var retry = new Retry<TResult>(method, numRetries, retryTimeout, onFailureAction);
        return retry.Execute();
    }
}

ps. rozwiązanie LBushkina wykonuje jeszcze jedną próbę = D


0

Do zaakceptowanej odpowiedzi dodałbym następujący kod

public static class Retry<TException> where TException : Exception //ability to pass the exception type
    {
        //same code as the accepted answer ....

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

            for (int retry = 0; retry < retryCount; retry++)
            {
                try
                {
                    return action();
                }
                catch (TException ex) //Usage of the exception type
                {
                    exceptions.Add(ex);
                    Thread.Sleep(retryInterval);
                }
            }

            throw new AggregateException(String.Format("Failed to excecute after {0} attempt(s)", retryCount), exceptions);
        }
    }

Zasadniczo powyższy kod sprawia, że Retryklasa jest ogólna, dzięki czemu można przekazać typ wyjątku, który chcesz złapać w celu ponownej próby.

Teraz użyj go prawie w ten sam sposób, ale określając typ wyjątku

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

Pętla for zawsze będzie wykonywana kilka razy (na podstawie twojego retryCount), nawet jeśli kod w pętli TRY CATCH został wykonany bez wyjątków. Sugerowałbym, aby ustawić retryCount równą retry var w pętli try, aby pętla for przestała nad nią przechodzić.
scre_www

@scre_www Uważam, że się mylisz. Jeśli actionnie rzuca, Dowraca w ten breaksposób od forpętli.
HappyNomad

W każdym razie występuje problem z tą implementacją. Po ostatniej próbie, tuż przed poddaniem się, Thread.Sleepzostaje wezwany bez powodu.
HappyNomad

0

Wiem, że ta odpowiedź jest bardzo stara, ale chciałem to skomentować, ponieważ napotkałem problemy podczas korzystania z nich, niezależnie od tego, co to jest z licznikami.

Myślę, że przez lata zdecydowałem się na lepsze podejście. Oznacza to użycie pewnego rodzaju agregacji zdarzeń, takich jak reaktywne rozszerzenia „Temat” lub tym podobne. Gdy próba się nie powiedzie, po prostu opublikujesz zdarzenie informujące, że próba się nie powiodła, i funkcja agregatora ponownie zaplanuje zdarzenie. Umożliwia to znacznie większą kontrolę nad ponowną próbą bez zanieczyszczania samego połączenia za pomocą wielu pętli ponownych prób, a co nie. Nie wiążesz też pojedynczej nici z wieloma snami.


0

Zrób to prosto w C #, Java lub innych językach:

  internal class ShouldRetryHandler {
    private static int RETRIES_MAX_NUMBER = 3;
    private static int numberTryes;

    public static bool shouldRetry() {
        var statusRetry = false;

        if (numberTryes< RETRIES_MAX_NUMBER) {
            numberTryes++;
            statusRetry = true;
            //log msg -> 'retry number' + numberTryes

        }

        else {
            statusRetry = false;
            //log msg -> 'reached retry number limit' 
        }

        return statusRetry;
    }
}

i użyj go w kodzie bardzo prosto:

 void simpleMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    //do some repetitive work
     }

    //some code    
    }

lub możesz użyć go w metodach rekurencyjnych:

void recursiveMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    recursiveMethod();
     }

    //some code    
    }

0
int retries = 3;
while (true)
{
    try
    {
        //Do Somthing
        break;
    }
    catch (Exception ex)
    {
        if (--retries == 0)
            return Request.BadRequest(ApiUtil.GenerateRequestResponse(false, "3 Times tried it failed do to : " + ex.Message, new JObject()));
        else
            System.Threading.Thread.Sleep(100);
    }

Co robisz z Request.BadRequest?
Danh,

0

Spróbuj ponownie pomocnika: ogólna implementacja Java, która zawiera zarówno zwroty zwrotne, jak i próby typu void.

import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RetryHelper {
  private static final Logger log = LoggerFactory.getLogger(RetryHelper.class);
  private int retryWaitInMS;
  private int maxRetries;

  public RetryHelper() {
    this.retryWaitInMS = 300;
    this.maxRetries = 3;
  }

  public RetryHelper(int maxRetry) {
    this.maxRetries = maxRetry;
    this.retryWaitInMS = 300;
  }

  public RetryHelper(int retryWaitInSeconds, int maxRetry) {
    this.retryWaitInMS = retryWaitInSeconds;
    this.maxRetries = maxRetry;
  }

  public <T> T retryAndReturn(Supplier<T> supplier) {
    try {
      return supplier.get();
    } catch (Exception var3) {
      return this.retrySupplier(supplier);
    }
  }

  public void retry(Runnable runnable) {
    try {
      runnable.run();
    } catch (Exception var3) {
      this.retrySupplier(() -> {
        runnable.run();
        return null;
      });
    }

  }

  private <T> T retrySupplier(Supplier<T> supplier) {
    log.error("Failed <TASK>, will be retried " + this.maxRetries + " times.");
    int retryCounter = 0;

    while(retryCounter < this.maxRetries) {
      try {
        return supplier.get();
      } catch (Exception var6) {
        ++retryCounter;
        log.error("<TASK> failed on retry: " + retryCounter + " of " + this.maxRetries + " with error: " + var6.getMessage());
        if (retryCounter >= this.maxRetries) {
          log.error("Max retries exceeded.");
          throw var6;
        }

        try {
          Thread.sleep((long)this.retryWaitInMS);
        } catch (InterruptedException var5) {
          var5.printStackTrace();
        }
      }
    }

    return supplier.get();
  }

  public int getRetryWaitInMS() {
    return this.retryWaitInMS;
  }

  public int getMaxRetries() {
    return this.maxRetries;
  }
}

Stosowanie:

    try {
      returnValue = new RetryHelper().retryAndReturn(() -> performSomeTask(args));
      //or no return type:
      new RetryHelper().retry(() -> mytask(args));
    } catch(Exception ex){
      log.error(e.getMessage());
      throw new CustomException();
    }

0

Oto wersja async/, awaitktóra agreguje wyjątki i obsługuje anulowanie.

/// <seealso href="https://docs.microsoft.com/en-us/azure/architecture/patterns/retry"/>
protected static async Task<T> DoWithRetry<T>( Func<Task<T>> action, CancellationToken cancelToken, int maxRetries = 3 )
{
    var exceptions = new List<Exception>();

    for ( int retries = 0; !cancelToken.IsCancellationRequested; retries++ )
        try {
            return await action().ConfigureAwait( false );
        } catch ( Exception ex ) {
            exceptions.Add( ex );

            if ( retries < maxRetries )
                await Task.Delay( 500, cancelToken ).ConfigureAwait( false ); //ease up a bit
            else
                throw new AggregateException( "Retry limit reached", exceptions );
        }

    exceptions.Add( new OperationCanceledException( cancelToken ) );
    throw new AggregateException( "Retry loop was canceled", exceptions );
}

-1
public delegate void ThingToTryDeletage();

public static void TryNTimes(ThingToTryDelegate, int N, int sleepTime)
{
   while(true)
   {
      try
      {
        ThingToTryDelegate();
      } catch {

            if( --N == 0) throw;
          else Thread.Sleep(time);          
      }
}

Ponieważ throw;jest to jedyny sposób, w jaki nieskończona pętla jest zakończona, ta metoda faktycznie implementuje „próbuj, aż nie powiedzie się N razy”, a nie pożądaną „próbę do Nczasów, aż się powiedzie”. Potrzebujesz połączenia break;lub return;po nim, w ThingToTryDelegate();przeciwnym razie będzie ono wywoływane w sposób ciągły, jeśli nigdy nie zawiedzie. Ponadto nie zostanie skompilowany, ponieważ pierwszy parametr TryNTimesnie ma nazwy. -1.
BACON

-1

Napisałem małą klasę na podstawie zamieszczonych tutaj odpowiedzi. Mam nadzieję, że to pomoże komuś: https://github.com/natenho/resiliency

using System;
using System.Threading;

/// <summary>
/// Classe utilitária para suporte a resiliência
/// </summary>
public sealed class Resiliency
{
    /// <summary>
    /// Define o valor padrão de número de tentativas
    /// </summary>
    public static int DefaultRetryCount { get; set; }

    /// <summary>
    /// Define o valor padrão (em segundos) de tempo de espera entre tentativas
    /// </summary>
    public static int DefaultRetryTimeout { get; set; }

    /// <summary>
    /// Inicia a parte estática da resiliência, com os valores padrões
    /// </summary>
    static Resiliency()
    {
        DefaultRetryCount = 3;
        DefaultRetryTimeout = 0;
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente DefaultRetryCount vezes  quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Não aguarda para realizar novas tentativa.</remarks>
    public static void Try(Action action)
    {
        Try<Exception>(action, DefaultRetryCount, TimeSpan.FromMilliseconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    public static void Try(Action action, int retryCount, TimeSpan retryTimeout)
    {
        Try<Exception>(action, retryCount, retryTimeout, null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    public static void Try(Action action, int retryCount, TimeSpan retryTimeout, Action<ResiliencyTryHandler<Exception>> tryHandler)
    {
        Try<Exception>(action, retryCount, retryTimeout, tryHandler);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente por até DefaultRetryCount vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try(Action action, Action<ResiliencyTryHandler<Exception>> tryHandler)
    {
        Try<Exception>(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="TException"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try<TException>(Action action) where TException : Exception
    {
        Try<TException>(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="TException"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount"></param>
    public static void Try<TException>(Action action, int retryCount) where TException : Exception
    {
        Try<TException>(action, retryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount"></param>
    /// <param name="retryTimeout"></param>
    public static void Try<TException>(Action action, int retryCount, TimeSpan retryTimeout) where TException : Exception
    {
        Try<TException>(action, retryCount, retryTimeout, null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try<TException>(Action action, Action<ResiliencyTryHandler<TException>> tryHandler) where TException : Exception
    {
        Try(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), tryHandler);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada uma <see cref="Exception"/> definida no tipo genérico
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Construído a partir de várias ideias no post <seealso cref="http://stackoverflow.com/questions/156DefaultRetryCount191/c-sharp-cleanest-way-to-write-retry-logic"/></remarks>
    public static void Try<TException>(Action action, int retryCount, TimeSpan retryTimeout, Action<ResiliencyTryHandler<TException>> tryHandler) where TException : Exception
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        while (retryCount-- > 0)
        {
            try
            {
                action();
                return;
            }
            catch (TException ex)
            {
                //Executa o manipulador de exception
                if (tryHandler != null)
                {
                    var callback = new ResiliencyTryHandler<TException>(ex, retryCount);
                    tryHandler(callback);
                    //A propriedade que aborta pode ser alterada pelo cliente
                    if (callback.AbortRetry)
                        throw;
                }

                //Aguarda o tempo especificado antes de tentar novamente
                Thread.Sleep(retryTimeout);
            }
        }

        //Na última tentativa, qualquer exception será lançada de volta ao chamador
        action();
    }

}

/// <summary>
/// Permite manipular o evento de cada tentativa da classe de <see cref="Resiliency"/>
/// </summary>
public class ResiliencyTryHandler<TException> where TException : Exception
{
    #region Properties

    /// <summary>
    /// Opção para abortar o ciclo de tentativas
    /// </summary>
    public bool AbortRetry { get; set; }

    /// <summary>
    /// <see cref="Exception"/> a ser tratada
    /// </summary>
    public TException Exception { get; private set; }

    /// <summary>
    /// Identifca o número da tentativa atual
    /// </summary>
    public int CurrentTry { get; private set; }

    #endregion

    #region Constructors

    /// <summary>
    /// Instancia um manipulador de tentativa. É utilizado internamente
    /// por <see cref="Resiliency"/> para permitir que o cliente altere o
    /// comportamento do ciclo de tentativas
    /// </summary>
    public ResiliencyTryHandler(TException exception, int currentTry)
    {
        Exception = exception;
        CurrentTry = currentTry;
    }

    #endregion

}

-1

Wdrożyłem asynchroniczną wersję zaakceptowanej odpowiedzi w ten sposób - i wydaje się, że działa dobrze - jakieś komentarze?


        public static async Task DoAsync(
            Action action,
            TimeSpan retryInterval,
            int maxAttemptCount = 3)
        {
            DoAsync<object>(() =>
            {
                action();
                return null;
            }, retryInterval, maxAttemptCount);
        }

        public static async Task<T> DoAsync<T>(
            Func<Task<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 await action();
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
            throw new AggregateException(exceptions);
        }

I nazwij to po prostu tak:

var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);

Thread.Sleep? Blokowanie wątku niweluje korzyści wynikające z asynchronii. Jestem też całkiem pewien, że Task DoAsync()wersja powinna zaakceptować argument typu Func<Task>.
Theodor Zoulias
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.