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
NoOp
policy andExecuteAndCapture
method - The success and failure cases are separated by using the
Fallback
policy
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
SampleCall
which can ruin the healthy state - and we have the
Compensate
which 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
ExecuteAndCapture
will execute the provided delegate and will return aPolicyResult
object, which has a couple of useful properties:Outcome
,FinalException
,ExceptionType
andContext
- If the
Outcome
is not aFailure
(so no exception has been thrown) then we will returntrue
and the retry policy will NOT be triggered. - If the
OutCome
is aFailure
then we will preform theCompensate
action and we will returnfalse
to trigger the retry policy.
- If the
- The
HandleResult
will examine the returned value and decides whether or not the provided delegate should be re-executed. - The
isSuccess
contains the final result.- It might be
true
if theSampleCall
succeeds with at most 3 executions (1 initial call and 2 retries). - Or it might be
false
if 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
true
from theExecute
‘s delegate. - In case of failure the
Execute
will propagate the Exception to theFallback
policy which executes theCompensate
action and then returnfalse
to trigger the retry policy.
- In case of success we return
- The
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.- For example, if a
NotImplementedException
is 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.