Найпростіший спосіб написати логіку спроби?


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

Виписки про вилов, які просто повторюють один і той же виклик, можуть бути небезпечними, якщо використовуватись як загальний механізм обробки винятків. Сказавши це, ось упаковка для повторної спроби на основі лямбда, яку ви можете використовувати з будь-яким методом. Я вибрав як коефіцієнт кількість повторень та час очікування на повторний час, як параметри для більшої гнучкості:

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: Виняток).
TrueWill

1
Це було моїм наміром, що "повторні спроби" насправді означали повторення. Але це не надто важко змінити, щоб це означало "намагається". Поки ім’я залишається значимим. Є й інші можливості вдосконалення коду, наприклад перевірка на наявність негативних спроб або негативних тайм-аутів - наприклад. Здебільшого я це опустив, щоб приклад був простим ... але знову ж таки, на практиці це, мабуть, буде хорошим вдосконаленням для впровадження.
Л.Бушкін

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

2
@Dexters Ми викидаємо новий виняток із початковим винятком як внутрішній виняток. Оригінальний слід стека доступний як атрибут внутрішніх винятків.
TToni

7
Ви також можете спробувати використати бібліотеку з відкритим кодом, наприклад, Поллі для цього. Існує набагато більше гнучкості для очікування між повторними спробами, і це було підтверджено багатьма іншими, хто використовував проект. Приклад: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen

221

Спробуйте спробувати Полі . Це написана мною бібліотека .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

60

Це, можливо, погана ідея. По-перше, це емблема максими: "визначення божевілля роблять одне і те ж саме двічі і щоразу очікуючи різних результатів". По-друге, ця схема кодування не відповідає собі. Наприклад:

Припустимо, ваш апаратний рівень мережі повторно відправляє пакет тричі під час відмови, чекаючи, скажімо, секунди між відмовами.

Тепер припустимо, що програмний рівень повторно надсилає сповіщення про помилку тричі щодо відмови пакету.

Тепер припустимо, що рівень сповіщення тричі активує сповіщення про помилку доставки сповіщень.

Тепер припустимо, що рівень повідомлення про помилки тричі активує рівень сповіщення про помилку сповіщення.

А тепер припустимо, що веб-сервер повторно активує повідомлення про помилку тричі про помилку.

А тепер припустимо, що веб-клієнт тричі повторно надсилає запит, отримуючи помилку від сервера.

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

Щоб ви не думали, що це лише дурний приклад: ми бачили цю помилку в коді клієнта, хоча і набагато гірше, ніж я описав тут. У конкретному коді клієнта розрив між станом помилки, що сталася, і тим, що нарешті повідомляється користувачеві, склав кілька тижнів, оскільки стільки шарів автоматично повторювались з очікуванням. Уявіть собі, що було б, якби було десять спроб замість трьох .

Зазвичай правильна ситуація з умовою помилки - це негайно повідомити про це і дати користувачеві вирішити, що робити. Якщо користувач хоче створити політику автоматичних повторних спроб, дозвольте їм створити цю політику на відповідному рівні в абстракції програмного забезпечення.


19
+1. Реймонд ділиться тут прикладом реального життя, blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspx
SolutionYogi

210
-1 Ця порада марна для тимчасових відмов мережі, з якими стикаються автоматизовані системи пакетної обробки.
nohat

15
Не впевнений, чи говорить це "Не робіть цього", а потім "зроби це". Більшість людей, які задають це питання, - це, мабуть, люди, які працюють у програмі абстракції.
Jim L

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

Більш сучасна версія з підтримкою асинхронізації / очікування:

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або поставити - позаду.
Стефанівд

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

2
Я додав версію async.
Дрю Ноакс

Тільки перерву , якщо дія returns true? Func<bool>
Кікенет

32

Блок застосувань перехідних несправностей забезпечує розширювану колекцію стратегій повторного використання, включаючи:

  • Поступовий
  • Фіксований інтервал
  • Експоненційний відступ

