[BREAKING] Make `ArchiCacheable` support `CancellationToken`
JustArchi opened this issue · comments
Checklist
- I read and understood ASF's Contributing guidelines
- I also read Setting-up and FAQ, I don't need help, this is an enhancement idea
- My idea doesn't duplicate existing ASF functionality described on the wiki
- I believe that my idea falls into ASF's scope and should be offered as part of ASF built-in functionality
- My idea doesn't violate the Steam Subscriber Agreement
- My idea doesn't violate the Steam Online Conduct
- This is not ASF-ui suggestion
Enhancement purpose
ArchiCacheable
doesn't support CancellationToken
for now, making it impossible to cancel the task before given delay or signal.
Solution
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2022-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
namespace ArchiSteamFarmBackend.Helpers;
internal sealed class ArchiCacheable<T> : IDisposable {
private readonly TimeSpan CacheLifetime;
private readonly SemaphoreSlim InitSemaphore = new(1, 1);
private readonly Func<CancellationToken, Task<(bool Success, T? Result)>> ResolveFunction;
private bool IsInitialized => InitializedAt > DateTime.MinValue;
private bool IsPermanentCache => CacheLifetime == Timeout.InfiniteTimeSpan;
private bool IsRecent => IsPermanentCache || (DateTime.UtcNow.Subtract(InitializedAt) < CacheLifetime);
private DateTime InitializedAt;
private T? InitializedValue;
internal ArchiCacheable(Func<CancellationToken, Task<(bool Success, T? Result)>> resolveFunction, TimeSpan? cacheLifetime = null) {
ResolveFunction = resolveFunction ?? throw new ArgumentNullException(nameof(resolveFunction));
CacheLifetime = cacheLifetime ?? Timeout.InfiniteTimeSpan;
}
public void Dispose() => InitSemaphore.Dispose();
internal async Task<(bool Success, T? Result)> GetValue(EFallback fallback = EFallback.DefaultForType, CancellationToken cancellationToken = default) {
if (!Enum.IsDefined(typeof(EFallback), fallback)) {
throw new InvalidEnumArgumentException(nameof(fallback), (int) fallback, typeof(EFallback));
}
if (IsInitialized && IsRecent) {
return (true, InitializedValue);
}
try {
await InitSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
} catch (OperationCanceledException e) {
Program.ArchiLogger.LogGenericDebuggingException(e);
return ReturnFailedValueFor(fallback);
}
try {
if (IsInitialized && IsRecent) {
return (true, InitializedValue);
}
(bool success, T? result) = await ResolveFunction(cancellationToken).ConfigureAwait(false);
if (!success) {
return ReturnFailedValueFor(fallback, result);
}
InitializedValue = result;
InitializedAt = DateTime.UtcNow;
return (true, result);
} catch (OperationCanceledException e) {
Program.ArchiLogger.LogGenericDebuggingException(e);
return ReturnFailedValueFor(fallback);
} finally {
InitSemaphore.Release();
}
}
internal async Task Reset() {
if (!IsInitialized) {
return;
}
await InitSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!IsInitialized) {
return;
}
InitializedAt = DateTime.MinValue;
} finally {
InitSemaphore.Release();
}
}
private (bool Success, T? Result) ReturnFailedValueFor(EFallback fallback, T? result = default) {
if (!Enum.IsDefined(typeof(EFallback), fallback)) {
throw new InvalidEnumArgumentException(nameof(fallback), (int) fallback, typeof(EFallback));
}
return fallback switch {
EFallback.DefaultForType => (false, default(T?)),
EFallback.FailedNow => (false, result),
EFallback.SuccessPreviously => (false, InitializedValue),
_ => throw new InvalidOperationException(nameof(fallback))
};
}
}
Or equivalent.
Why currently available solutions are not sufficient?
Usually those calls are expensive, allow inheritors to implement their own cancellation if needed.
Can you help us with this enhancement idea?
Yes, I can code the solution myself and send a pull request
Additional info
Breaking change for all inheritors, don't ship before .NET 8 when we break more things.