mirror of
https://github.com/Stone-Red-Code/CuteUtils.git
synced 2026-09-04 00:46:11 +02:00
Merge branch 'main' into develop
This commit is contained in:
@@ -2,8 +2,7 @@ name: Upload nuget package
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ "main" ]
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Upload nuget package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x' # SDK Version to use.
|
||||||
|
source-url: https://nuget.pkg.github.com/Stone-Red-Code/index.json
|
||||||
|
env:
|
||||||
|
NUGET_AUTH_TOKEN: ${{secrets.NUGET_TOKEN}}
|
||||||
|
- run: dotnet build --configuration Release src/CuteUtils
|
||||||
|
- name: Create the package
|
||||||
|
run: dotnet pack --configuration Release src/CuteUtils
|
||||||
|
- name: Publish the package to nuget.org
|
||||||
|
run: dotnet nuget push src/CuteUtils/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json
|
||||||
|
env:
|
||||||
|
NUGET_AUTH_TOKEN: ${{secrets.NUGET_TOKEN}}
|
||||||
@@ -1,2 +1,9 @@
|
|||||||
# CuteUtils
|
# CuteUtils
|
||||||
> A "cute" utility library for C#
|
> A "cute" utility library for C#
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
**Package Manager:** `Install-Package CuteUtils`\
|
||||||
|
**.NET CLI:** `dotnet add package CuteUtils`
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
**Reference:** https://github.com/Stone-Red-Code/CuteUtils/wiki/Reference
|
||||||
|
|||||||
@@ -10,10 +10,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using CuteUtils.Misc;
|
||||||
|
|
||||||
|
namespace CuteUtils.Tests.Misc;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class SafeRetryTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsyncT_SucceedsFirstTry()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
int result = await SafeRetry.RetryAsync(async () =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
await Task.Delay(10);
|
||||||
|
return 42;
|
||||||
|
});
|
||||||
|
Assert.AreEqual(1, callCount);
|
||||||
|
Assert.AreEqual(42, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsyncT_RetriesAndSucceeds()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
int result = await SafeRetry.RetryAsync(async () =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 3)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
return 99;
|
||||||
|
}, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
Assert.AreEqual(3, callCount);
|
||||||
|
Assert.AreEqual(99, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsyncT_ThrowsAfterMaxRetries()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
_ = await Assert.ThrowsExceptionAsync<InvalidOperationException>(async () =>
|
||||||
|
{
|
||||||
|
_ = await SafeRetry.RetryAsync<int>(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
throw new InvalidOperationException("fail");
|
||||||
|
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
});
|
||||||
|
Assert.AreEqual(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsync_SucceedsFirstTry()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
await SafeRetry.RetryAsync(async () =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
await Task.Delay(10);
|
||||||
|
});
|
||||||
|
Assert.AreEqual(1, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsync_RetriesAndSucceeds()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
await SafeRetry.RetryAsync(async () =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 2)
|
||||||
|
{
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
Assert.AreEqual(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsync_ThrowsAfterMaxRetries()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
_ = await Assert.ThrowsExceptionAsync<InvalidOperationException>(async () =>
|
||||||
|
{
|
||||||
|
await SafeRetry.RetryAsync(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
throw new Exception();
|
||||||
|
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
});
|
||||||
|
Assert.AreEqual(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RetryT_SucceedsFirstTry()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
int result = SafeRetry.Retry(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
return 7;
|
||||||
|
});
|
||||||
|
Assert.AreEqual(1, callCount);
|
||||||
|
Assert.AreEqual(7, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RetryT_RetriesAndSucceeds()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
int result = SafeRetry.Retry(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 4)
|
||||||
|
{
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 123;
|
||||||
|
}, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
Assert.AreEqual(4, callCount);
|
||||||
|
Assert.AreEqual(123, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RetryT_ThrowsAfterMaxRetries()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
_ = Assert.ThrowsException<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
_ = SafeRetry.Retry<int>(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
throw new Exception();
|
||||||
|
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
});
|
||||||
|
Assert.AreEqual(3, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Retry_SucceedsFirstTry()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
SafeRetry.Retry(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
});
|
||||||
|
Assert.AreEqual(1, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Retry_RetriesAndSucceeds()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
SafeRetry.Retry(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 2)
|
||||||
|
{
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
Assert.AreEqual(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Retry_ThrowsAfterMaxRetries()
|
||||||
|
{
|
||||||
|
int callCount = 0;
|
||||||
|
_ = Assert.ThrowsException<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
SafeRetry.Retry(() =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
throw new Exception();
|
||||||
|
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||||
|
});
|
||||||
|
Assert.AreEqual(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsyncT_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
_ = await Assert.ThrowsExceptionAsync<ArgumentNullException>(async () =>
|
||||||
|
{
|
||||||
|
_ = await SafeRetry.RetryAsync<int>(null!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RetryAsync_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
_ = await Assert.ThrowsExceptionAsync<ArgumentNullException>(async () =>
|
||||||
|
{
|
||||||
|
await SafeRetry.RetryAsync(null!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RetryT_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
_ = Assert.ThrowsException<ArgumentNullException>(() =>
|
||||||
|
{
|
||||||
|
_ = SafeRetry.Retry<int>(null!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Retry_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
_ = Assert.ThrowsException<ArgumentNullException>(() =>
|
||||||
|
{
|
||||||
|
SafeRetry.Retry(null!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using CuteUtils.Misc;
|
||||||
|
|
||||||
|
namespace CuteUtils.Tests.Misc;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class WaitTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public async Task UntilAsync_ConditionMet_CompletesSuccessfully()
|
||||||
|
{
|
||||||
|
bool flag = false;
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
flag = true;
|
||||||
|
});
|
||||||
|
await Wait.UntilAsync(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2));
|
||||||
|
Assert.IsTrue(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public async Task UntilAsync_ConditionNotMet_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
await Wait.UntilAsync(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Until_ConditionMet_CompletesSuccessfully()
|
||||||
|
{
|
||||||
|
bool flag = false;
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
flag = true;
|
||||||
|
});
|
||||||
|
Wait.Until(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2));
|
||||||
|
Assert.IsTrue(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public void Until_ConditionNotMet_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
Wait.Until(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task WaitForEventAsync_EventRaised_CompletesSuccessfully()
|
||||||
|
{
|
||||||
|
void subscribe(Action handler)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
handler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Wait.WaitForEventAsync(subscribe, TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public async Task WaitForEventAsync_EventNotRaised_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
void subscribe(Action handler) { }
|
||||||
|
await Wait.WaitForEventAsync(subscribe, TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task WaitForEventAsyncT_EventRaised_CompletesWithValue()
|
||||||
|
{
|
||||||
|
void subscribe(Action<int> handler)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
handler(42);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
int result = await Wait.WaitForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromSeconds(2));
|
||||||
|
Assert.AreEqual(42, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public async Task WaitForEventAsyncT_EventNotRaised_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
void subscribe(Action<int> handler) { }
|
||||||
|
_ = await Wait.WaitForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void WaitForEvent_EventRaised_CompletesSuccessfully()
|
||||||
|
{
|
||||||
|
void subscribe(Action handler)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
handler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Wait.WaitForEvent(subscribe, TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public void WaitForEvent_EventNotRaised_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
void subscribe(Action handler) { }
|
||||||
|
Wait.WaitForEvent(subscribe, TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void WaitForEventT_EventRaised_CompletesWithValue()
|
||||||
|
{
|
||||||
|
void subscribe(Action<string> handler)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
handler("hello");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
string result = Wait.WaitForEvent((Action<Action<string>>)subscribe, TimeSpan.FromSeconds(2));
|
||||||
|
Assert.AreEqual("hello", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TimeoutException))]
|
||||||
|
public void WaitForEventT_EventNotRaised_ThrowsTimeoutException()
|
||||||
|
{
|
||||||
|
void subscribe(Action<string> handler) { }
|
||||||
|
_ = Wait.WaitForEvent((Action<Action<string>>)subscribe, TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
-->
|
-->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserTempFolder>$([System.IO.Path]::GetTempPath())</UserTempFolder>
|
<UserTempFolder>$([System.IO.Path]::GetTempPath())</UserTempFolder>
|
||||||
<Version>1.0.0.0</Version>
|
<Version>1.1.0.0</Version>
|
||||||
<Description>A "cute" utility library for C#</Description>
|
<Description>A "cute" utility library for C#</Description>
|
||||||
<RepositoryUrl>https://github.com/Stone-Red-Code/CuteUtils</RepositoryUrl>
|
<RepositoryUrl>https://github.com/Stone-Red-Code/CuteUtils</RepositoryUrl>
|
||||||
<PackageTags>Utility, Helper</PackageTags>
|
<PackageTags>Utility, Helper</PackageTags>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
namespace CuteUtils.Misc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods for safely retrying actions and functions with optional delay and retry count.
|
||||||
|
/// </summary>
|
||||||
|
public static class SafeRetry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retries an asynchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the result returned by the function.</typeparam>
|
||||||
|
/// <param name="action">The asynchronous function to execute.</param>
|
||||||
|
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||||
|
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||||
|
/// <returns>The result of the function if successful.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||||
|
public static async Task<T> RetryAsync<T>(Func<Task<T>> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
delay ??= TimeSpan.FromSeconds(1);
|
||||||
|
Exception? lastException = null;
|
||||||
|
for (int i = 0; i < maxRetries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
await Task.Delay(delay.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retries an asynchronous action up to a specified number of times, with an optional delay between attempts.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The asynchronous action to execute.</param>
|
||||||
|
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||||
|
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||||
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||||
|
public static async Task RetryAsync(Func<Task> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
delay ??= TimeSpan.FromSeconds(1);
|
||||||
|
Exception? lastException = null;
|
||||||
|
for (int i = 0; i < maxRetries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
await Task.Delay(delay.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retries a synchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the result returned by the function.</typeparam>
|
||||||
|
/// <param name="action">The function to execute.</param>
|
||||||
|
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||||
|
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||||
|
/// <returns>The result of the function if successful.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||||
|
public static T Retry<T>(Func<T> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
delay ??= TimeSpan.FromSeconds(1);
|
||||||
|
Exception? lastException = null;
|
||||||
|
for (int i = 0; i < maxRetries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
System.Threading.Thread.Sleep(delay.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retries a synchronous action up to a specified number of times, with an optional delay between attempts.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The action to execute.</param>
|
||||||
|
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||||
|
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||||
|
public static void Retry(Action action, int maxRetries = 3, TimeSpan? delay = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
delay ??= TimeSpan.FromSeconds(1);
|
||||||
|
Exception? lastException = null;
|
||||||
|
for (int i = 0; i < maxRetries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
System.Threading.Thread.Sleep(delay.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
namespace CuteUtils.Misc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides utility methods for waiting on conditions or events with optional timeouts and intervals.
|
||||||
|
/// </summary>
|
||||||
|
public static class Wait
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously waits until the specified condition is met or the timeout is reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="condition">A function that returns true when the wait should end.</param>
|
||||||
|
/// <param name="interval">The interval to check the condition. Defaults to 100ms.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the condition is not met within the timeout.</exception>
|
||||||
|
public static async Task UntilAsync(Func<bool> condition, TimeSpan? interval = null, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
interval ??= TimeSpan.FromMilliseconds(100);
|
||||||
|
DateTime endTime = DateTime.UtcNow.Add(timeout.Value);
|
||||||
|
while (!condition() && DateTime.UtcNow < endTime)
|
||||||
|
{
|
||||||
|
await Task.Delay(interval.Value);
|
||||||
|
}
|
||||||
|
if (!condition())
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The condition was not met within the specified timeout.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronously waits until the specified condition is met or the timeout is reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="condition">A function that returns true when the wait should end.</param>
|
||||||
|
/// <param name="interval">The interval to check the condition. Defaults to 100ms.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the condition is not met within the timeout.</exception>
|
||||||
|
public static void Until(Func<bool> condition, TimeSpan? interval = null, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
interval ??= TimeSpan.FromMilliseconds(100);
|
||||||
|
DateTime endTime = DateTime.UtcNow.Add(timeout.Value);
|
||||||
|
while (!condition() && DateTime.UtcNow < endTime)
|
||||||
|
{
|
||||||
|
System.Threading.Thread.Sleep(interval.Value);
|
||||||
|
}
|
||||||
|
if (!condition())
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The condition was not met within the specified timeout.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously waits for an event to be raised or the timeout to be reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscribe">An action that subscribes a handler to the event.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||||
|
public static async Task WaitForEventAsync(Action<Action> subscribe, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
TaskCompletionSource tcs = new TaskCompletionSource();
|
||||||
|
void Handler()
|
||||||
|
{
|
||||||
|
_ = tcs.TrySetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(Handler);
|
||||||
|
|
||||||
|
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||||
|
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await tcs.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The event was not raised within the specified timeout.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously waits for an event to be raised or the timeout to be reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscribe">An action that subscribes a handler to the event.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <typeparam name="T">The type of the event argument.</typeparam>
|
||||||
|
/// <returns>A task that completes with the event argument when the event is raised.</returns>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||||
|
public static async Task<T> WaitForEventAsync<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
|
||||||
|
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||||
|
|
||||||
|
void Handler(T arg)
|
||||||
|
{
|
||||||
|
_ = tcs.TrySetResult(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(Handler);
|
||||||
|
|
||||||
|
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||||
|
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The event was not raised within the specified timeout.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronously waits for an event to be raised or the timeout to be reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscribe">An action that subscribes a handler to the event.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||||
|
public static void WaitForEvent(Action<Action> subscribe, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
TaskCompletionSource tcs = new TaskCompletionSource();
|
||||||
|
void Handler()
|
||||||
|
{
|
||||||
|
_ = tcs.TrySetResult();
|
||||||
|
}
|
||||||
|
subscribe(Handler);
|
||||||
|
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||||
|
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tcs.Task.Wait();
|
||||||
|
}
|
||||||
|
catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronously waits for an event to be raised or the timeout to be reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscribe">An action that subscribes a handler to the event.</param>
|
||||||
|
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||||
|
/// <typeparam name="T">The type of the event argument.</typeparam>
|
||||||
|
/// <returns>The event argument when the event is raised.</returns>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||||
|
public static T WaitForEvent<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
timeout ??= TimeSpan.MaxValue;
|
||||||
|
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||||
|
void Handler(T arg)
|
||||||
|
{
|
||||||
|
_ = tcs.TrySetResult(arg);
|
||||||
|
}
|
||||||
|
subscribe(Handler);
|
||||||
|
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||||
|
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return tcs.Task.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user