[C#] 재시도 로직을 작성하는 가장 깨끗한 방법?

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

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

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

TryThreeTimes(DoSomething);

C #에서 가능합니까? 이 TryThreeTimes()방법 의 코드는 무엇입니까 ?



답변

일반적인 예외 처리 메커니즘으로 사용하는 경우 단순히 동일한 호출을 재 시도하는 담요 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과부하 를 일으킬 수도 있습니다.


답변

당신은 폴리 를 시도해야 . 개발자가 재시도, 재시도 재시도, 대기 및 재시도 또는 회로 차단기와 같은 일시적인 예외 처리 정책을 유창하게 표현할 수있는 저에 의해 작성된 .NET 라이브러리입니다.

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


답변

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

네트워크 하드웨어 계층이 실패 할 때 패킷을 세 번 다시 전송한다고 가정합시다 (예 : 실패 사이에 1 초).

이제 소프트웨어 계층이 패킷 실패시 3 회 실패에 대한 알림을 다시 보낸다고 가정합니다.

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

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

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

이제 웹 클라이언트가 서버에서 오류가 발생하면 요청을 세 번 다시 보낸다고 가정합니다.

이제 알림을 관리자에게 라우팅해야하는 네트워크 스위치의 회선이 분리되었다고 가정합니다. 웹 클라이언트 사용자는 언제 마지막으로 오류 메시지를 받습니까? 나는 약 12 ​​분 후에 그것을 만든다.

이것이 단지 어리석은 예라고 생각하지 않도록하십시오 : 우리는 여기에 설명 된 것보다 훨씬 나쁘지만 고객 코드 에서이 버그를 보았습니다. 특정 고객 코드에서 발생하는 오류 상태와 최종 사용자에게보고되는 간격은 몇 주가 지났으므로 많은 계층이 대기 상태에서 자동으로 다시 시도하기 때문입니다. 3 번이 아닌 10 번의 재 시도 가 있다면 어떻게 될지 상상해보십시오 .

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


답변

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


답변

과도 장애 관리 응용 프로그램 블록 을 포함하여 재시도 전략의 확장 모음을 제공합니다 :

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

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

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

NuGet을 통해 사용 가능합니다 ( ‘ topaz ‘ 검색 ).


답변

기능 허용 및 메시지 재시도

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 테스트와 비슷한 문제가 있으며 무한대 재시도 또는 음수 값이 주어지면 예외가 발생하지 않습니다. 또한 위의 catch 블록에서 Sleep (-1000)이 실패합니다. 사람들이 얼마나 ‘어리석은’것으로 예상 하느냐에 따라 방어 적 프로그래밍은 결코 아프지 않습니다.