programing

재시도 로직을 작성하는 가장 깨끗한 방법은 무엇입니까?

skycolor 2023. 5. 17. 22:40
반응형

재시도 로직을 작성하는 가장 깨끗한 방법은 무엇입니까?

때때로 작업을 중단하기 전에 여러 번 다시 시도해야 하는 경우가 있습니다.내 코드는 다음과 같습니다.

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

다음과 같은 일반 재시도 기능으로 다시 작성하고 싶습니다.

TryThreeTimes(DoSomething);

C#에서 가능합니까?다음을 위한 코드는 무엇입니까?TryThreeTimes()방법?

일반적인 예외 처리 메커니즘으로 사용되는 경우 동일한 호출을 다시 시도하는 포괄적인 캐치 문이 위험할 수 있습니다.그렇긴 하지만, 여기 어떤 방법으로든 사용할 수 있는 람다 기반 재시도 래퍼가 있습니다.재시도 횟수와 재시도 시간 초과를 매개 변수로 고려하여 유연성을 높였습니다.

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과중

폴리를 시도해보세요.개발자들이 Retry, Retry Forever, Wait and Retry 또는 Circuit Breaker와 같은 일시적 예외 처리 정책을 유창하게 표현할 수 있도록 해주는 제가 작성한 .NET 라이브러리입니다.

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, _ => TimeSpan.FromSeconds(3))
    .Execute(DoSomething);
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);

이것은 아마도 나쁜 생각일 것입니다.첫째, "광기의 정의는 같은 일을 두 번 하고 매번 다른 결과를 기대하는 것"이라는 격언을 상징합니다.둘째, 이 코딩 패턴은 그 자체로 잘 구성되지 않습니다.예:

네트워크 하드웨어 계층이 장애 발생 시 패킷을 세 번 재전송한다고 가정합니다. 예를 들어, 장애 발생 간격은 1초입니다.

이제 소프트웨어 계층이 패킷 장애 시 장애에 대한 알림을 세 번 다시 보낸다고 가정해 보겠습니다.

이제 알림 계층이 알림 배달 실패 시 알림을 세 번 다시 활성화한다고 가정합니다.

이제 오류 보고 계층이 알림 실패 시 알림 계층을 세 번 다시 활성화한다고 가정합니다.

이제 웹 서버가 오류 실패 시 오류 보고를 세 번 다시 활성화한다고 가정합니다.

이제 웹 클라이언트가 서버에서 오류를 수신할 때 요청을 세 번 재전송한다고 가정합니다.

이제 네트워크 스위치에서 관리자에게 통지를 라우트할 회선이 분리되었다고 가정합니다.웹 클라이언트의 사용자는 언제 오류 메시지를 받게 됩니까?저는 약 12분 후에 그것을 만듭니다.

이것이 단지 어리석은 예라고 생각하지 않도록 하십시오. 고객 코드에서 이 버그를 본 적이 있지만, 여기서 설명한 것보다 훨씬 더 심각합니다.특정 고객 코드에서는 매우 많은 계층이 대기를 통해 자동으로 다시 시도했기 때문에 오류 상태가 발생하고 사용자에게 최종 보고되는 간격이 몇 였습니다.세 번의 재시도가 아니라 열 번의 재시도가 있다면 어떤 일이 일어날지 상상해 보십시오.

일반적으로 오류 조건에서 올바른 작업은 즉시 보고하고 사용자가 수행할 작업을 결정하도록 하는 입니다.사용자가 자동 재시도 정책을 생성하려면 해당 정책을 소프트웨어 추상화의 적절한 수준에서 생성하도록 합니다.

일시적 결함 처리 애플리케이션 블록은 다음과 같은 확장 가능한 재시도 전략 모음을 제공합니다.

  • 증분
  • 고정 간격
  • 지수 백오프

또한 클라우드 기반 서비스에 대한 오류 감지 전략 모음도 포함되어 있습니다.

자세한 내용은 개발자 설명서의 이 장을 참조하십시오.

NuGet을 통해 사용할 수 있습니다('topaz' 검색).

저는 재귀와 확장 방법의 팬입니다. 그래서 여기 제 2센트가 있습니다.

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

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

기능 및 재시도 메시지 허용

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

다시 시도할 예외 유형을 추가할 수도 있습니다.예를 들어, 시간 초과 예외를 다시 시도하시겠습니까?데이터베이스 예외?

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)도 위의 캐치 블록에서 실패합니다.사람들이 얼마나 '바보적'이기를 기대하느냐에 따라 다르지만 방어적인 프로그래밍은 결코 해롭지 않습니다.

폴리 사용

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

다음은 Polly와 함께 사용하는 재시도 일반입니다.

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

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

    return policyResult.Result;
}

이렇게 사용합니다.

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

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

LBushkin의 답변을 최신 방식으로 구현했습니다.

    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. 탐지/재시도할 예외 유형을 지정합니다.예외를 다시 시도하는 것은 명백하게 잘못된 것이기 때문에 이것이 기본적인 향상입니다.
  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 ) );

