From b89ea13c78e00294d939de3dffaa50eaae28bdfc Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Wed, 1 Jun 2022 15:59:02 +0500 Subject: [PATCH] Converted cache --- .../EfCache/EfCacheExtensions.cs | 332 ++++++++++++++++ .../EfCache/EfCacheWithKeyExtensions.cs | 358 ++++++++++++++++++ ConsoleApp1/Program.cs | 20 +- 3 files changed, 699 insertions(+), 11 deletions(-) create mode 100644 AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs create mode 100644 AsbCloudInfrastructure/EfCache/EfCacheWithKeyExtensions.cs diff --git a/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs b/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs new file mode 100644 index 00000000..beaf1001 --- /dev/null +++ b/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs @@ -0,0 +1,332 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.EfCache +{ +#nullable enable + /// + /// Кеширование запросов EF.
+ /// Кеш не отслеживается ChangeTracker. + ///
+ public static class EfCacheExtensions + { + private static readonly Dictionary 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 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 GetOrAddCacheAsync(string tag, Func> 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; + } + + /// + /// Кешировать запрос в List<>. + /// + /// + /// + /// Метка кеша + /// Период устаревания данных + /// + public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence) + where TEntity : class + { + IEnumerable factory() + { + var queryData = query.AsNoTracking() + .ToList(); + return queryData; + } + var cache = GetOrAddCache(tag, factory, obsolescence); + if(cache.Data is IEnumerable typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Кешировать запрос с последующим преобразованием из в .
+ /// Преобразование выполняется после получения из БД, результат кешируется в List<>. + ///
+ /// + /// + /// + /// Метка кеша + /// Период устаревания данных + /// Преобразование данных БД в DTO + /// + public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func convert) + where TEntity : class + { + IEnumerable factory () + { + var queryData = query.AsNoTracking() + .ToList(); + var data = queryData + .Select(convert) + .ToList(); + return data; + } + var cache = GetOrAddCache(tag, factory, obsolescence); + if (cache.Data is IEnumerable typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Асинхронно кешировать запрос в List<>.
+ ///
+ /// + /// + /// Метка кеша + /// Период устаревания данных + /// + /// + public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) + where TEntity : class + { + async Task factory(CancellationToken token) + { + var queryData = await query.AsNoTracking().ToListAsync(token); + return queryData; + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); + if (cache.Data is IEnumerable typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Асинхронно кешировать запрос с последующим преобразованием из в .
+ /// Преобразование выполняется после получения из БД, результат кешируется в List<>. + ///
+ /// + /// + /// + /// Метка кеша + /// Период устаревания данных + /// Преобразование данных БД в DTO + /// + /// + public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func convert, CancellationToken token = default) + where TEntity : class + { + async Task factory(CancellationToken token) + { + var queryData = await query.AsNoTracking().ToListAsync(token); + var data = queryData + .Select(convert) + .ToList(); + return data; + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); + if (cache.Data is IEnumerable typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Очистить кеш + /// + /// + /// + /// Метка кеша + public static void DropCache(this IQueryable query, string tag) + { + caches.Remove(tag, out var _); + } + } +#nullable disable +} diff --git a/AsbCloudInfrastructure/EfCache/EfCacheWithKeyExtensions.cs b/AsbCloudInfrastructure/EfCache/EfCacheWithKeyExtensions.cs new file mode 100644 index 00000000..347b9e17 --- /dev/null +++ b/AsbCloudInfrastructure/EfCache/EfCacheWithKeyExtensions.cs @@ -0,0 +1,358 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.EfCache +{ +#nullable enable + /// + /// Кеширование запросов EF. + /// Кеш не отслеживается ChangeTracker. + /// + public static class EfCacheDictionaryExtensions + { + private static readonly Dictionary 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 IDictionary? Data; + internal DateTime DateObsolete; + internal DateTime DateObsoleteTotal; + internal readonly SemaphoreSlim semaphore = new(1); + } + + private static CacheItem GetOrAddCache(string tag, Func 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 GetOrAddCacheAsync(string tag, Func> 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; + } + + /// + /// Кешировать запрос в Dictionary<, >. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + public static Dictionary FromCacheDictionary( + this IQueryable query, + string tag, + TimeSpan obsolescence, + Func keySelector) + where TEntity : class + where TKey : notnull + { + IDictionary factory() + { + var queryData = query.AsNoTracking() + .ToDictionary(keySelector); + return queryData; + } + var cache = GetOrAddCache(tag, factory, obsolescence); + if (cache.Data is Dictionary typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Кешировать запрос с последующим преобразованием из в .
+ /// Преобразование выполняется после получения из БД, результат кешируется в Dictionary<, >. + ///
+ /// тип ключа + /// тип значения + /// + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// Преобразование данных БД в DTO + /// + public static Dictionary FromCacheDictionary( + this IQueryable query, + string tag, + TimeSpan obsolescence, + Func keySelector, + Func convert) + where TEntity : class + where TKey : notnull + { + IDictionary factory() + { + var queryData = query.AsNoTracking() + .ToList(); + var data = queryData + .ToDictionary(keySelector, convert); + return data; + } + var cache = GetOrAddCache(tag, factory, obsolescence); + if (cache.Data is Dictionary typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Асинхронно кешировать запрос в Dictionary<, >. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + /// + public static async Task> FromCacheDictionaryAsync( + this IQueryable query, + string tag, + TimeSpan obsolescence, + Func keySelector, + CancellationToken token = default) + where TEntity : class + where TKey : notnull + { + async Task factory(CancellationToken token) + { + var queryData = await query.AsNoTracking().ToDictionaryAsync(keySelector, token); + return queryData; + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); + + if (cache.Data is Dictionary typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Асинхронно кешировать запрос с последующим преобразованием из в .
+ /// Преобразование выполняется после получения из БД, результат кешируется в Dictionary<, >. + ///
+ /// тип ключа + /// тип значения + /// + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// Преобразование данных БД в DTO + /// + /// + public static async Task> FromCacheDictionaryAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, Func convert, CancellationToken token = default) + where TEntity : class + where TKey : notnull + { + async Task factory(CancellationToken token) + { + var queryData = await query.AsNoTracking().ToListAsync(token); + var data = queryData.ToDictionary(keySelector, convert); + return data; + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); + + if (cache.Data is Dictionary typedData) + return typedData; + throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); + } + + /// + /// Очистить кеш + /// + /// + /// + /// Метка кеша + public static void DropCacheDictionary(this IQueryable query, string tag) + { + caches.Remove(tag, out var _); + } + } +#nullable disable +} diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs index 44c26ec9..44610e58 100644 --- a/ConsoleApp1/Program.cs +++ b/ConsoleApp1/Program.cs @@ -1,6 +1,6 @@ using AsbCloudApp.Data; using AsbCloudDb.Model; -using AsbCloudInfrastructure.Services.Cache; +using AsbCloudInfrastructure.EfCache; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -22,9 +22,9 @@ namespace ConsoleApp1 for (int i = 0; i < 24; i++) { var t = new Thread(_ => { - for (int j = 0; j < 64; j++) - Task.Run(GetClastersAsync).Wait(); - //GetClasters(); + for (int j = 0; j < 32; j++) + //Task.Run(GetDataAsync).Wait(); + GetData(); }); t.Start(); } @@ -34,7 +34,7 @@ namespace ConsoleApp1 } static TimeSpan obso = TimeSpan.FromSeconds(5); - static (long, long) GetClasters() + static (long, long) GetData() { using var db = ServiceFactory.MakeContext(); var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -42,10 +42,9 @@ namespace ConsoleApp1 .Where(t => t.IdTelemetry == 135) .OrderBy(t => t.DateTime) .Take(100_000) - .FromCache("tds", obso) + .FromCache("tds", obso, r => new { r.Pressure, r.HookWeight }) .ToList(); sw.Stop(); - //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2rev4.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); GC.Collect(); @@ -53,18 +52,17 @@ namespace ConsoleApp1 return (cs.Count, sw.ElapsedMilliseconds); } - static async Task<(long, long)> GetClastersAsync() + static async Task<(long, long)> GetDataAsync() { using var db = ServiceFactory.MakeContext(); var sw = System.Diagnostics.Stopwatch.StartNew(); - var cs = ( await db.TelemetryDataSaub + var cs = (await db.TelemetryDataSaub .Where(t => t.IdTelemetry == 135) .OrderBy(t => t.DateTime) .Take(100_000) - .FromCacheAsync("tds", obso, r => (r.IdTelemetry, r.DateTime))) + .FromCacheDictionaryAsync("tds", obso, r => (r.IdTelemetry, r.DateTime), r => new {r.Pressure, r.HookWeight })) .ToList(); sw.Stop(); - //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2rev4.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); GC.Collect();