Він також включає колекцію стратегій виявлення помилок для хмарних служб.

Для отримання додаткової інформації див цей розділ Посібника для розробників.

Доступні з допомогою NuGet (пошук « Топаз »).


1
Цікаво. Чи можете ви використовувати це за межами Windows Azure, скажімо, у програмі Winforms?
Метью Лок

6
Абсолютно. Використовуйте основний механізм повторної спроби та вкажіть власні стратегії виявлення. Ми навмисно розв’язували їх. Знайдіть основний пакунок нута тут: nuget.org/packages/TransientFaultHandling.Core
Григорій Мельник

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?
Кікенет

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, або повторіть нескінченність, або не вдалося зібрати винятки, коли дано негативне значення. Також режим сну (-1000) вийде з ладу у блоках виловів, наведених вище. Залежить від того, наскільки дурними ви очікуєте, що люди будуть, але оборонне програмування ніколи не шкодить.


9
+1, але чому б не зробити RetryForException <T> (...), де T: Виняток, тоді ловить (T e)? Просто спробував, і він працює чудово.
TrueWill

Або тут, оскільки мені не потрібно нічого робити із типом, якщо я подумав, що звичайний старий параметр зробить свою справу.
csharptest.net

@TrueWill , по- видимому зловити (T ех) має деякі помилки в Відповідно до цього після stackoverflow.com/questions/1577760 / ...
csharptest.net

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

1
"Також режим сну (-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 для уникнення виключення. Хоча я б зробив порожню спробу (...) і кинув щось? Булові повернення та / або коди повернення занадто часто не помічаються.
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функцію для підтримки різних типів делегатів, які я хочу використовувати.


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 слід вводити.
СерГ

Існує проблема з цією реалізацією. Після остаточного спроби, безпосередньо перед відмовою, Task.Delayвикликається без причини.
HappyNomad

@HappyNomad це 6-річна відповідь, і тепер я вважаю, що це досить поганий підхід для створення логіки спроб :)) дякую за повідомлення. Відповідно до цього, я оновлю свою відповідь.
Cihan Uygun

0

А як щодо того, щоб зробити це трохи акуратніше….

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

Я вважаю, що метання винятків, як правило, слід уникати як механізм, якщо ви не переходите їх між межами (наприклад, створення бібліотеки, яку можуть використовувати інші люди). Чому б просто не DoSomething()повернути команду, trueякщо вона була успішною і в falseіншому випадку?

EDIT: І це може бути інкапсульовано всередині функції, як запропонували інші. Проблема полягає лише в тому, якщо ви не пишете 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 рівним повторюваному var у циклі спробу, тож цикл for перестане переходити через нього.
scre_www

@scre_www Я вважаю, що ти помилився. Якщо actionне кидає, то Doповертається, таким чином, breakподалі від forпетлі.
HappyNomad

У будь-якому випадку є проблема з цією реалізацією. Після остаточного спроби, безпосередньо перед відмовою, Thread.Sleepвикликається без причини.
HappyNomad

0

Я знаю, що ця відповідь дуже стара, але я просто хотів прокоментувати це, тому що я зіткнувся з проблемами, використовуючи ці, хоча, будь-які заяви з лічильниками.

З роками я налаштувався на кращий підхід, який я думаю. Тобто використовувати якесь агрегування подій, як-от реактивних розширень "Тема" тощо. Якщо спроба не вдається, ви просто публікуєте подію, в якій зазначається, що спроба не вдалася, і функція агрегатора повторно планує подію. Це дозволяє набагато більше контролю над повторним спробою, не забруднюючи сам виклик купою повторних циклів, а що ні. Ви також не зав'язуєте одну нитку з купою ниток спить.


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

-1

Я написав невеликий клас на основі відповідей, розміщених тут. Сподіваємось, це допоможе комусь: 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

Я реалізував асинхронну версію прийнятої відповіді так - і, здається, це добре працює - будь-які коментарі?


        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.