저는 Polly를 사용하여 이 패턴을 두 가지 구현했습니다.하나는 비동기입니다.

나의 동기식 방법은 에릭 버그스테트의 이 대답을 기반으로 합니다.

public static T Retry<T>(Func<T> action, TimeSpan retryWait, int retryCount = 0)
{
    PolicyResult<T> policyResult = Policy
        .Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
        .WaitAndRetry(retryCount, retryAttempt => retryWait)
        .ExecuteAndCapture(action);

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

    return policyResult.Result;
}

비동기:

public static async Task<T> RetryAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
{
    PolicyResult<T> policyResult = await Policy
        .Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
        .ExecuteAndCaptureAsync(action);

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

    return policyResult.Result;
}

또한 예외 유형에 대한 람다뿐만 아니라 예외 유형도 쉽게 전달할 수 있습니다.

이를 구현할 수 있습니다.

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

다른 예에서 사용되는 방식으로 예외를 사용하지 않을 것입니다.제가 보기에 어떤 방법이 성공하지 못할 가능성을 예상한다면, 그 방법의 실패도 예외는 아닙니다.그래서 제가 호출하는 메소드는 성공하면 true를 반환하고 실패하면 false를 반환해야 합니다.

왜그요까럴?요▁it▁why▁a▁is까?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>뿐만 아니라.

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>

저는 취소를 지원하는 방법이 필요했는데, 제가 있는 동안 중간 실패를 반환하는 지원을 추가했습니다.

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이와 같이 작동합니다. 취소하지 않고 10초 지연으로 3회 재시도하십시오.

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

또는 취소하지 않는 한 5초마다 영구적으로 재시도합니다.

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사용하고자 하는 다양한 델타게이트 유형을 지원하는 기능.

이 메서드를 사용하면 특정 예외 유형에 대해 재시도할 수 있습니다(다른 예외 유형은 즉시 삭제).

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

지수 백오프는 단순히 x번 시도하는 것보다 좋은 재시도 전략입니다. Polly와 같은 라이브러리를 사용하여 구현할 수 있습니다.

예외에 대해 재시도하거나 예외 유형을 명시적으로 설정하는 두 가지 옵션을 모두 사용하려면 다음을 사용합니다.

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

여기 있습니다.async/await예외를 집계하고 취소를 지원하는 버전입니다.

/// <seealso href="https://learn.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 );
}

재시도하기 위해 매개 변수를 메서드에 전달하고 결과 값을 가져야 했기 때문에 표현식이 필요합니다.저는 이 작업을 수행하는 클래스를 구축합니다(LBushkin의 클래스에서 영감을 얻음). 다음과 같이 사용할 수 있습니다.

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

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

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

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

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

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

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

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

ps. LBushkin의 솔루션은 한 번 더 재시도합니다 = D

승인된 답변에 다음 코드를 추가하겠습니다.

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

는 기적으위코다만듭다니음을을 만드는 입니다.Retryretry.class generic에 할 수 .

이제 거의 동일한 방식으로 사용하되 예외 유형을 지정합니다.

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

저는 이 답변이 매우 오래된 것이라는 것을 알고 있지만 카운터가 있는 어떤 진술이든 사용하는 동안 문제가 발생했기 때문에 이에 대해 언급하고 싶습니다.

몇 년 동안 저는 더 나은 접근법을 결정했다고 생각합니다.즉, 반응형 확장 "Subject"와 같은 이벤트 집합을 사용하는 것입니다.시도가 실패하면 시도가 실패했다고 이벤트를 게시하고 집계 기능이 이벤트를 다시 예약하도록 합니다.이렇게 하면 여러 번의 재시도 루프 등으로 통화 자체를 오염시키지 않고 재시도를 훨씬 더 효과적으로 제어할 수 있습니다.한 가닥의 실잠을 한 다발로 묶는 것도 마찬가지입니다.

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

아니면 좀 더 깔끔하게 하는 게 어때요?

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

예외를 던지는 것은 경계 사이를 통과하지 않는 한 메커니즘으로 일반적으로 피해야 한다고 생각합니다(예: 다른 사람들이 사용할 수 있는 도서관 건설).왜 그냥 가지고 있지 않습니까?DoSomething() 반환 령명 환반true만약 그것이 성공적이었다면 그리고.false그렇지 않으면?

편집: 이것은 다른 사람들이 제안한 것처럼 함수 안에 캡슐화될 수 있습니다.유일한 문제는 당신이 글을 쓰고 있지 않다는 것입니다.DoSomething() 기능하기

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

저는 여기에 올라온 답변을 바탕으로 작은 수업을 작성했습니다.누군가에게 도움이 되기를 바랍니다: 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

}

재시도 도우미: 반환 가능한 재시도와 void 유형의 재시도를 모두 포함하는 일반 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();
    }

저는 그렇게 승인된 답변의 비동기 버전을 구현했습니다. 잘 작동하는 것 같습니다. 의견이 있으십니까?


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

언급URL : https://stackoverflow.com/questions/1563191/cleanest-way-to-write-retry-logic

반응형