// Copyright Epic Games, Inc. All Rights Reserved. using System.Diagnostics; using EpicGames.Core; namespace HordeServer.Utilities { /// /// Caches a value and asynchronously updates it after a period of time /// /// public sealed class AsyncCachedValue : IAsyncDisposable { class State { public readonly T Value; readonly Stopwatch _timer; public Task? _next; public TimeSpan Elapsed => _timer.Elapsed; public State(T value) { Value = value; _timer = Stopwatch.StartNew(); } } CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); /// /// The current state /// Task? _current = null; /// /// Generator for the new value /// readonly Func> _generator; /// /// Time at which to start to refresh the value /// readonly TimeSpan _minRefreshTime; /// /// Time at which to wait for the value to refresh /// readonly TimeSpan _maxRefreshTime; /// /// Default constructor /// public AsyncCachedValue(Func> generator, TimeSpan refreshTime) : this(generator, refreshTime * 0.75, refreshTime) { } /// /// Default constructor /// public AsyncCachedValue(Func> generator, TimeSpan minRefreshTime, TimeSpan maxRefreshTime) { _generator = generator; _minRefreshTime = minRefreshTime; _maxRefreshTime = maxRefreshTime; } /// public async ValueTask DisposeAsync() { if (_current != null) { await _cancellationTokenSource.CancelAsync(); await _current.IgnoreCanceledExceptionsAsync(); _current = null; } if (_cancellationTokenSource != null) { _cancellationTokenSource.Dispose(); _cancellationTokenSource = null!; } } /// /// Invalidates the current value /// public void Invalidate() { _current = null; } /// /// Tries to get the current value /// /// Cancellation token for the request /// The cached value, if valid public Task GetAsync(CancellationToken cancellationToken = default) { return GetAsync(_maxRefreshTime, cancellationToken); } /// /// Tries to get the current value /// /// The cached value, if valid public Task GetAsync(TimeSpan maxAge, CancellationToken cancellationToken = default) { Task task = GetInternalAsync(maxAge); if (cancellationToken.CanBeCanceled) { // The returned task object is shared, so we don't want to cancel computation of it; just the wait for a result. task = WrapCancellationAsync(task, cancellationToken); } return task; } static async Task WrapCancellationAsync(Task task, CancellationToken cancellationToken) { await Task.WhenAny(task, Task.Delay(-1, cancellationToken)); cancellationToken.ThrowIfCancellationRequested(); return await task; } async Task GetInternalAsync(TimeSpan maxAge) { Task stateTask = CreateOrGetStateTaskAsync(ref _current); State state = await stateTask; if (state._next != null && state._next.IsCompleted) { _ = Interlocked.CompareExchange(ref _current, state._next, stateTask); state = await state._next; } if (state.Elapsed > maxAge) { state = await CreateOrGetStateTaskAsync(ref state._next); } if (state.Elapsed > _minRefreshTime) { _ = CreateOrGetStateTaskAsync(ref state._next); } return state.Value; } Task CreateOrGetStateTaskAsync(ref Task? stateTask) { for (; ; ) { Task? currentStateTask = stateTask; if (currentStateTask != null) { return currentStateTask; } Task> innerNewStateTask = new Task>(() => CreateStateAsync()); if (Interlocked.CompareExchange(ref stateTask, innerNewStateTask.Unwrap(), null) == null) { innerNewStateTask.Start(); } } } async Task CreateStateAsync() { return new State(await _generator(_cancellationTokenSource.Token)); } } }