Самый простой способ написать логику повторных попыток?


455

Иногда мне нужно повторить операцию несколько раз, прежде чем сдаться. Мой код похож на:

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

Я хотел бы переписать это в общей функции повтора, как:

TryThreeTimes(DoSomething);

Возможно ли это в C #? Какой будет код для TryThreeTimes()метода?


1
Простой цикл не достаточно? Почему бы просто не перебрать и выполнить логику несколько раз?
Рестута

13
Лично я был бы крайне осторожен с любым таким вспомогательным методом. Конечно, возможно реализовать с помощью лямбды, но сам шаблон чрезвычайно вонючий, поэтому введение помощника для него (что подразумевает, что он часто повторяется) само по себе очень подозрительно, и намекает на плохой общий дизайн.
Павел Минаев

12
В моем случае мои DoSomething () делают что-то на удаленных машинах, например, удаляют файлы или пытаются подключиться к сетевому порту. В обоих случаях возникают серьезные проблемы с синхронизацией, когда DoSomething будет успешным, и из-за удаленности нет событий, которые я мог бы прослушать. Так что да, вонючий. Предложения приветствуются.
noctonura

13
@PavelMinaev, почему повторные попытки намекают на плохой общий дизайн? Если вы пишете много кода, который соединяет точки интеграции, тогда повторные попытки - это определенно шаблон, который вы должны серьезно рассмотреть.
bytedev

Ответы:


569

Общие операторы catch, которые просто повторяют один и тот же вызов, могут быть опасными, если используются в качестве общего механизма обработки исключений. Сказав это, вот основанная на лямбде обертка для повторов, которую вы можете использовать с любым методом. Я выбрал в качестве параметров количество повторных попыток и время ожидания повторных попыток для большей гибкости:

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

Теперь вы можете использовать этот служебный метод для выполнения логики повторов:

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

или:

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

или:

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

Или вы могли бы даже сделать asyncперегрузку.


7
+1, особенно для предупреждения и проверки ошибок. Мне было бы удобнее, если бы это передавалось в типе исключения, чтобы поймать как универсальный параметр (где T: Exception), хотя.
TrueWill

1
Я хотел, чтобы «повторные попытки» действительно означали повторные попытки. Но это не так сложно изменить, чтобы означать «пытается». Пока имя остается значимым. Существуют и другие возможности для улучшения кода, например, проверка на наличие отрицательных повторных попыток или отрицательных тайм-аутов - например. Я пропустил это, в основном, для простоты примера ... но опять же, на практике это, вероятно, будет хорошим улучшением реализации.
Л.Бушкин

40
Мы используем аналогичный шаблон для доступа к БД в приложении Biztalk большого объема, но с двумя улучшениями: у нас есть черные списки исключений, которые не следует повторять, и мы сохраняем первое возникающее исключение и выкидываем его, когда повторная попытка в конечном итоге завершается неудачей. Причина в том, что второе и последующее исключения часто отличаются от первого. В этом случае вы скрываете исходную проблему при сбросе только последнего исключения.
TToni

2
@Dexters Мы выбрасываем новое исключение с исходным исключением как внутреннее исключение. Исходная трассировка стека доступна как атрибут из внутренних исключений.
TToni

7
Вы также можете попробовать использовать библиотеку с открытым исходным кодом, такую ​​как Polly, чтобы справиться с этим. Существует гораздо больше гибкости для ожидания между повторными попытками, и это было подтверждено многими другими, которые использовали проект. Пример: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Тодд Майнерсхаген

222

Ты должен попробовать Полли . Это библиотека .NET, написанная мной, которая позволяет разработчикам свободно выражать политики обработки временных исключений, такие как Retry, Retry Forever, Wait and Retry или Circuit Breaker.

пример

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

1
Что на самом деле является делегатом OnRetry? Я предполагаю, что это то, что нам нужно выполнить, когда происходит исключение. Поэтому, когда возникает исключение, делегат OnRetry вызывает и впоследствии выполняет делегат. Это так?
user6395764

61

Возможно, это плохая идея. Во-первых, это символизирует принцип «определение безумия - это делать одно и то же дважды и каждый раз ожидать разных результатов». Во-вторых, этот шаблон кодирования не соответствует самому себе. Например:

