forked from ddrilling/AsbCloudServer
412 lines
17 KiB
C#
412 lines
17 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace AsbCloudInfrastructure.Services.Cache
|
||
{
|
||
#nullable enable
|
||
/// <summary>
|
||
/// Кеширование запросов EF.
|
||
/// </summary>
|
||
public static class EfCacheL2
|
||
{
|
||
/// <summary>
|
||
/// Кол-во обращений к БД.
|
||
/// </summary>
|
||
public static int CountOfRequestsToDB = 0;
|
||
private static readonly Dictionary<string, CacheItem> caches = new(16);
|
||
private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25);
|
||
private static readonly SemaphoreSlim semaphore = new(1);
|
||
private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2);
|
||
|
||
private class CacheItem
|
||
{
|
||
internal IEnumerable? Data;
|
||
internal DateTime DateObsolete;
|
||
internal DateTime DateObsoleteTotal;
|
||
internal readonly SemaphoreSlim semaphore = new(1);
|
||
}
|
||
|
||
private static CacheItem GetOrAddCache(string tag, Func<IEnumerable> valueFactory, TimeSpan obsolete)
|
||
{
|
||
CacheItem cache;
|
||
while (!caches.ContainsKey(tag))
|
||
{
|
||
if (semaphore.Wait(0))
|
||
{
|
||
try {
|
||
cache = new CacheItem();
|
||
|
||
var dateObsolete = DateTime.Now + obsolete;
|
||
var dateQueryStart = DateTime.Now;
|
||
var data = valueFactory();
|
||
var queryTime = DateTime.Now - dateQueryStart;
|
||
|
||
if (dateObsolete - DateTime.Now < minCacheTime)
|
||
dateObsolete = DateTime.Now + minCacheTime;
|
||
|
||
cache.Data = data;
|
||
cache.DateObsolete = dateObsolete;
|
||
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
|
||
caches.Add(tag, cache);
|
||
}
|
||
catch
|
||
{
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
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");
|
||
}
|
||
}
|
||
}
|
||
|
||
cache = caches[tag];
|
||
|
||
if (cache.DateObsolete < DateTime.Now)
|
||
{
|
||
if (cache.semaphore.Wait(0))
|
||
{
|
||
try
|
||
{
|
||
var dateObsolete = DateTime.Now + obsolete;
|
||
var dateQueryStart = DateTime.Now;
|
||
var data = valueFactory();
|
||
var queryTime = DateTime.Now - dateQueryStart;
|
||
|
||
if (dateObsolete - DateTime.Now < minCacheTime)
|
||
dateObsolete = DateTime.Now + minCacheTime;
|
||
|
||
cache.Data = data;
|
||
cache.DateObsolete = dateObsolete;
|
||
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
|
||
}
|
||
catch
|
||
{
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
cache.semaphore.Release();
|
||
}
|
||
}
|
||
else if(cache.DateObsoleteTotal < DateTime.Now)
|
||
{
|
||
if (cache.semaphore.Wait(semaphoreTimeout))
|
||
{
|
||
cache.semaphore.Release();
|
||
}
|
||
else
|
||
{
|
||
cache.semaphore.Release();
|
||
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
|
||
}
|
||
}
|
||
}
|
||
return cache;
|
||
}
|
||
|
||
private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<IEnumerable>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
|
||
{
|
||
CacheItem cache;
|
||
while (!caches.ContainsKey(tag))
|
||
{
|
||
if (semaphore.Wait(0))
|
||
{
|
||
try
|
||
{
|
||
cache = new CacheItem();
|
||
|
||
var dateObsolete = DateTime.Now + obsolete;
|
||
var dateQueryStart = DateTime.Now;
|
||
var data = await valueFactoryAsync(token);
|
||
var queryTime = DateTime.Now - dateQueryStart;
|
||
|
||
if (dateObsolete - DateTime.Now < minCacheTime)
|
||
dateObsolete = DateTime.Now + minCacheTime;
|
||
|
||
cache.Data = data;
|
||
cache.DateObsolete = dateObsolete;
|
||
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
|
||
caches.Add(tag, cache);
|
||
}
|
||
catch
|
||
{
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
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");
|
||
}
|
||
}
|
||
}
|
||
|
||
cache = caches[tag];
|
||
|
||
if (cache.DateObsolete < DateTime.Now)
|
||
{
|
||
if (cache.semaphore.Wait(0))
|
||
{
|
||
try
|
||
{
|
||
var dateObsolete = DateTime.Now + obsolete;
|
||
var dateQueryStart = DateTime.Now;
|
||
var data = await valueFactoryAsync(token);
|
||
var queryTime = DateTime.Now - dateQueryStart;
|
||
|
||
if (dateObsolete - DateTime.Now < minCacheTime)
|
||
dateObsolete = DateTime.Now + minCacheTime;
|
||
|
||
cache.Data = data;
|
||
cache.DateObsolete = dateObsolete;
|
||
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
|
||
}
|
||
catch
|
||
{
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
cache.semaphore.Release();
|
||
}
|
||
}
|
||
else if (cache.DateObsoleteTotal < DateTime.Now)
|
||
{
|
||
if (await cache.semaphore.WaitAsync(semaphoreTimeout, token))
|
||
{
|
||
cache.semaphore.Release();
|
||
}
|
||
else
|
||
{
|
||
cache.semaphore.Release();
|
||
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache");
|
||
}
|
||
}
|
||
}
|
||
return cache;
|
||
}
|
||
|
||
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 if (data is IEnumerable<T> enumerable)
|
||
{
|
||
System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};");
|
||
return enumerable.ToDictionary(keySelector);
|
||
}
|
||
else
|
||
throw new NotSupportedException("cache.Data has wrong type.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кешировать запрос в List\<typeparamref name="T"\>.
|
||
/// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
|
||
/// </summary>
|
||
/// <typeparam name="T"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <returns></returns>
|
||
public static IEnumerable<T> FromCache<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence)
|
||
{
|
||
IEnumerable factory ()
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return query.ToList();
|
||
}
|
||
var cache = GetOrAddCache(tag, factory, obsolescence);
|
||
return ConvertToIEnumerable<T>(cache.Data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос в List\<typeparamref name="T"\>.
|
||
/// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
|
||
/// </summary>
|
||
/// <typeparam name="T"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
public static async Task<IEnumerable<T>> FromCacheAsync<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, CancellationToken token = default)
|
||
{
|
||
async Task<IEnumerable> factory(CancellationToken token)
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return await query.ToListAsync(token);
|
||
}
|
||
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
|
||
return ConvertToIEnumerable<T>(cache.Data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кешировать запрос в Dictionary\<typeparamref name="TKey", typeparamref name="T"\>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="T">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <returns></returns>
|
||
/// <example>
|
||
///
|
||
/// </example>
|
||
public static Dictionary<TKey, T> FromCache<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector)
|
||
where TKey: notnull
|
||
{
|
||
IEnumerable factory ()
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return query.ToDictionary(keySelector);
|
||
}
|
||
var cache = GetOrAddCache(tag, factory, obsolescence);
|
||
return ConvertToDictionary(cache.Data, keySelector);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос в Dictionary\<typeparamref name="TKey", typeparamref name="T"\>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="T">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
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
|
||
{
|
||
async Task<IEnumerable> factory(CancellationToken token)
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return await query.ToDictionaryAsync(keySelector, token);
|
||
}
|
||
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
|
||
return ConvertToDictionary(cache.Data, keySelector);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить запись из кеша по ключу.
|
||
/// При отсутствии кеша создаст его для всех записей из query.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="T">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="key"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="NotSupportedException">if cache contains trash</exception>
|
||
public static T? FromCacheGetValueOrDefault<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key)
|
||
where TKey : notnull
|
||
{
|
||
IEnumerable factory()
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return query.ToDictionary(keySelector);
|
||
}
|
||
var cache = GetOrAddCache(tag, factory, obsolescence);
|
||
var data = cache.Data;
|
||
if (data is Dictionary<TKey, T> dictionary)
|
||
return dictionary.GetValueOrDefault(key);
|
||
else if (data is IEnumerable<T> enumerable)
|
||
{
|
||
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
|
||
return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
|
||
}
|
||
else
|
||
throw new NotSupportedException("cache.Data has wrong type.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно получить запись из кеша по ключу.
|
||
/// При отсутствии кеша создаст его для всех записей из query.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="T">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="key"></param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="NotSupportedException">if cache contains trash</exception>
|
||
public static async Task<T?> FromCacheGetValueOrDefaultAsync<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key, CancellationToken token = default)
|
||
where TKey : notnull
|
||
{
|
||
async Task<IEnumerable> factory(CancellationToken token)
|
||
{
|
||
CountOfRequestsToDB++;
|
||
return await query.ToDictionaryAsync(keySelector, token);
|
||
}
|
||
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
|
||
|
||
var data = cache.Data;
|
||
if (data is Dictionary<TKey, T> dictionary)
|
||
return dictionary.GetValueOrDefault(key);
|
||
else if (data is IEnumerable<T> enumerable)
|
||
{
|
||
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
|
||
return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
|
||
}
|
||
else
|
||
throw new NotSupportedException("cache.Data has wrong type.");
|
||
}
|
||
|
||
public static void DropCache<T>(this IQueryable<T> query, string tag)
|
||
{
|
||
caches.Remove(tag, out var _);
|
||
}
|
||
}
|
||
#nullable disable
|
||
}
|