Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAKING] Make ArchiCacheable support CancellationToken #3066

Closed
7 tasks done
JustArchi opened this issue Nov 11, 2023 · 0 comments
Closed
7 tasks done

[BREAKING] Make ArchiCacheable support CancellationToken #3066

JustArchi opened this issue Nov 11, 2023 · 0 comments
Labels
💣 Breaking changes Issues marked with this label indicate possible breaking changes that might need major version bump. ✨ Enhancement Issues marked with this label indicate further enhancements to the program, such as new features. ⛔ On hold Issues marked with this label are blocked from being worked on, very often due to third-parties. 🙏 Wishlist Issues marked with this label are wishlisted. We'd like to make them happen but they're not crucial.

Comments

@JustArchi
Copy link
Member

Checklist

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: [email protected]

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.

@JustArchi JustArchi added ✨ Enhancement Issues marked with this label indicate further enhancements to the program, such as new features. 👀 Evaluation Issues marked with this label are currently being evaluated if they're going to be considered. ⛔ On hold Issues marked with this label are blocked from being worked on, very often due to third-parties. 🙏 Wishlist Issues marked with this label are wishlisted. We'd like to make them happen but they're not crucial. 💣 Breaking changes Issues marked with this label indicate possible breaking changes that might need major version bump. and removed 👀 Evaluation Issues marked with this label are currently being evaluated if they're going to be considered. labels Nov 11, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
💣 Breaking changes Issues marked with this label indicate possible breaking changes that might need major version bump. ✨ Enhancement Issues marked with this label indicate further enhancements to the program, such as new features. ⛔ On hold Issues marked with this label are blocked from being worked on, very often due to third-parties. 🙏 Wishlist Issues marked with this label are wishlisted. We'd like to make them happen but they're not crucial.
Projects
None yet
Development

No branches or pull requests

1 participant