Предположим, что уровень сетевого оборудования повторно отправляет пакет три раза при сбое, ожидая, скажем, секунду между сбоями.

Теперь предположим, что программный уровень повторно отправляет уведомление о сбое три раза при сбое пакета.

Теперь предположим, что слой уведомлений повторно активирует уведомление три раза в случае сбоя доставки уведомления.

Теперь предположим, что уровень сообщения об ошибках повторно активирует уровень уведомления три раза в случае сбоя уведомления.

А теперь предположим, что веб-сервер повторно активирует отчеты об ошибках три раза в случае ошибки.

А теперь предположим, что веб-клиент повторно отправляет запрос три раза после получения ошибки от сервера.

Теперь предположим, что линия на сетевом коммутаторе, которая должна направлять уведомление администратору, отключена. Когда пользователь веб-клиента наконец получает сообщение об ошибке? Я делаю это примерно через двенадцать минут.

Чтобы вы не подумали, что это просто глупый пример: мы видели эту ошибку в клиентском коде, хотя и намного, намного хуже, чем я описал здесь. В конкретном коде клиента разрыв между случившейся ошибкой и окончательным сообщением пользователю составил несколько недель, потому что многие слои автоматически повторяли попытки с ожиданием. Только представьте, что произойдет, если будет десять попыток вместо трех .

Как правило, правильное решение проблемы заключается в том, чтобы немедленно сообщить о ней и позволить пользователю решить, что делать. Если пользователь хочет создать политику автоматических повторных попыток, позвольте ему создать эту политику на соответствующем уровне в абстракции программного обеспечения.


19
+1. Раймонд поделился
SolutionYogi

210
-1 Этот совет бесполезен для временных сбоев сети, возникающих в автоматизированных системах пакетной обработки.
nohat

15
Не уверен, если это говорит "Не делай этого", а затем "сделать это". Большинство людей, задающих этот вопрос, вероятно, люди, работающие в программной абстракции.
Джим Л

44
Если у вас есть длительные пакетные задания, которые используют сетевые ресурсы, такие как веб-службы, вы не можете ожидать, что сеть будет на 100% надежной. Будут случайные таймауты, разъединения сокетов, возможно даже ложные сбои маршрутизации или перебои в работе сервера, которые происходят во время его использования. Одним из вариантов является сбой, но это может означать возобновление длительной работы позже. Другой вариант - повторить попытку несколько раз с подходящей задержкой, чтобы увидеть, если это временная проблема, а затем потерпеть неудачу. Я согласен с композицией, о которой вы должны знать ... но иногда это лучший выбор.
Эрик Фанкенбуш

18
Я думаю, что цитата, которую вы использовали в начале вашего ответа, интересна. «Ожидание разных результатов» - это безумие, только если предыдущий опыт регулярно дает вам одни и те же результаты. Хотя программное обеспечение построено на обещании согласованности, существуют определенные обстоятельства, когда мы должны взаимодействовать с ненадежными силами вне нашего контроля.
Майкл Ричардсон

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

Тогда вы бы позвонили:

TryThreeTimes(DoSomething);

... или альтернативно ...

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

Более гибкий вариант:

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

Для использования в качестве:

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

Более современная версия с поддержкой 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);
        }
   }
}

Для использования в качестве:

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

2
предпочтительно измените if на: --retryCount <= 0потому что это будет продолжаться вечно, если вы хотите отключить повторные попытки, установив его на 0. Технически термин retryCountне является действительно хорошим именем, потому что он не будет повторяться, если вы установите его на 1. либо переименуйте это tryCountили поставить - позади.
Stefanvds

2
@Saille Я согласен. Однако ОП (и все другие ответы) используют Thread.Sleep. Альтернативой является использование таймеров, или более вероятно, в настоящее время использовать asyncдля повторения, с Task.Delay.
Дрю Ноакс

2
Я добавил асинхронную версию.
Дрю Ноакс

Только Разорватьесли действиеreturns true? Func<bool>
Kiquenet

32

Обработка переходных неисправностей Блок Применение предоставляет расширяемую коллекцию стратегий повторов в том числе:

  • дополнительный
  • Фиксированный интервал
  • Экспоненциальный откат

Он также включает в себя набор стратегий обнаружения ошибок для облачных сервисов.

