Skip to content

Instantly share code, notes, and snippets.

@ian-beer
Last active March 12, 2019 17:35
Show Gist options
  • Save ian-beer/4c83713b1346aba06e5e8453606fec47 to your computer and use it in GitHub Desktop.
Save ian-beer/4c83713b1346aba06e5e8453606fec47 to your computer and use it in GitHub Desktop.
async fault retry utility inspired from Microsoft docs
/*
useage:
var updateResult = await FaultRetryUtility.AsyncFaultRetry(() => _aManager.UpdateSomthing(Id, data), _logger);
will have to figure out your own log interface
*/
using System;
using System.Configuration;
using System.Threading.Tasks;
namespace Utilities.Utilities
{
/// <summary>
/// Utility that contains logic for retrying functions in case of an exception
/// </summary>
public static class FaultRetryUtility
{
private static readonly int NumberOfRetryAttempts = Convert.ToInt32(ConfigurationManager.AppSettings.Get("FaultRetry:NumberOfRetryAttempts"));
private static readonly double DelaySeed = Convert.ToDouble(ConfigurationManager.AppSettings.Get("FaultRetry:DelaySeed"));
/// <summary>
/// this takes in a function of type: () => [non-async MethodToCall] of any type but void return, and will retry a number of times if an exception is thrown.
/// NOTE: for this to work properly the whole call stack above this call needs to be async method calls
/// todo: needs better transient exception logic
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="func">() => MethodToCall</param>
/// <param name="logger">logger to use to log error caught</param>
/// <returns>result of function passed in</returns>
public async static Task<TResult> FaultRetry<TResult>(Func<TResult> func, ILogger logger)
{
int currentRetry = 0;
for (;;)
{
try
{
return await Task.Factory.StartNew(func);
}
catch (Exception ex)
{
logger?.Log(ex);
currentRetry++;
if (currentRetry > NumberOfRetryAttempts /*|| !IsTransient(ex)*/)
{
throw;
}
}
var delay = (int)(500 * (Math.Pow(DelaySeed, currentRetry)));
await Task.Delay(delay);
}
}
/// <summary>
/// this takes in a function of type: () => [async MethodToCall] of any type but void return, and will retry a number of times if an exception is thrown.
/// NOTE: for this to work properly the whole call stack needs to be async method calls
/// todo: needs better transient exception logic
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="func"></param>
/// <param name="logger"></param>
/// <returns></returns>
public async static Task<TResult> AsyncFaultRetry<TResult>(Func<Task<TResult>> func, ILogger logger)
{
int currentRetry = 0;
for (;;)
{
try
{
return await func();
}
catch (Exception ex)
{
logger?.Log(ex);
currentRetry++;
if (currentRetry > NumberOfRetryAttempts /*|| !IsTransient(ex)*/)
{
throw;
}
}
var delay = (int)(500 * (Math.Pow(DelaySeed, currentRetry)));
await Task.Delay(delay);
}
}
private static bool IsTransient(Exception exception)
{
/* todo
Check if the exception thrown was a transient exception
based on the logic in the error detection strategy.
Determine whether to retry the operation,
*/
return false;
}
}
}
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Tests.Utilities
{
/// <summary>
/// Summary description for FaultRetryUtility
/// </summary>
[TestClass]
public class FaultRetryUtilityTests
{
/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext { get; set; }
#region Additional test attributes
//
// You can use the following additional attributes as you write your tests:
//
// Use ClassInitialize to run code before running the first test in the class
// [ClassInitialize()]
// public static void MyClassInitialize(TestContext testContext) { }
//
// Use ClassCleanup to run code after all tests in a class have run
// [ClassCleanup()]
// public static void MyClassCleanup() { }
//
// Use TestInitialize to run code before running each test
// [TestInitialize()]
// public void MyTestInitialize() { }
//
// Use TestCleanup to run code after each test has run
// [TestCleanup()]
// public void MyTestCleanup() { }
//
#endregion
#region FaultRetry
[TestMethod]
public async Task FaultRetryUtility_FaultRetry_ShouldRunMethod_ReturnInt()
{
//Arrange
var expectedResult = 10;
//Act
var resultTask = await FaultRetryUtility.FaultRetry(() => WorkerTestMethod(20), null);
//Assert
Assert.AreEqual(expectedResult, resultTask);
}
[TestMethod]
public async Task FaultRetryUtility_FaultRetry_ShouldRunMethod_ReturnString()
{
//Arrange
const string a = "Hi";
const string b = "There";
const string expectedResult = "HiThere";
//Act
var resultTask = await FaultRetryUtility.FaultRetry(() => WorkerTestMethod(a, b), null);
//Assert
Assert.AreEqual(expectedResult, resultTask);
}
[TestMethod]
public async Task FaultRetryUtility_FaultRetry_ShouldRunMethod_ReturnObject()
{
//Arrange
var obj = new TestObject();
var a = "Hi";
var b = "There";
//Act
var resultTask = await FaultRetryUtility.FaultRetry(() => WorkerTestMethod(obj, a, b), null);
//Assert
Assert.IsNotNull(resultTask);
Assert.AreEqual(a, resultTask.A);
Assert.AreEqual(b, resultTask.B);
}
[TestMethod]
[ExpectedException(typeof(Exception))]
public async Task FaultRetryUtility_FaultRetry_ShouldThrowError_AfterMaxAttempts()
{
//Arrange
var loggerMoq = new Mock<ILogger>();
loggerMoq.Setup(m => m.Log(It.IsAny<Exception>(), null)).Verifiable();
//Act
try
{
await FaultRetryUtility.FaultRetry(WorkerTestMethod, loggerMoq.Object);
}
catch (Exception)
{
loggerMoq.Verify(m => m.Log(It.IsAny<Exception>(), null), Times.Exactly(6));
throw;
}
//Assert
//should throw
}
#endregion
#region AsyncFaultRetry
[TestMethod]
public async Task FaultRetryUtility_AsyncFaultRetry_ShouldRunMethod_ReturnInt()
{
//Arrange
var expectedResult = 10;
//Act
var resultTask = await FaultRetryUtility.AsyncFaultRetry(() => AsyncWorkerTestMethod(20), null);
//Assert
Assert.AreEqual(expectedResult, resultTask);
}
[TestMethod]
public async Task FaultRetryUtility_AsyncFaultRetry_ShouldRunMethod_ReturnString()
{
//Arrange
var a = "Hi";
var b = "There";
var expectedResult = "HiThere";
//Act
var resultTask = await FaultRetryUtility.AsyncFaultRetry(() => AsyncWorkerTestMethod(a, b), null);
//Assert
Assert.AreEqual(expectedResult, resultTask);
}
[TestMethod]
public async Task FaultRetryUtility_AsyncFaultRetry_ShouldRunMethod_ReturnObject()
{
//Arrange
var obj = new TestObject();
var a = "Hi";
var b = "There";
//Act
var resultTask = await FaultRetryUtility.AsyncFaultRetry(() => AsyncWorkerTestMethod(obj, a, b), null);
//Assert
Assert.IsNotNull(resultTask);
Assert.AreEqual(a, resultTask.A);
Assert.AreEqual(b, resultTask.B);
}
[TestMethod]
[ExpectedException(typeof(Exception))]
public async Task FaultRetryUtility_AsyncFaultRetry_ShouldThrowError_AfterMaxAttempts()
{
//Arrange
var loggerMoq = new Mock<ILogger>();
loggerMoq.Setup(m => m.Log(It.IsAny<Exception>(), null)).Verifiable();
//Act
try
{
await FaultRetryUtility.AsyncFaultRetry(AsyncWorkerTestMethod, loggerMoq.Object);
}
catch (Exception)
{
loggerMoq.Verify(m => m.Log(It.IsAny<Exception>(), null), Times.Exactly(6));
throw;
}
//Assert
//should throw
}
#endregion
#region Private Methods
private int WorkerTestMethod(int x)
{
return x / 2;
}
private async Task<int> AsyncWorkerTestMethod(int x)
{
await Task.Delay(1);
return await Task.Factory.StartNew( () => x / 2);
}
private string WorkerTestMethod(string a, string b)
{
return a + b;
}
private async Task<string> AsyncWorkerTestMethod(string a, string b)
{
await Task.Delay(1);
return await Task.Factory.StartNew(() => a + b);
}
private TestObject WorkerTestMethod(TestObject obj, string a, string b)
{
obj.A = a;
obj.B = b;
return obj;
}
private async Task<TestObject> AsyncWorkerTestMethod(TestObject obj, string a, string b)
{
await Task.Delay(1);
return await Task.Factory.StartNew(() =>
{
obj.A = a;
obj.B = b;
return obj;
});
}
private object WorkerTestMethod()
{
throw new Exception("looser!!");
}
private async Task<object> AsyncWorkerTestMethod()
{
await Task.Delay(1);
return await Task.Factory.StartNew(() =>
{
throw new Exception("looser!!");
#pragma warning disable 162
return new object();//do not delete need for type discovery
#pragma warning restore 162
});
}
private class TestObject
{
public string A { get; set; }
public string B { get; set; }
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment