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;
}
});
正如您所描述的,WaitAndRetry
的 onRetry
委托在重试策略进入睡眠状态之前运行。该委托通常用于捕获日志信息,而不是执行任何类型的补偿操作。
如果您需要回滚,那么您有几种选择:
- 回滚是你要执行委托的一部分
- 用策略修饰的方法
- 利用
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
对象,它有几个有用的属性:Outcome
、FinalException
, ExceptionType
和 Context
- 如果
Outcome
不是 Failure
(因此没有抛出异常)那么我们将 return true
并且不会触发重试策略。
- 如果
OutCome
是 Failure
那么我们将执行 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 的角度来看,这是一个未处理的异常,因此需要升级。
- 在我们的例子中,我们使用它来执行自我修复,然后触发重试。
我希望这两个简单的例子能帮助您决定您更喜欢哪种方式来实现您的目标。
我正在使用 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;
}
});
正如您所描述的,WaitAndRetry
的 onRetry
委托在重试策略进入睡眠状态之前运行。该委托通常用于捕获日志信息,而不是执行任何类型的补偿操作。
如果您需要回滚,那么您有几种选择:
- 回滚是你要执行委托的一部分
- 用策略修饰的方法
- 利用
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
对象,它有几个有用的属性:Outcome
、FinalException
,ExceptionType
和Context
- 如果
Outcome
不是Failure
(因此没有抛出异常)那么我们将 returntrue
并且不会触发重试策略。 - 如果
OutCome
是Failure
那么我们将执行Compensate
操作并且我们将 returnfalse
触发重试策略。
- 如果
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
策略,然后 returnfalse
触发重试策略。
- 如果成功,我们 return
-
Policy.Wrap
通常用于定义升级链。如果内部失败并且无法处理给定的情况,那么它将调用外部。- 例如,如果抛出
NotImplementedException
,那么从 Fallback 的角度来看,这是一个未处理的异常,因此需要升级。
- 例如,如果抛出
- 在我们的例子中,我们使用它来执行自我修复,然后触发重试。
我希望这两个简单的例子能帮助您决定您更喜欢哪种方式来实现您的目标。