Для получения дополнительной информации см. Эту главу Руководства разработчика.

Доступно через NuGet (поиск по ' топаз »).


1
Интересно. Можете ли вы использовать это вне Windows Azure, скажем, в приложении Winforms?
Мэтью Лок

6
Абсолютно. Используйте механизм повтора ядра и предоставьте свои собственные стратегии обнаружения. Мы намеренно развязали их. Найдите основной пакет nuget
Григорий Мельник,

2
Кроме того, проект сейчас находится под Apache 2.0 и принимает участие сообщества. aka.ms/entlibopen
Григорий Мельник

1
@Alex. Части этого делают это в платформу.
Григорий Мельник

2
Теперь это устарело, и в последний раз, когда я его использовал, в нем были некоторые ошибки, которые, насколько я знаю, не были и никогда не будут исправлены: github.com/MicrosoftArchive/… .
Охад Шнайдер

15

Разрешение для функций и повторных сообщений

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 к RETVAL истинны, илиmax retries?
Kiquenet

14

Вы также можете добавить тип исключения, для которого хотите повторить попытку. Например, это исключение тайм-аута, которое вы хотите повторить? Исключение базы данных?

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

Вы также можете заметить, что во всех других примерах есть похожая проблема с проверкой повторов == 0 и либо повторяется бесконечность, либо не выдается исключение, если задано отрицательное значение. Также Sleep (-1000) потерпит неудачу в блоках catch выше. Зависит от того, насколько «глупо» вы ожидаете от людей, но защитное программирование никогда не повредит.


9
+1, но почему бы не сделать RetryForException <T> (...) где T: Exception, а затем перехватить (T e)? Только что попробовал и работает отлично.
TrueWill

Либо или здесь, так как мне не нужно ничего делать с типом, при условии, что я полагал, что простой старый параметр поможет.
csharptest.net

@TrueWill, очевидно, catch (T ex) имеет некоторые ошибки, согласно этому сообщению stackoverflow.com/questions/1577760/…
csharptest.net

3
Обновление: фактически лучшая реализация, которую я использовал, использует делегат Predicate <Exception>, который возвращает true, если повторная попытка уместна. Это позволяет вам использовать собственные коды ошибок или другие свойства исключения, чтобы определить, применима ли повторная попытка. Например, коды HTTP 503.
csharptest.net

1
«Также Sleep (-1000) потерпит неудачу в вышеуказанных блоках перехвата» ... используйте TimeSpan, и у вас не возникнет этой проблемы. Плюс TimeSpan гораздо более гибкий и информативный. Из вашей подписи "int retryTimeout", как мне узнать, является ли retryTimeout MS, секундами, минутами, годами ?? ;-)
bytedev

13

Я фанат методов рекурсии и расширения, поэтому вот мои два цента:

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

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

7

Основываясь на предыдущей работе, я подумал об улучшении логики повторов тремя способами:

  1. Указание, какой тип исключения перехватить / повторить. Это основной шаг, так как повторная попытка любого исключения является просто неправильной.
  2. Не вкладывать последнюю попытку в попытку / поймать, достигнув немного лучшей производительности
  3. Делаем это Actionметодом расширения

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

Затем метод может быть вызван так (можно также использовать анонимные методы):

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

1
Это отлично. Но лично я бы не назвал это «retryTimeout», так как на самом деле это не Timeout. «RetryDelay», возможно?
Холф

7

Будьте проще с 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
Мне любопытно, будет ли это порождать безумное количество потоков с большим количеством повторов и интервалом из-за возврата того же ожидаемого метода?
HuntK24

6

Используйте Полли

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

Вот универсальный повтор, который я использую с Полли

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

Используйте это так

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

5

Реализовал ответ Л.Бушкина по последней моде:

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

и использовать это:

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

тогда как функция [TaskFunction]может быть Task<T>или просто Task.


1
Спасибо, Фабиан! Это должно быть одобрено до самого верха!
JamesHoux

1
@MarkLauter короткий ответ - да. ;-)
Фабиан Биглер

4

Я бы реализовал это:

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

Я бы не использовал исключения так, как они используются в других примерах. Мне кажется, что если мы ожидаем, что метод не будет успешным, его ошибка не станет исключением. Таким образом, метод, который я вызываю, должен вернуть true, если он был успешным, и false, если это не удалось.

