【问题标题】:Polly retry policy with sql holding transaction openPolly 重试策略与 sql 保持事务打开
【发布时间】:2020-12-08 16:03:35
【问题描述】:

我正在使用 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;
    });

【问题讨论】:

    标签: c# sql .net polly


    【解决方案1】:

    正如您所描述的,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 将执行提供的委托并返回一个 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 会将异常传播到执行Compensate 操作的Fallback 策略,然后返回false 以触发重试策略。
    • Policy.Wrap 通常用于定义升级链。如果内部失败并且没有处理给定的情况,那么它将调用外部。
      • 例如,如果抛出 NotImplementedException,那么从 Fallback 的角度来看,这是一个未处理的异常,因此需要升级。
    • 在我们的例子中,我们使用它来执行自我修复,然后触发重试。

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

    【讨论】:

      【解决方案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;
              }
          });
      

      【讨论】:

        猜你喜欢
        • 2022-10-19
        • 1970-01-01
        • 1970-01-01
        • 2018-02-05
        • 1970-01-01
        • 2020-10-29
        • 1970-01-01
        • 1970-01-01
        • 2019-05-05
        相关资源
        最近更新 更多