From cb4eb3341c1cb623b8f5c2425fdfdb9c3650e72f Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Wed, 1 Jun 2022 12:18:10 +0500 Subject: [PATCH] EfCacheL2 tested. --- .../Services/Cache/EfCacheL2.cs | 401 ++++++++++++------ ConsoleApp1/ConsoleApp1.csproj | 4 +- ConsoleApp1/Program.cs | 43 +- 3 files changed, 295 insertions(+), 153 deletions(-) diff --git a/AsbCloudInfrastructure/Services/Cache/EfCacheL2.cs b/AsbCloudInfrastructure/Services/Cache/EfCacheL2.cs index ad503b29..cd147d92 100644 --- a/AsbCloudInfrastructure/Services/Cache/EfCacheL2.cs +++ b/AsbCloudInfrastructure/Services/Cache/EfCacheL2.cs @@ -1,6 +1,6 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -9,44 +9,67 @@ using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.Cache { #nullable enable + /// + /// Кеширование запросов EF. + /// public static class EfCacheL2 { - public static int RequestsToDb = 0; - - static readonly ConcurrentDictionary> caches = new(1, 16); - - private const int semaphoreTimeout = 25_000; + /// + /// Кол-во обращений к БД. + /// + public static int CountOfRequestsToDB = 0; + 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 struct CacheItem + private class CacheItem { - public readonly IEnumerable Data; - public readonly DateTime DateObsolete; - - public CacheItem(IEnumerable data, TimeSpan obsolescence) - { - DateObsolete = DateTime.Now + obsolescence; - Data = data; - } + internal IEnumerable? Data; + internal DateTime DateObsolete; + internal DateTime DateObsoleteTotal; + internal readonly SemaphoreSlim semaphore = new(1); } - private static CacheItem GetOrAddCache(string tag, Func valueFactory) + private static CacheItem GetOrAddCache(string tag, Func valueFactory, TimeSpan obsolete) { - Lazy? lazyCache; - while (!caches.TryGetValue(tag, out lazyCache)) + CacheItem cache; + while (!caches.ContainsKey(tag)) { if (semaphore.Wait(0)) { - lazyCache = new Lazy(valueFactory); - caches.TryAdd(tag, lazyCache); - _ = lazyCache.Value; - semaphore.Release(); + 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)) + if (semaphore.Wait(semaphoreTimeout)) + { semaphore.Release(); + } else { semaphore.Release(); @@ -55,55 +78,91 @@ namespace AsbCloudInfrastructure.Services.Cache } } - if (lazyCache.Value.DateObsolete < DateTime.Now) + cache = caches[tag]; + + if (cache.DateObsolete < DateTime.Now) { - var isUpdated = false; - if (semaphore.Wait(0)) + if (cache.semaphore.Wait(0)) { - lazyCache = new Lazy(valueFactory); - caches.Remove(tag, out _); - caches.TryAdd(tag, lazyCache); - _ = lazyCache.Value; - isUpdated = true; - semaphore.Release(); + 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 + else if(cache.DateObsoleteTotal < DateTime.Now) { - if (semaphore.Wait(semaphoreTimeout)) - semaphore.Release(); + if (cache.semaphore.Wait(semaphoreTimeout)) + { + cache.semaphore.Release(); + } else { - semaphore.Release(); + cache.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; + return cache; } - private static async Task GetOrAddCacheAsync(string tag, Func valueFactory, CancellationToken token) + private static async Task GetOrAddCacheAsync(string tag, Func> valueFactoryAsync, TimeSpan obsolete, CancellationToken token) { - Lazy? lazyCache; - while (!caches.TryGetValue(tag, out lazyCache)) + CacheItem cache; + while (!caches.ContainsKey(tag)) { - if (semaphore.Wait(0, CancellationToken.None)) + if (semaphore.Wait(0)) { - lazyCache = new Lazy(valueFactory); - caches.TryAdd(tag, lazyCache); - _ = lazyCache.Value; - semaphore.Release(); + 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(); @@ -112,39 +171,52 @@ namespace AsbCloudInfrastructure.Services.Cache } } - if (lazyCache.Value.DateObsolete < DateTime.Now) + cache = caches[tag]; + + if (cache.DateObsolete < DateTime.Now) { - var isUpdated = false; - if (semaphore.Wait(0, CancellationToken.None)) + if (cache.semaphore.Wait(0)) { - lazyCache = new Lazy(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 + try { - semaphore.Release(); - throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); + 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"); } } - - if (isUpdated || caches.TryGetValue(tag, out lazyCache)) - return lazyCache.Value; - - throw new Exception("EfCacheL2.GetOrAddCache it should never happens"); } - else - return lazyCache.Value; + return cache; } - private static IEnumerable ConvertToIEnumerable(IEnumerable data) + private static IEnumerable ConvertToIEnumerable(IEnumerable? data) { if (data is IEnumerable list) return list; @@ -157,84 +229,177 @@ namespace AsbCloudInfrastructure.Services.Cache throw new NotSupportedException("cache.Data has wrong type."); } - private static Dictionary ConvertToDictionary(IEnumerable data, Func keySelector) + private static Dictionary ConvertToDictionary(IEnumerable? data, Func keySelector) where TKey : notnull { if (data is Dictionary dictionary) return dictionary; - else + else if (data is IEnumerable enumerable) { System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};"); - return ((IEnumerable)data).ToDictionary(keySelector); + return enumerable.ToDictionary(keySelector); } + else + throw new NotSupportedException("cache.Data has wrong type."); } - private static Func MakeValueListFactory(IQueryable query, TimeSpan obsolescence) - { - CacheItem ValueFactory() - { - var list = query.ToList(); - RequestsToDb++; - return new CacheItem(list, obsolescence); - } - return ValueFactory; - } - - private static Func MakeValueDictionaryFactory(IQueryable query, TimeSpan obsolescence, Func keySelector) - where TKey : notnull - { - CacheItem ValueFactory() - { - var dictionary = query.ToDictionary(keySelector); - RequestsToDb++; - return new CacheItem(dictionary, obsolescence); - }; - return ValueFactory; - } - + /// + /// Кешировать запрос в List\. + /// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря. + /// + /// + /// + /// Метка кеша + /// Период устаревания данных + /// public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence) { - var factory = MakeValueListFactory(query, obsolescence); - var cache = GetOrAddCache(tag, factory); + IEnumerable factory () + { + CountOfRequestsToDB++; + return query.ToList(); + } + var cache = GetOrAddCache(tag, factory, obsolescence); return ConvertToIEnumerable(cache.Data); } + /// + /// Асинхронно кешировать запрос в List\. + /// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря. + /// + /// + /// + /// Метка кеша + /// Период устаревания данных + /// + /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) { - var factory = MakeValueListFactory(query, obsolescence); - var cache = await GetOrAddCacheAsync(tag, factory, token); + async Task factory(CancellationToken token) + { + CountOfRequestsToDB++; + return await query.ToListAsync(token); + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return ConvertToIEnumerable(cache.Data); } + /// + /// Кешировать запрос в Dictionary\. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + /// + /// + /// public static Dictionary FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector) where TKey: notnull { - var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); - var cache = GetOrAddCache(tag, factory); + IEnumerable factory () + { + CountOfRequestsToDB++; + return query.ToDictionary(keySelector); + } + var cache = GetOrAddCache(tag, factory, obsolescence); return ConvertToDictionary(cache.Data, keySelector); } + /// + /// Асинхронно кешировать запрос в Dictionary\. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, CancellationToken token = default) where TKey : notnull { - var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); - var cache = await GetOrAddCacheAsync(tag, factory, token); + async Task factory(CancellationToken token) + { + CountOfRequestsToDB++; + return await query.ToDictionaryAsync(keySelector, token); + } + var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return ConvertToDictionary(cache.Data, keySelector); } + /// + /// Получить запись из кеша по ключу. + /// При отсутствии кеша создаст его для всех записей из query. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + /// + /// if cache contains trash public static T? FromCacheGetValueOrDefault(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key) where TKey : notnull { - var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); - var cache = GetOrAddCache(tag, factory); - - if (cache.Data is Dictionary dictionary) + IEnumerable factory() + { + CountOfRequestsToDB++; + return query.ToDictionary(keySelector); + } + var cache = GetOrAddCache(tag, factory, obsolescence); + var data = cache.Data; + if (data is Dictionary dictionary) return dictionary.GetValueOrDefault(key); - else + else if (data is IEnumerable enumerable) { System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};"); - return ((IEnumerable)cache.Data).FirstOrDefault(v => keySelector(v).Equals(key)); + return enumerable.FirstOrDefault(v => keySelector(v).Equals(key)); } + else + throw new NotSupportedException("cache.Data has wrong type."); + } + + /// + /// Асинхронно получить запись из кеша по ключу. + /// При отсутствии кеша создаст его для всех записей из query. + /// + /// тип ключа + /// тип значения + /// + /// Метка кеша + /// Период устаревания данных + /// Делегат получения ключа из записи + /// + /// + /// + /// if cache contains trash + public static async Task FromCacheGetValueOrDefaultAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key, CancellationToken token = default) + where TKey : notnull + { + async Task 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 dictionary) + return dictionary.GetValueOrDefault(key); + else if (data is IEnumerable 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(this IQueryable query, string tag) diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj index 1b11ff58..e0d86f92 100644 --- a/ConsoleApp1/ConsoleApp1.csproj +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs index 47aafc94..44c26ec9 100644 --- a/ConsoleApp1/Program.cs +++ b/ConsoleApp1/Program.cs @@ -1,7 +1,6 @@ using AsbCloudApp.Data; using AsbCloudDb.Model; using AsbCloudInfrastructure.Services.Cache; -//using AsbCloudInfrastructure.Services.Cache; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -14,40 +13,18 @@ namespace ConsoleApp1 class Program { + // use ServiceFactory to make services static void Main(/*string[] args*/) { - TypeAdapterConfig.GlobalSettings.Default.Config - .ForType() - .MapWith((source) => source.DateTime); - - TypeAdapterConfig.GlobalSettings.Default.Config - .ForType() - .MapWith((source) => source == default ? new DateTime(0, DateTimeKind.Utc) : source); - - TypeAdapterConfig.GlobalSettings.Default.Config - .ForType() - .MapWith((source) => source == default? default: source.MakeTimeOnly()); - - TypeAdapterConfig.GlobalSettings.Default.Config - .ForType() - .MapWith((source) => new (source)); - - var sh = new ScheduleDto{ - ShiftStart = new TimeDto { Hour = 11, Minute = 30, } - }; - var en = sh.Adapt(); - var aa = en.Adapt(); - // use ServiceFactory to make services Console.WriteLine("hit keyboard to start"); Console.ReadLine(); for (int i = 0; i < 24; i++) { - //Thread.Sleep(3000); var t = new Thread(_ => { for (int j = 0; j < 64; j++) - //Task.Run(GetClastersAsync).Wait(); - GetClasters(); + Task.Run(GetClastersAsync).Wait(); + //GetClasters(); }); t.Start(); } @@ -65,14 +42,14 @@ namespace ConsoleApp1 .Where(t => t.IdTelemetry == 135) .OrderBy(t => t.DateTime) .Take(100_000) - .FromCache("tds", obso, r=>(r.IdTelemetry, r.DateTime)) + .FromCache("tds", obso) .ToList(); sw.Stop(); - Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); - //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); + //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(); - Thread.Sleep(100); + Thread.Sleep(10); return (cs.Count, sw.ElapsedMilliseconds); } @@ -87,11 +64,11 @@ namespace ConsoleApp1 .FromCacheAsync("tds", obso, r => (r.IdTelemetry, r.DateTime))) .ToList(); sw.Stop(); - Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); - //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); + //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(); - Thread.Sleep(100); + Thread.Sleep(10); return (cs.Count, sw.ElapsedMilliseconds); } }