Почему это, Func<bool, bool>а не просто Func<bool>? Так что, если я хочу, чтобы метод мог генерировать исключение при сбое, у меня есть способ сообщить ему, что это последняя попытка.

Так что я мог бы использовать его с кодом вроде:

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

или

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

Если передача параметра, который метод не использует, оказывается неуклюжей, реализовать триггер, Retryкоторый просто принимает , тоже тривиально Func<bool>.


1
+1 за избежание исключения. Хотя я бы сделал бесполезный Retry (...) и бросил бы что-нибудь? Булевы возвраты и / или коды возврата слишком часто игнорируются.
csharptest.net

1
«если мы ожидаем, что метод не будет успешным, его ошибка не является исключением» - хотя в некоторых случаях это правда, исключение не обязательно подразумевает исключение. Это для обработки ошибок. Нет гарантии, что вызывающая сторона проверит логический результат. Там является гарантией того, что исключение будет обработано (исполняющей выключая приложение , если ничего не делает).
TrueWill

Я не могу найти ссылку, но я считаю, что .NET определяет исключение как «метод не сделал того, что он сказал, что он будет делать». 1 цель состоит в том, чтобы использовать исключения для обозначения проблемы, а не шаблон Win32, требующий от вызывающей стороны проверить возвращаемое значение, если функция выполнена успешно или нет.
noctonura

Но исключения не просто «указывают на проблему». Они также включают в себя массу диагностической информации, которая требует времени и памяти для компиляции. Очевидно, что есть ситуации, в которых это не имеет большого значения. Но есть много, где это происходит. .NET не использует исключения для потока управления (сравните, скажем, с использованием StopIterationисключения Python ), и есть причина.
Роберт Росни

Схема TryDoметода - скользкий уклон. Прежде чем вы это узнаете, весь ваш стек вызовов будет состоять из TryDoметодов. Исключения были придуманы, чтобы избежать такого беспорядка.
HappyNomad


2

Для тех, кто хочет иметь возможность повторить попытку для любого исключения или явно установить тип исключения, используйте это:

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

Мне был нужен метод, который поддерживает отмену, в то время как я был на нем, я добавил поддержку для возврата промежуточных сбоев.

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

Вы можете использовать такую Retryфункцию, повторить 3 раза с 10-секундной задержкой, но без отмены.

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.
}

Или повторяйте вечно каждые пять секунд, если не отменено.

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.
}

Как вы можете догадаться, в моем исходном коде я перегружал Retryфункцию для поддержки различных типов delgate, которые я хочу использовать.


2

Этот метод позволяет повторять попытки для определенных типов исключений (другие сразу генерируются).

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

Пример использования:

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

1

Обновление через 6 лет: сейчас я считаю, что подход ниже довольно плохой. Чтобы создать логику повторных попыток, мы должны рассмотреть использование библиотеки, подобной Polly.


Моя asyncреализация метода повтора:

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

Ключевые моменты: я использовал .ConfigureAwait(false);и Func<dynamic>вместоFunc<T>


Это не дает ответа на вопрос. Подумайте о том, чтобы опубликовать свой ответ как новый вопрос, используя кнопку «Задать вопрос» в верхней части страницы, а затем опубликовать собственный ответ на вопрос, чтобы поделиться тем, что вы узнали, с сообществом.
Эликсинид

Гораздо проще с C # 5.0, чем codereview.stackexchange.com/q/55983/54000, но, возможно, CansellactionToken должен быть введен.
SerG

Есть проблема с этой реализацией. После последней попытки, прямо перед отказом, Task.Delayвызывается без причины.
HappyNomad

@HappyNomad - это 6-летний ответ, и теперь я считаю, что это довольно плохой подход к созданию логики повторов :)) спасибо за уведомление. Я обновлю свой ответ в соответствии с этим соображением.
Джихан Уйгун

0

Или как насчет того, чтобы сделать это немного аккуратнее ....

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

Я считаю, что исключения исключений, как правило, следует избегать как механизм, если только вы не передаете их между границами (например, создаете библиотеку, которую могут использовать другие люди). Почему бы просто не DoSomething()вернуть команду, trueесли она прошла успешно, и falseиначе?

