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;
});
As you have described the
WaitAndRetry'sonRetrydelegate 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:
NoOppolicy andExecuteAndCapturemethodFallbackpolicyLet me show you the last two via a simplified example:
Simplified application
To put it simply:
SampleCallwhich can ruin the healthy stateCompensatewhich can do self-healing.NoOp+ExecuteAndCaptureNoOp, as its name suggests, does not do anything special. It will execute the provided the delegate and that's it.ExecuteAndCapturewill execute the provided delegate and will return aPolicyResultobject, which has a couple of useful properties:Outcome,FinalException,ExceptionTypeandContextOutcomeis not aFailure(so no exception has been thrown) then we will returntrueand the retry policy will NOT be triggered.OutComeis aFailurethen we will preform theCompensateaction and we will returnfalseto trigger the retry policy.HandleResultwill examine the returned value and decides whether or not the provided delegate should be re-executed.isSuccesscontains the final result.trueif theSampleCallsucceeds with at most 3 executions (1 initial call and 2 retries).falseif the all 3 executions fail.Fallbacktruefrom theExecute's delegate.Executewill propagate the Exception to theFallbackpolicy which executes theCompensateaction and then returnfalseto trigger the retry policy.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.NotImplementedExceptionis thrown then from the Fallback's perspective this is an unhandled exception so escalation is needed.I hope these two simple examples help you to decided which way you prefer to achieve your goal.