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