РЕДАКТИРОВАТЬ: И это может быть заключено в функцию, как и другие предложили. Проблема только в том, что вы сами не пишете DoSomething()функцию


7
«Я считаю, что исключения исключений, как правило, следует избегать как механизм, если только вы не передаете их между границами», - я совершенно не согласен. Откуда вы знаете, что звонящий проверил ваш ложный (или, что еще хуже, нулевой) возврат? ПОЧЕМУ код не удался? Ложь говорит вам больше ничего. Что делать, если вызывающий должен передать ошибку в стек? Прочтите msdn.microsoft.com/en-us/library/ms229014.aspx - это для библиотек, но они имеют такой же смысл для внутреннего кода. И в команде другие люди, вероятно, будут называть ваш код.
TrueWill

0

Мне нужно было передать некоторый параметр в мой метод, чтобы повторить попытку и получить значение результата; поэтому мне нужно выражение .. Я создаю этот класс, который выполняет работу (он вдохновлен классом Л.Бушкина), вы можете использовать его следующим образом:

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

пс. решение Л.Бушкина делает еще одну попытку = D


0

Я бы добавил следующий код к принятому ответу

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

По сути, приведенный выше код делает Retryкласс универсальным, чтобы вы могли передать тип исключения, которое вы хотите перехватить, для повторной попытки.

Теперь используйте его почти таким же образом, но указав тип исключения.

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

Цикл for всегда будет выполняться пару раз (на основе вашего retryCount), даже если код в цикле TRY CATCH был выполнен без исключений. Я бы посоветовал установить значение retryCount равным переменной retry в цикле try, поэтому цикл for перестанет проходить через него.
scre_www

@scre_www Я думаю, ты ошибаешься. Если actionне выбрасывает, то Doвозвращается, таким образом, в breakсторону от forцикла.
HappyNomad

В любом случае, есть проблема с этой реализацией. После последней попытки, прямо перед отказом, Thread.Sleepвызывается без причины.
HappyNomad

0

Я знаю, что этот ответ очень старый, но я просто хотел прокомментировать это, потому что я столкнулся с проблемами, используя эти операторы while, do, независимо от выражения со счетчиками.

С годами я остановился на лучшем подходе, как мне кажется. То есть использовать какой-либо тип агрегации событий, например, реактивные расширения «Тема» или тому подобное. Если попытка не удалась, вы просто публикуете событие, в котором говорится, что попытка не удалась, и функция агрегатора перепланирует событие. Это позволяет вам гораздо больше контролировать повторные попытки, не загрязняя сам вызов кучей повторных попыток, а что нет. И при этом вы не связываете одну нить с кучей спящих нитей.


0

Сделайте это просто на C #, Java или других языках:

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

и использовать его в своем коде очень просто:

 void simpleMethod(){
    //some code

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

    //some code    
    }

или вы можете использовать его в рекурсивных методах:

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

Что ты делаешь с Request.BadRequest?
Дан

0

Помощник по повторным попыткам: универсальная реализация Java, которая содержит как возвратные, так и повторные попытки типа.

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

Применение:

    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

Вот async/ awaitверсия, которая объединяет исключения и поддерживает отмену.

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

Поскольку throw;это единственный способ, которым бесконечный цикл завершается, этот метод фактически реализует «попытаться до тех пор, пока он не потерпит неудачу N раз», а не желаемое «попытаться до Nтех пор, пока не удастся». Вам нужен вызов break;или return;после вызова, ThingToTryDelegate();иначе он будет вызываться непрерывно, если он никогда не выходит из строя. Кроме того, это не скомпилируется, потому что первый параметр TryNTimesне имеет имени. -1.
Бекон

-1

Я написал небольшой класс на основе ответов, размещенных здесь. Надеюсь, это кому-нибудь поможет: https://github.com/natenho/resilidity

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

Я реализовал асинхронную версию принятого ответа примерно так - и, кажется, хорошо работает - какие-либо комментарии?


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

И назовите это просто так:

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

Thread.Sleep? Блокировка потока сводит на нет преимущества асинхронности. Также я уверен, что Task DoAsync()версия должна принимать аргумент типа Func<Task>.
Теодор Зулиас
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.