I am using Polly to implement a retry policy for transient SQL errors. The issue is I need to wrap my db calls up in a transaction (because if any one fails, I want to rollback). This was easy before I implemented retry from Polly because I would just catch the exception and rollback. However, I am now using the code below to implement Polly and retry a few times. The issue is, when I have an exception and Polly does the retry and let’s say the retry doesn’t work and it fails all of the attempts, the transaction is held open and I get errors that say “Cannot begin a transaction while in a transaction”. I know why this is happening, it is because the .WaitAndRetry will execute the code in the block BEFORE each attempt. This is where I have my rollback now. This works for all attempts except for the last one.
The question is, how do I implement Polly when I have a transaction and need to rollback after each failure so that even on the last failure, it is still rolled back?
Here is what I am doing now:
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;
});
Advertisement
Answer
As you have described the WaitAndRetry‘s onRetry delegate runs before the retry policy goes to sleep. That delegate is normally used to capture log information, not to perform any sort of compensating action.
If you need to rollback then you have several options:
- The rollback is part of your to be executed delegate
- the method which is decorated with the policy
- By making use of
NoOppolicy andExecuteAndCapturemethod - The success and failure cases are separated by using the
Fallbackpolicy
Let me show you the last two via a simplified example:
Simplified application
private static bool isHealthy = true;
static void SampleCall()
{
Console.WriteLine("SampleCall");
isHealthy = false;
throw new NotSupportedException();
}
static void Compensate()
{
Console.WriteLine("Compensate");
isHealthy = true;
}
To put it simply:
- we have the
SampleCallwhich can ruin the healthy state - and we have the
Compensatewhich can do self-healing.
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);
}
- The
NoOp, as its name suggests, does not do anything special. It will execute the provided the delegate and that’s it. - The
ExecuteAndCapturewill execute the provided delegate and will return aPolicyResultobject, which has a couple of useful properties:Outcome,FinalException,ExceptionTypeandContext- If the
Outcomeis not aFailure(so no exception has been thrown) then we will returntrueand the retry policy will NOT be triggered. - If the
OutComeis aFailurethen we will preform theCompensateaction and we will returnfalseto trigger the retry policy.
- If the
- The
HandleResultwill examine the returned value and decides whether or not the provided delegate should be re-executed. - The
isSuccesscontains the final result.- It might be
trueif theSampleCallsucceeds with at most 3 executions (1 initial call and 2 retries). - Or it might be
falseif the all 3 executions fail.
- It might be
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);
}
- Here we have separated the success and failure cases.
- In case of success we return
truefrom theExecute‘s delegate. - In case of failure the
Executewill propagate the Exception to theFallbackpolicy which executes theCompensateaction and then returnfalseto trigger the retry policy.
- In case of success we return
- The
Policy.Wrapis normally used to define an escalation chain. If the inner fails and does not handle the given situation then it will call the outer.- For example, if a
NotImplementedExceptionis thrown then from the Fallback’s perspective this is an unhandled exception so escalation is needed.
- For example, if a
- In our case we use it to perform the self-healing and then trigger the retry.
I hope these two simple examples help you to decided which way you prefer to achieve your goal.