Last active
March 12, 2019 17:35
-
-
Save ian-beer/4c83713b1346aba06e5e8453606fec47 to your computer and use it in GitHub Desktop.
async fault retry utility inspired from Microsoft docs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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