sql 持有事务打开的 Polly 重试策略

Polly retry policy with sql holding transaction open

我正在使用 Polly 为瞬态 SQL 错误实施重试策略。问题是我需要将我的数据库调用包装在一个事务中(因为如果任何一个失败,我想回滚)。在我从 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;
    });

通过一些研究和测试,这是我想出的一种可能的解决方案。在功能上它有效,所以我提供它作为答案,但我不知道是否有更好的方法或 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;
        }
    });

正如您所描述的,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可以破坏健康状态
  • 而且我们有Compensate可以自我修复

NoOp + 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 将执行提供的委托并将 return 一个 PolicyResult 对象,它有几个有用的属性:OutcomeFinalException , ExceptionTypeContext
    • 如果 Outcome 不是 Failure(因此没有抛出异常)那么我们将 return true 并且不会触发重试策略。
    • 如果 OutComeFailure 那么我们将执行 Compensate 操作并且我们将 return false 触发重试策略。
  • HandleResult 将检查 returned 值并决定是否应重新执行提供的委托。
  • 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);
}
  • 这里我们把成功和失败的案例分开了。
    • 如果成功,我们 return true 来自 Execute 的代表。
    • 如果失败,Execute 会将异常传播到执行 Compensate 操作的 Fallback 策略,然后 return false 触发重试策略。
  • Policy.Wrap 通常用于定义升级链。如果内部失败并且无法处理给定的情况,那么它将调用外部。
    • 例如,如果抛出 NotImplementedException,那么从 Fallback 的角度来看,这是一个未处理的异常,因此需要升级。
  • 在我们的例子中,我们使用它来执行自我修复,然后触发重试。

我希望这两个简单的例子能帮助您决定您更喜欢哪种方式来实现您的目标。