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
'sonRetry
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:
NoOp
policy andExecuteAndCapture
methodFallback
policyLet me show you the last two via a simplified example:
Simplified application
To put it simply:
SampleCall
which can ruin the healthy stateCompensate
which can do self-healing.NoOp
+ExecuteAndCapture
NoOp
, as its name suggests, does not do anything special. It will execute the provided the delegate and that's it.ExecuteAndCapture
will execute the provided delegate and will return aPolicyResult
object, which has a couple of useful properties:Outcome
,FinalException
,ExceptionType
andContext
Outcome
is not aFailure
(so no exception has been thrown) then we will returntrue
and the retry policy will NOT be triggered.OutCome
is aFailure
then we will preform theCompensate
action and we will returnfalse
to trigger the retry policy.HandleResult
will examine the returned value and decides whether or not the provided delegate should be re-executed.isSuccess
contains the final result.true
if theSampleCall
succeeds with at most 3 executions (1 initial call and 2 retries).false
if the all 3 executions fail.Fallback
true
from theExecute
's delegate.Execute
will propagate the Exception to theFallback
policy which executes theCompensate
action and then returnfalse
to trigger the retry policy.Policy.Wrap
is normally used to define an escalation chain. If the inner fails and does not handle the given situation then it will call the outer.NotImplementedException
is 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.