DD.WellWorkover.Cloud/AsbCloudInfrastructure/Services/Cache/EfCacheL2.cs
2022-06-01 12:18:10 +05:00

412 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}