// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
#pragma warning disable VSTHRD110 // Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method
namespace HordeServer.Utilities
{
///
/// Options for
///
public class LazyCacheOptions
{
///
/// Time after which a value will asynchronously be updated
///
public TimeSpan? RefreshTime { get; set; } = TimeSpan.FromMinutes(1.0);
///
/// Maximum age of any returned value. This will prevent a cached value being returned.
///
public TimeSpan? MaxAge { get; set; } = TimeSpan.FromMinutes(2.0);
}
///
/// Implements a cache which starts an asynchronous update of a value after a period of time.
///
/// Key for the cache
/// Value for the cache
public sealed class LazyCache : IDisposable where TKey : notnull
{
class Item
{
public Task? _currentTask;
public Stopwatch _timer = Stopwatch.StartNew();
public Task? _updateTask;
}
readonly ConcurrentDictionary _dictionary = new ConcurrentDictionary();
readonly Func> _getValueAsync;
readonly LazyCacheOptions _options;
///
/// Constructor
///
/// Function used to get a value
///
public LazyCache(Func> getValueAsync, LazyCacheOptions options)
{
_getValueAsync = getValueAsync;
_options = options;
}
///
public void Dispose()
{
foreach (Item item in _dictionary.Values)
{
item._currentTask?.Wait();
item._updateTask?.Wait();
}
}
///
/// Gets the value associated with a key
///
/// The key to query
/// Maximum age for values to return
///
public Task GetAsync(TKey key, TimeSpan? maxAge = null)
{
Item item = _dictionary.GetOrAdd(key, key => new Item());
// Create the task to get the current value
Task currentTask = InterlockedCreateTaskAsync(ref item._currentTask, () => _getValueAsync(key));
// If an update has completed, swap it out
Task? updateTask = item._updateTask;
if (updateTask != null && updateTask.IsCompleted)
{
Interlocked.CompareExchange(ref item._currentTask, updateTask, currentTask);
Interlocked.CompareExchange(ref item._updateTask, null, updateTask);
item._timer.Restart();
}
// Check if we need to update the value
TimeSpan age = item._timer.Elapsed;
if (maxAge != null && age > maxAge.Value)
{
return InterlockedCreateTaskAsync(ref item._updateTask, () => _getValueAsync(key));
}
if (age > _options.RefreshTime)
{
InterlockedCreateTaskAsync(ref item._updateTask, () => _getValueAsync(key));
}
return currentTask;
}
///
/// Creates a task, guaranteeing that only one task will be assigned to the given slot. Creates a cold task and only starts it once the variable is set.
///
///
///
///
static Task InterlockedCreateTaskAsync(ref Task? value, Func> createTask)
{
Task? currentTask = value;
while (currentTask == null)
{
Task> newTask = new Task>(createTask);
if (Interlocked.CompareExchange(ref value, newTask.Unwrap(), null) == null)
{
newTask.Start();
}
currentTask = value;
}
return currentTask;
}
}
}