forked from ddrilling/AsbCloudServer
247 lines
9.4 KiB
C#
247 lines
9.4 KiB
C#
|
using System;
|
|||
|
using System.Collections;
|
|||
|
using System.Collections.Concurrent;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Linq;
|
|||
|
using System.Threading;
|
|||
|
using System.Threading.Tasks;
|
|||
|
|
|||
|
namespace AsbCloudInfrastructure.Services.Cache
|
|||
|
{
|
|||
|
#nullable enable
|
|||
|
public static class EfCacheL2
|
|||
|
{
|
|||
|
public static int RequestsToDb = 0;
|
|||
|
|
|||
|
static readonly ConcurrentDictionary<string, Lazy<CacheItem>> caches = new(1, 16);
|
|||
|
|
|||
|
private const int semaphoreTimeout = 25_000;
|
|||
|
private static readonly SemaphoreSlim semaphore = new(1);
|
|||
|
|
|||
|
private struct CacheItem
|
|||
|
{
|
|||
|
public readonly IEnumerable Data;
|
|||
|
public readonly DateTime DateObsolete;
|
|||
|
|
|||
|
public CacheItem(IEnumerable data, TimeSpan obsolescence)
|
|||
|
{
|
|||
|
DateObsolete = DateTime.Now + obsolescence;
|
|||
|
Data = data;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static CacheItem GetOrAddCache(string tag, Func<CacheItem> valueFactory)
|
|||
|
{
|
|||
|
Lazy<CacheItem>? lazyCache;
|
|||
|
while (!caches.TryGetValue(tag, out lazyCache))
|
|||
|
{
|
|||
|
if (semaphore.Wait(0))
|
|||
|
{
|
|||
|
lazyCache = new Lazy<CacheItem>(valueFactory);
|
|||
|
caches.TryAdd(tag, lazyCache);
|
|||
|
_ = lazyCache.Value;
|
|||
|
semaphore.Release();
|
|||
|
break;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if(semaphore.Wait(semaphoreTimeout))
|
|||
|
semaphore.Release();
|
|||
|
else
|
|||
|
{
|
|||
|
semaphore.Release();
|
|||
|
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (lazyCache.Value.DateObsolete < DateTime.Now)
|
|||
|
{
|
|||
|
var isUpdated = false;
|
|||
|
if (semaphore.Wait(0))
|
|||
|
{
|
|||
|
lazyCache = new Lazy<CacheItem>(valueFactory);
|
|||
|
caches.Remove(tag, out _);
|
|||
|
caches.TryAdd(tag, lazyCache);
|
|||
|
_ = lazyCache.Value;
|
|||
|
isUpdated = true;
|
|||
|
semaphore.Release();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if (semaphore.Wait(semaphoreTimeout))
|
|||
|
semaphore.Release();
|
|||
|
else
|
|||
|
{
|
|||
|
semaphore.Release();
|
|||
|
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (isUpdated || caches.TryGetValue(tag, out lazyCache))
|
|||
|
return lazyCache.Value;
|
|||
|
|
|||
|
throw new Exception("EfCacheL2.GetOrAddCache it should never happens");
|
|||
|
}
|
|||
|
else
|
|||
|
return lazyCache.Value;
|
|||
|
}
|
|||
|
|
|||
|
private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CacheItem> valueFactory, CancellationToken token)
|
|||
|
{
|
|||
|
Lazy<CacheItem>? lazyCache;
|
|||
|
while (!caches.TryGetValue(tag, out lazyCache))
|
|||
|
{
|
|||
|
if (semaphore.Wait(0, CancellationToken.None))
|
|||
|
{
|
|||
|
lazyCache = new Lazy<CacheItem>(valueFactory);
|
|||
|
caches.TryAdd(tag, lazyCache);
|
|||
|
_ = lazyCache.Value;
|
|||
|
semaphore.Release();
|
|||
|
break;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if (await semaphore.WaitAsync(semaphoreTimeout, token))
|
|||
|
semaphore.Release();
|
|||
|
else
|
|||
|
{
|
|||
|
semaphore.Release();
|
|||
|
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (lazyCache.Value.DateObsolete < DateTime.Now)
|
|||
|
{
|
|||
|
var isUpdated = false;
|
|||
|
if (semaphore.Wait(0, CancellationToken.None))
|
|||
|
{
|
|||
|
lazyCache = new Lazy<CacheItem>(valueFactory);
|
|||
|
caches.Remove(tag, out _);
|
|||
|
caches.TryAdd(tag, lazyCache);
|
|||
|
_ = lazyCache.Value;
|
|||
|
isUpdated = true;
|
|||
|
semaphore.Release();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if (await semaphore.WaitAsync(semaphoreTimeout, token))
|
|||
|
semaphore.Release();
|
|||
|
else
|
|||
|
{
|
|||
|
semaphore.Release();
|
|||
|
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (isUpdated || caches.TryGetValue(tag, out lazyCache))
|
|||
|
return lazyCache.Value;
|
|||
|
|
|||
|
throw new Exception("EfCacheL2.GetOrAddCache it should never happens");
|
|||
|
}
|
|||
|
else
|
|||
|
return lazyCache.Value;
|
|||
|
}
|
|||
|
|
|||
|
private static IEnumerable<T> ConvertToIEnumerable<T>(IEnumerable data)
|
|||
|
{
|
|||
|
if (data is IEnumerable<T> list)
|
|||
|
return list;
|
|||
|
else if (data is IDictionary dictionary)
|
|||
|
{
|
|||
|
System.Diagnostics.Trace.TraceWarning($"ConvertToIEnumerable. Use keyless method on keyed cache. Type: {typeof(T).Name};");
|
|||
|
return (IEnumerable<T>)dictionary.Values;
|
|||
|
}
|
|||
|
else
|
|||
|
throw new NotSupportedException("cache.Data has wrong type.");
|
|||
|
}
|
|||
|
|
|||
|
private static Dictionary<TKey, T> ConvertToDictionary<TKey, T>(IEnumerable data, Func<T, TKey> keySelector)
|
|||
|
where TKey : notnull
|
|||
|
{
|
|||
|
if (data is Dictionary<TKey, T> dictionary)
|
|||
|
return dictionary;
|
|||
|
else
|
|||
|
{
|
|||
|
System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};");
|
|||
|
return ((IEnumerable<T>)data).ToDictionary(keySelector);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static Func<CacheItem> MakeValueListFactory<T>(IQueryable<T> query, TimeSpan obsolescence)
|
|||
|
{
|
|||
|
CacheItem ValueFactory()
|
|||
|
{
|
|||
|
var list = query.ToList();
|
|||
|
RequestsToDb++;
|
|||
|
return new CacheItem(list, obsolescence);
|
|||
|
}
|
|||
|
return ValueFactory;
|
|||
|
}
|
|||
|
|
|||
|
private static Func<CacheItem> MakeValueDictionaryFactory<TKey, T>(IQueryable<T> query, TimeSpan obsolescence, Func<T, TKey> keySelector)
|
|||
|
where TKey : notnull
|
|||
|
{
|
|||
|
CacheItem ValueFactory()
|
|||
|
{
|
|||
|
var dictionary = query.ToDictionary(keySelector);
|
|||
|
RequestsToDb++;
|
|||
|
return new CacheItem(dictionary, obsolescence);
|
|||
|
};
|
|||
|
return ValueFactory;
|
|||
|
}
|
|||
|
|
|||
|
public static IEnumerable<T> FromCache<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence)
|
|||
|
{
|
|||
|
var factory = MakeValueListFactory(query, obsolescence);
|
|||
|
var cache = GetOrAddCache(tag, factory);
|
|||
|
return ConvertToIEnumerable<T>(cache.Data);
|
|||
|
}
|
|||
|
|
|||
|
public static async Task<IEnumerable<T>> FromCacheAsync<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, CancellationToken token = default)
|
|||
|
{
|
|||
|
var factory = MakeValueListFactory(query, obsolescence);
|
|||
|
var cache = await GetOrAddCacheAsync(tag, factory, token);
|
|||
|
return ConvertToIEnumerable<T>(cache.Data);
|
|||
|
}
|
|||
|
|
|||
|
public static Dictionary<TKey, T> FromCache<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector)
|
|||
|
where TKey: notnull
|
|||
|
{
|
|||
|
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector);
|
|||
|
var cache = GetOrAddCache(tag, factory);
|
|||
|
return ConvertToDictionary(cache.Data, keySelector);
|
|||
|
}
|
|||
|
|
|||
|
public static async Task<Dictionary<TKey, T>> FromCacheAsync<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, CancellationToken token = default)
|
|||
|
where TKey : notnull
|
|||
|
{
|
|||
|
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector);
|
|||
|
var cache = await GetOrAddCacheAsync(tag, factory, token);
|
|||
|
return ConvertToDictionary(cache.Data, keySelector);
|
|||
|
}
|
|||
|
|
|||
|
public static T? FromCacheGetValueOrDefault<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key)
|
|||
|
where TKey : notnull
|
|||
|
{
|
|||
|
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector);
|
|||
|
var cache = GetOrAddCache(tag, factory);
|
|||
|
|
|||
|
if (cache.Data is Dictionary<TKey, T> dictionary)
|
|||
|
return dictionary.GetValueOrDefault(key);
|
|||
|
else
|
|||
|
{
|
|||
|
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
|
|||
|
return ((IEnumerable<T>)cache.Data).FirstOrDefault(v => keySelector(v).Equals(key));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public static void DropCache<T>(this IQueryable<T> query, string tag)
|
|||
|
{
|
|||
|
caches.Remove(tag, out var _);
|
|||
|
}
|
|||
|
}
|
|||
|
#nullable disable
|
|||
|
}
|