Polly重试策略在保持SQL事务打开状态时的应用

4
我正在使用Polly来实现对瞬态SQL错误的重试策略。问题在于我需要将我的db调用包装在一个事务中(因为如果有任何一个失败,我想回滚)。在实现Polly之前,这很容易,因为我只需捕获异常并回滚即可。但是,现在我正在使用下面的代码来实现Polly并进行几次重试。问题在于,当我有一个异常并且Polly执行重试时,假设重试不起作用并且所有尝试均失败,则事务会保持打开状态,并且我会收到错误消息,指出“无法在事务中开始事务”。我知道为什么会发生这种情况,因为.WaitAndRetry将在每次尝试之前执行块中的代码。这就是我现在进行回滚的地方。这适用于除最后一个尝试外的所有尝试。
问题是,当我有一个事务并且需要在每次失败后回滚时,如何实现Polly,以便即使在最后一次失败时它仍然被回滚?
以下是我目前所做的事情:
return Policy
    .Handle<SQLiteException>()
    .WaitAndRetry(retryCount: 2, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (exception, retryCount, context) =>
    {
        connection.Rollback();
         Logger.Instance.WriteLog<DataAccess>($"Retry {retryCount} of inserting employee files", LogLevel.Error, exception);
    })
    .Execute(() =>
    {
        connection.BeginTransaction();
        connection.Update(batch);
        connection.Insert(pkgs);
        if (pkgStatus != null)
            connection.Insert(pkgStatus);
        if (extended != null)
            connection.Insert(extended);
        connection.Commit();
        return true;
    });
2个回答

3

如您所述,WaitAndRetryonRetry 委托会在重试策略进入休眠状态之前运行。此委托通常用于捕获日志信息,而不是执行任何补偿操作。

如果您需要回滚,则有几个选项:

  • 回滚是要执行的委托的一部分
    • 已装饰有策略的方法
  • 通过使用 NoOp 策略和 ExecuteAndCapture 方法
  • 使用 Fallback 策略将成功和失败情况分开

让我通过一个简化的示例来展示后两种方法:

简化应用

private static bool isHealthy = true;
static void SampleCall()
{
    Console.WriteLine("SampleCall");
    isHealthy = false;
    throw new NotSupportedException();
}

static void Compensate()
{
    Console.WriteLine("Compensate");
    isHealthy = true;
}

简单来说:
我们有能够破坏健康状态的SampleCall,以及可以进行自我修复的CompensateNoOp+ExecuteAndCapture:
static void Main(string[] args)
{
    var retry = Policy<bool>
        .HandleResult(isSucceeded => !isSucceeded)
        .Retry(2);

    var noop = Policy.NoOp();

    bool isSuccess = retry.Execute(() =>
    {
        var result = noop.ExecuteAndCapture(SampleCall);
        if (result.Outcome != OutcomeType.Failure) 
            return true;
        
        Compensate();
        return false;

    });

    Console.WriteLine(isSuccess);
}
  • NoOp不会有特殊操作,只会执行提供的委托。
  • ExecuteAndCapture会执行提供的委托并返回PolicyResult对象,它包含几个有用的属性:OutcomeFinalExceptionExceptionTypeContext
    • 如果Outcome不是Failure(没有抛出异常),则会返回true,且不会触发重试策略。
    • 如果OutcomeFailure,则会执行Compensate操作,并返回false以触发重试策略。
  • HandleResult将检查返回的值并决定是否应该重新执行提供的委托。
  • isSuccess包含最终结果。
    • 如果SampleCall成功执行不超过3次(1次初始调用和2次重试),则可能为true
    • 或者如果3次全部执行失败,则可能为false

Fallback

static void Main(string[] args)
{
    var retry = Policy<bool>
        .HandleResult(isSucceeded => !isSucceeded)
        .Retry(2);

    var fallback = Policy<bool>
        .Handle<NotSupportedException>()
        .Fallback(() => { Compensate(); return false; });

    var strategy = Policy.Wrap(retry, fallback);

    bool isSuccess = strategy.Execute(() =>
    {
        SampleCall();
        return true;
    });

    Console.WriteLine(isSuccess);
}
  • 这里我们将成功和失败的情况分开处理。
    • 成功的情况下,从 Execute 委托中返回 true
    • 失败的情况下,Execute 将异常传递给 Fallback 策略,该策略执行 Compensate 操作,然后返回 false 以触发重试策略。
  • Policy.Wrap 通常用于定义升级链。如果内部失败并且无法处理给定的情况,则会调用外部。
    • 例如,如果抛出 NotImplementedException,则从回退的角度来看,这是一个未处理的异常,因此需要升级。
  • 在我们的例子中,我们使用它来进行自我修复,然后触发重试。

希望这两个简单的示例能帮助您决定实现目标的方式。


2

经过一些研究和测试,我提出了一个可能的解决方案。在功能上它能够工作,因此我将其作为答案提供,但我不知道是否有更好的方式来做到这一点,或者Polly内部是否支持更好的方式。

一种方法是将连接事务相关的内容放置在.Execute中,使用单独的try/catch包装起来,以便回滚可以在那里发生,然后重新抛出异常,以便Polly可以捕获并触发重试。

以下是我的做法:

return Policy
    .Handle<SQLiteException>()
    .WaitAndRetry(retryCount: 2, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (exception, retryCount, context) =>
    {
        //connection.Rollback();
        Logger.Instance.WriteLog<DataAccess>($"Retry {retryCount} of inserting employee files", LogLevel.Error, exception);
    })
    .Execute(() =>
    {
        try
        {
            connection.BeginTransaction();
            connection.Update(batch);
            connection.Insert(pkgs);
            if (pkgStatus != null)
                connection.Insert(pkgStatus);
            if (extended != null)
                connection.Insert(extended);
            connection.Commit();
            return true;
        }
        catch (SQLiteException sx)
        {
            connection.Rollback();
            throw;
        }
    });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接