// 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));
}
}
}