Skip to content

Instantly share code, notes, and snippets.

@ahmetb
Last active August 29, 2015 13:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahmetb/9539490 to your computer and use it in GitHub Desktop.
Save ahmetb/9539490 to your computer and use it in GitHub Desktop.
Retry logic modified for our testing needs, see comments at the end
using System;
using System.Diagnostics;
using System.Threading.Tasks;
public class RetryLoop<TResult>
{
public RetryLoop(Func<RetryIterationContext<TResult>, Task<TResult>> func, Func<RetryIterationContext<TResult>, bool> succeeded)
{
this.func = func;
this.timer = new Stopwatch();
this.BeforeRetry = r => Task.FromResult(false);
this.AfterRetry = r => Task.FromResult(false);
this.ShouldRetry = r => true;
this.Succeeded = succeeded;
}
/// <summary>
/// Optional. The function will be called between retries before making the retry.
/// Can be used to add timing policies between retries.
/// </summary>
public Func<RetryIterationContext<TResult>, Task> BeforeRetry { get; set; }
/// <summary>
/// Optional. The function will be called after a retry even though iteration has not
/// succeeded. Can be used to capture <see cref="RetryIterationContext{TResult}.Exception"/>
/// from the retryable function or <see cref="RetryIterationContext{TResult}.Value"/> produced
/// between runs and log various things about those.
/// </summary>
/// <remarks>
/// Exceptions thrown by this are not handled in the retry loop or served in
/// <see cref="RetryIterationContext{TResult}.Exception"/>. Handle separately.
/// </remarks>
public Func<RetryIterationContext<TResult>, Task> AfterRetry { get; set; }
/// <summary>
/// Optional. This function will be called to determine if function should be retried
/// again. Can be used to stop retrying after a condition is met (e.g. timeout).
/// If not specified, it will be indefinitely retried until <see cref="Succeeded"/>
/// returns <c>true</c>.
/// </summary>
public Func<RetryIterationContext<TResult>, bool> ShouldRetry { get; set; }
/// <summary>
/// This function will be called to determine if the retry iteration is
/// successful. If returns <c>true</c>, function will not be retried again.
/// </summary>
public Func<RetryIterationContext<TResult>, bool> Succeeded { get; private set; }
private readonly Func<RetryIterationContext<TResult>, Task<TResult>> func;
private readonly Stopwatch timer;
/// <returns>Context of last iteration that finished.</returns>
public async Task<RetryIterationContext<TResult>> ExecuteAsync()
{
RetryIterationContext<TResult> iterationContext;
bool shouldRetry = false;
TimeSpan startTime = this.timer.Elapsed;
int iteration = 0;
this.timer.Start();
do
{
iterationContext = new RetryIterationContext<TResult>(iteration);
try
{
if (shouldRetry)
{
await this.BeforeRetry(iterationContext);
}
iterationContext.Elapsed = this.timer.Elapsed - startTime;
iterationContext.StartDate = DateTime.UtcNow;
iterationContext.Value = await this.func(iterationContext);
}
catch (Exception e)
{
iterationContext.Exception = e;
}
iterationContext.Elapsed = this.timer.Elapsed - startTime;
iterationContext.Succeeded = this.Succeeded(iterationContext);
await this.AfterRetry(iterationContext);
shouldRetry = this.ShouldRetry(iterationContext);
iteration++;
} while (!iterationContext.Succeeded && shouldRetry);
return iterationContext;
}
}
public class RetryIterationContext<TResult>
{
public RetryIterationContext(int iteration)
{
this.Iteration = iteration;
}
/// <summary>
/// Iteration number of this instance.
/// </summary>
public int Iteration { get; set; }
/// <summary>
/// UTC time when retry has started just after <see cref="RetryLoop{TResult}.BeforeRetry"/> is
/// executed, if specified.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
/// UTC time when run has finished and <see cref="RetryLoop{TResult}.AfterRetry"/> has not
/// been executed yet, if specified.
/// </summary>
public DateTime FinishDate
{
get { return StartDate + Elapsed; }
}
/// <summary>
/// Time elapsed since retry loop has started.
/// </summary>
public TimeSpan Elapsed { get; set; }
/// <summary>
/// Success of the loop on this iteration.
/// </summary>
public bool Succeeded { get; set; }
/// <summary>
/// Stores exception, if any, thrown on this iteration
/// </summary>
public Exception Exception { get; set; }
/// <summary>
/// Value produced in this iteration, if any. Accessing the value
/// if the iteration is not succeeded may result in undefined behavior,
/// based on the function passed to the <see cref="RetryLoop{TResult}"/>.
/// </summary>
public TResult Value { get; set; }
}
@ahmetb
Copy link
Author

ahmetb commented Mar 13, 2014

Here is what changed from Brian's original code

  • Takes for functions that return a value. (Brian's was not returning value)
  • Succeeded func is mandatory (moved to constructor, will nullchk)
  • RetryContext is now per iteration (not common for the whole loop)
    • Contains current iteration number
    • Added Start & End Date in UTC (required for logging)
    • I will add how long the func took to execute (for logging) (Elapsed contains the time elapsed since first retry)
    • Contains Value obtained in that iteration (intermediate retry iterations can produce steps too, and we can use them in ShouldRetry).
  • Loop retries indefinitely by default (ShouldRetry is true if not overwritten)
  • Brian's code was wrapping in AggregateException, I am storing the original exc.
  • Made iteration number uint (was int) and starting from 0 (it was so in Brian's)

@ahmetb
Copy link
Author

ahmetb commented Mar 14, 2014

Fixes made based on Brian's suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment