diff --git a/AsbCloudApp/AsbCloudApp.csproj b/AsbCloudApp/AsbCloudApp.csproj index 7a10181c..8ca1bbfe 100644 --- a/AsbCloudApp/AsbCloudApp.csproj +++ b/AsbCloudApp/AsbCloudApp.csproj @@ -18,10 +18,6 @@ - - - - diff --git a/AsbCloudApp/CyclycArray.cs b/AsbCloudApp/CyclycArray.cs new file mode 100644 index 00000000..f47edc5b --- /dev/null +++ b/AsbCloudApp/CyclycArray.cs @@ -0,0 +1,198 @@ +#nullable enable +using System.Linq; + +namespace System.Collections.Generic +{ + /// + /// Цикличный массив + /// + /// + public class CyclycArray : IEnumerable + { + readonly T[] array; + int used, current = -1; + + /// + /// constructor + /// + /// + public CyclycArray(int capacity) + { + array = new T[capacity]; + } + + /// + /// Количество элементов в массиве + /// + public int Count => used; + + /// + /// Добавить новый элемент
+ /// Если capacity достигнуто, то вытеснит самый первый элемент + ///
+ /// + public void Add(T item) + { + current = (++current) % array.Length; + array[current] = item; + if (used < array.Length) + used++; + UpdatedInvoke(current, item); + } + + /// + /// Добавить новые элементы.
+ /// Если capacity достигнуто, то вытеснит самые первые элементы.
+ /// Не вызывает Updated! + ///
+ /// + public void AddRange(IEnumerable items) + { + var capacity = array.Length; + var newItems = items.TakeLast(capacity).ToArray(); + if (newItems.Length == capacity) + { + Array.Copy(newItems, array, capacity); + current = capacity - 1; + } + else + { + current = (++current) % capacity; + var countToEndOfArray = capacity - current; + if (newItems.Length <= countToEndOfArray) + { + Array.Copy(newItems, 0, array, current, newItems.Length); + current += newItems.Length - 1; + } + else + { + var firstStepLength = countToEndOfArray; + Array.Copy(newItems, 0, array, current, firstStepLength); + var secondStepCount = newItems.Length - firstStepLength; + Array.Copy(newItems, firstStepLength, array, 0, secondStepCount); + current = secondStepCount - 1; + } + } + + if (used < capacity) + { + used += newItems.Length; + used = used > capacity ? capacity : used; + } + } + + /// + /// Индекс + /// + /// + /// + public T this[int index] + { + get + { + if (used == 0) + throw new IndexOutOfRangeException(); + + var i = (current + 1 + index) % used; + return array[i]; + } + set + { + var devider = used > 0 ? used : array.Length; + var i = (current + 1 + index) % devider; + array[i] = value; + UpdatedInvoke(current, value); + } + } + + /// + /// событие на изменение элемента в массиве + /// + public event EventHandler<(int index, T value)>? Updated; + private void UpdatedInvoke(int index, T value) + { + Updated?.Invoke(this, (index, value)); + } + + /// + /// Агрегирование значения по всему массиву + /// + /// + /// + /// + /// + public Tout Aggregate(Func func, Tout startValue) + { + Tout result = startValue; + for (int i = 0; i < used; i++) + result = func(this[i], result); + return result; + } + + /// + public IEnumerator GetEnumerator() + => new CyclycListEnumerator(array, current, used); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + class CyclycListEnumerator : IEnumerator + { + private readonly Te[] array; + private readonly int used; + private readonly int first; + private int current = -1; + + public CyclycListEnumerator(Te[] array, int first, int used) + { + this.array = new Te[array.Length]; + array.CopyTo(this.array, 0); + this.used = used; + this.first = first; + } + + public Te Current + { + get + { + if (IsCurrentOk()) + { + var i = (current + first + 1) % used; + return array[i]; + } + else + return default!; + } + } + + object? IEnumerator.Current => Current; + + public void Dispose() {; } + + private bool IsCurrentOk() => current >= 0 && current < used; + + public bool MoveNext() + { + if (current < used) + current++; + return IsCurrentOk(); + } + + public void Reset() + { + current = -1; + } + } + + /// + /// Очистить весь массив + /// + public void Clear() + { + used = 0; + current = -1; + } + } +} +#nullable disable \ No newline at end of file diff --git a/AsbCloudApp/Data/Subsystems/SubsystemActiveWellStatDto.cs b/AsbCloudApp/Data/Subsystems/SubsystemActiveWellStatDto.cs index 2b62f171..2aee1d7f 100644 --- a/AsbCloudApp/Data/Subsystems/SubsystemActiveWellStatDto.cs +++ b/AsbCloudApp/Data/Subsystems/SubsystemActiveWellStatDto.cs @@ -10,7 +10,7 @@ namespace AsbCloudApp.Data.Subsystems /// /// Активная скважина /// - public WellDto Well { get; set; } + public WellDto Well { get; set; } = null!; /// /// Наработки подсистемы АКБ /// diff --git a/AsbCloudApp/Repositories/IFileStorageRepository.cs b/AsbCloudApp/Repositories/IFileStorageRepository.cs index 0d867db0..6118cc7a 100644 --- a/AsbCloudApp/Repositories/IFileStorageRepository.cs +++ b/AsbCloudApp/Repositories/IFileStorageRepository.cs @@ -51,7 +51,6 @@ namespace AsbCloudApp.Repositories /// /// Вывод списка всех файлов из базы, для которых нет файла на диске /// - /// /// /// IEnumerable GetListFilesNotDisc(IEnumerable files); diff --git a/AsbCloudApp/Services/FileService.cs b/AsbCloudApp/Services/FileService.cs index 3ba250aa..9b7ca156 100644 --- a/AsbCloudApp/Services/FileService.cs +++ b/AsbCloudApp/Services/FileService.cs @@ -165,15 +165,6 @@ namespace AsbCloudApp.Services public async Task> GetInfoByIdsAsync(IEnumerable idsFile, CancellationToken token) { var result = await fileRepository.GetInfoByIdsAsync(idsFile, token).ConfigureAwait(false); - - foreach (var entity in result) - { - - var ext = Path.GetExtension(entity.Name); - var relativePath = GetUrl(entity.IdWell, entity.IdCategory, entity.Id, ext); - var fullPath = Path.GetFullPath(relativePath); - } - return result; } diff --git a/AsbCloudApp/Services/IAuthService.cs b/AsbCloudApp/Services/IAuthService.cs index 80e9e3d7..93d14124 100644 --- a/AsbCloudApp/Services/IAuthService.cs +++ b/AsbCloudApp/Services/IAuthService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; namespace AsbCloudApp.Services { +#nullable enable /// /// Сервис авторизации /// @@ -33,13 +34,14 @@ namespace AsbCloudApp.Services /// /// токен отмены задачи /// - Task LoginAsync(string login, + Task LoginAsync(string login, string password, CancellationToken token = default); /// /// Обновление токена авторизации /// - /// + /// + /// /// Task RefreshAsync(ClaimsPrincipal identity, CancellationToken token); @@ -51,4 +53,5 @@ namespace AsbCloudApp.Services /// int Register(UserRegistrationDto userDto); } +#nullable disable } \ No newline at end of file diff --git a/AsbCloudApp/Services/IFileCategoryService.cs b/AsbCloudApp/Services/IFileCategoryService.cs index 70441331..91ca05a7 100644 --- a/AsbCloudApp/Services/IFileCategoryService.cs +++ b/AsbCloudApp/Services/IFileCategoryService.cs @@ -10,6 +10,12 @@ namespace AsbCloudApp.Services /// public interface IFileCategoryService { + /// + /// Получить категории файлов + /// + /// + /// + /// Task GetOrDefaultAsync(int id, CancellationToken token); /// diff --git a/AsbCloudApp/Services/ITelemetryDataService.cs b/AsbCloudApp/Services/ITelemetryDataService.cs index 6b0020ba..8efc74ae 100644 --- a/AsbCloudApp/Services/ITelemetryDataService.cs +++ b/AsbCloudApp/Services/ITelemetryDataService.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - +#nullable enable namespace AsbCloudApp.Services { /// @@ -22,7 +22,7 @@ namespace AsbCloudApp.Services /// кол-во элементов до которых эти данные прореживаются /// /// - Task> GetAsync(int idWell, + Task?> GetOrDefaultAsync(int idWell, DateTime dateBegin = default, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default); @@ -35,4 +35,5 @@ namespace AsbCloudApp.Services /// Task UpdateDataAsync(string uid, IEnumerable dtos, CancellationToken token = default); } -} \ No newline at end of file +} +#nullable disable \ No newline at end of file diff --git a/AsbCloudApp/Services/ITelemetryService.cs b/AsbCloudApp/Services/ITelemetryService.cs index c4e50b35..62b8f8e1 100644 --- a/AsbCloudApp/Services/ITelemetryService.cs +++ b/AsbCloudApp/Services/ITelemetryService.cs @@ -55,7 +55,7 @@ namespace AsbCloudApp.Services /// /// /// - int? GetIdTelemetryByIdWell(int idWell); + int? GetOrDefaultIdTelemetryByIdWell(int idWell); /// /// получить диапазон дат за которые есть данные diff --git a/AsbCloudApp/Services/Subsystems/ISubsystemService.cs b/AsbCloudApp/Services/Subsystems/ISubsystemService.cs index 7daf7d68..ff26a4d2 100644 --- a/AsbCloudApp/Services/Subsystems/ISubsystemService.cs +++ b/AsbCloudApp/Services/Subsystems/ISubsystemService.cs @@ -6,8 +6,19 @@ using System.Threading.Tasks; namespace AsbCloudApp.Services.Subsystems { #nullable enable + // TODO: move this to repositories + + /// + /// репозиторий получения подсистем + /// public interface ISubsystemService { + /// + /// получение списка подсистем. Если скважина указана, то получим только использованные в скважине подсистемы. + /// + /// + /// + /// Task?> GetSubsystemAsync(int? idWell, CancellationToken token); } #nullable disable diff --git a/AsbCloudDb/Model/AsbCloudDbContext.cs b/AsbCloudDb/Model/AsbCloudDbContext.cs index 058b9110..4e91eafe 100644 --- a/AsbCloudDb/Model/AsbCloudDbContext.cs +++ b/AsbCloudDb/Model/AsbCloudDbContext.cs @@ -60,15 +60,7 @@ namespace AsbCloudDb.Model public DbSet Record50 => Set(); public DbSet Record60 => Set(); public DbSet Record61 => Set(); - - private System.Text.Json.JsonSerializerOptions jsonSerializerOptions = new() - { - AllowTrailingCommas = true, - WriteIndented = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | - System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals, - }; - + public AsbCloudDbContext() : base() { } @@ -369,10 +361,5 @@ namespace AsbCloudDb.Model var sql = $"REFRESH MATERIALIZED VIEW {materializedViewName};"; return Database.ExecuteSqlRawAsync(sql, token); } - - public Task RefreshMaterializedViewAsync(string? mwName = null, CancellationToken token = default) where TEntity : class - { - throw new System.NotImplementedException(); - } } } diff --git a/AsbCloudDb/Model/DailyReport/Bha.cs b/AsbCloudDb/Model/DailyReport/Bha.cs index 5167c4b8..92c5c9b8 100644 --- a/AsbCloudDb/Model/DailyReport/Bha.cs +++ b/AsbCloudDb/Model/DailyReport/Bha.cs @@ -5,58 +5,57 @@ /// /// КНБК описание /// - public string BHADescription { get; set; } + public string BHADescription { get; set; } = string.Empty; /// /// Бурение с наращиваниями в инт. 2195-2763м. Время начала /// - public string ExtensionDrillingOneBegin { get; set; } + public string ExtensionDrillingOneBegin { get; set; } = string.Empty; /// /// Бурение с наращиваниями в инт. 2195-2763м. Время окончания /// - public string ExtensionDrillingOneFinish { get; set; } + public string ExtensionDrillingOneFinish { get; set; } = string.Empty; /// /// Промывка. Время начала /// - public string SluiceBegin { get; set; } + public string SluiceBegin { get; set; } = string.Empty; /// /// Промывка. Время окончания /// - public string SluiceFinish { get; set; } + public string SluiceFinish { get; set; } = string.Empty; /// /// Подьем КНБК. Время начала /// - public string ClimbBegin { get; set; } + public string ClimbBegin { get; set; } = string.Empty; /// /// Подьем КНБК. Время окончания /// - public string ClimbFinish { get; set; } + public string ClimbFinish { get; set; } = string.Empty; /// /// Спуск КНБК. Время начала /// - public string DescentBegin { get; set; } + public string DescentBegin { get; set; } = string.Empty; /// /// Спуск КНБК. Время окончания /// - public string DescentFinish { get; set; } + public string DescentFinish { get; set; } = string.Empty; /// /// Бурение с наращиваниями в инт. 2763-2850м. Время начала /// - public string ExtensionDrillingTwoBegin { get; set; } + public string ExtensionDrillingTwoBegin { get; set; } = string.Empty; /// /// Бурение с наращиваниями в инт. 2763-2850м. Время окончания /// - public string ExtensionDrillingTwoFinish { get; set; } + public string ExtensionDrillingTwoFinish { get; set; } = string.Empty; } } - diff --git a/AsbCloudDb/Model/DailyReport/Head.cs b/AsbCloudDb/Model/DailyReport/Head.cs index 8d90a556..16ef0fc1 100644 --- a/AsbCloudDb/Model/DailyReport/Head.cs +++ b/AsbCloudDb/Model/DailyReport/Head.cs @@ -6,22 +6,22 @@ namespace AsbCloudDb.Model.DailyReport /// /// название скважины /// - public string WellName { get; set; } + public string WellName { get; set; } = string.Empty; /// /// название куста /// - public string ClusterName { get; set; } + public string ClusterName { get; set; } = string.Empty; /// /// заказчик /// - public string Customer { get; set; } + public string Customer { get; set; } = string.Empty; /// /// подрядчик /// - public string Contractor { get; set; } + public string Contractor { get; set; } = string.Empty; /// /// дата рапорта @@ -61,12 +61,12 @@ namespace AsbCloudDb.Model.DailyReport /// /// ФИО бурильщиков /// - public string FirstDriller { get; set; } + public string FirstDriller { get; set; } = string.Empty; /// /// ФИО бурильщиков /// - public string SecondDriller { get; set; } + public string SecondDriller { get; set; } = string.Empty; /// /// Время работы АПД diff --git a/AsbCloudDb/Model/DailyReport/Saub.cs b/AsbCloudDb/Model/DailyReport/Saub.cs index dae26861..7399f148 100644 --- a/AsbCloudDb/Model/DailyReport/Saub.cs +++ b/AsbCloudDb/Model/DailyReport/Saub.cs @@ -72,32 +72,32 @@ namespace AsbCloudDb.Model.DailyReport /// /// указываются все причины, которые влияют на снижение МСП. /// - public string DeclinesReasonsROP { get; set; } + public string DeclinesReasonsROP { get; set; } = string.Empty; /// /// Увеличение мех скорости за секцию % /// - public string IncreaseSpeedSection { get; set; } + public string IncreaseSpeedSection { get; set; } = string.Empty; /// /// Увеличение мех скорости за сутки % /// - public string IncreaseSpeedDay { get; set; } + public string IncreaseSpeedDay { get; set; } = string.Empty; /// /// Сокращение времени бурения за секцию, ч /// - public string ReductionTimeDrilling { get; set; } + public string ReductionTimeDrilling { get; set; } = string.Empty; /// /// Ротор/Слайд % /// - public string RotorSlidePercent { get; set; } + public string RotorSlidePercent { get; set; } = string.Empty; /// /// МСП /// - public string MspSection { get; set; } + public string MspSection { get; set; } = string.Empty; } } diff --git a/AsbCloudDb/Model/DailyReport/Sign.cs b/AsbCloudDb/Model/DailyReport/Sign.cs index ebef294d..78271fe9 100644 --- a/AsbCloudDb/Model/DailyReport/Sign.cs +++ b/AsbCloudDb/Model/DailyReport/Sign.cs @@ -5,12 +5,12 @@ /// /// ФИО Мастера буровой /// - public string DrillingMaster { get; set; } + public string DrillingMaster { get; set; } = string.Empty; /// /// ФИО супервайзера /// - public string Supervisor { get; set; } + public string Supervisor { get; set; } = string.Empty; } } diff --git a/AsbCloudDb/Model/DailyReport/TimeBalance.cs b/AsbCloudDb/Model/DailyReport/TimeBalance.cs index a2d3a7e7..ee8c19f7 100644 --- a/AsbCloudDb/Model/DailyReport/TimeBalance.cs +++ b/AsbCloudDb/Model/DailyReport/TimeBalance.cs @@ -5,92 +5,92 @@ /// /// Бурение /// - public string Drilling { get; set; } + public string Drilling { get; set; } = string.Empty; /// /// Промывка /// - public string Flushing { get; set; } + public string Flushing { get; set; } = string.Empty; /// /// Наращивание /// - public string Building { get; set; } + public string Building { get; set; } = string.Empty; /// /// Проработка /// - public string Elaboration { get; set; } + public string Elaboration { get; set; } = string.Empty; /// /// Расширка /// - public string Extension { get; set; } + public string Extension { get; set; } = string.Empty; /// /// Ремонт /// - public string Repair { get; set; } + public string Repair { get; set; } = string.Empty; /// /// КНБК /// - public string Knbk { get; set; } + public string Knbk { get; set; } = string.Empty; /// /// СПО /// - public string Spo { get; set; } + public string Spo { get; set; } = string.Empty; /// /// ПЗР /// - public string Pzr { get; set; } + public string Pzr { get; set; } = string.Empty; /// /// ПВО /// - public string Pvo { get; set; } + public string Pvo { get; set; } = string.Empty; /// /// ПГР /// - public string Pgr { get; set; } + public string Pgr { get; set; } = string.Empty; /// /// ГИС /// - public string Gis { get; set; } + public string Gis { get; set; } = string.Empty; /// /// ОЗЦ /// - public string Ozc { get; set; } + public string Ozc { get; set; } = string.Empty; /// /// Тех. работы /// - public string EngineeringWorks { get; set; } + public string EngineeringWorks { get; set; } = string.Empty; /// /// Снятие замера /// - public string TakingMeasure { get; set; } + public string TakingMeasure { get; set; } = string.Empty; /// /// Цементирование /// - public string Cementing { get; set; } + public string Cementing { get; set; } = string.Empty; /// /// Простой /// - public string Simple { get; set; } + public string Simple { get; set; } = string.Empty; /// /// НПВ /// - public string Npv { get; set; } + public string Npv { get; set; } = string.Empty; } } diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 706ceb37..cc3d096f 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -16,6 +16,7 @@ using AsbCloudInfrastructure.Services.SAUB; using AsbCloudInfrastructure.Services.Subsystems; using AsbCloudInfrastructure.Services.WellOperationService; using AsbCloudInfrastructure.Validators; +using DocumentFormat.OpenXml.Spreadsheet; using FluentValidation.AspNetCore; using Mapster; using Microsoft.EntityFrameworkCore; @@ -90,7 +91,8 @@ namespace AsbCloudInfrastructure services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString(connectionStringName))); - services.AddFluentValidation(); + // TODO: переместить FluentValidation в описание моделей + services.AddFluentValidationClientsideAdapters(); services.AddScoped(provider => provider.GetService()); services.AddScoped(); @@ -101,6 +103,8 @@ namespace AsbCloudInfrastructure services.AddSingleton(new WitsInfoService()); services.AddSingleton(new CacheDb()); services.AddSingleton(new InstantDataRepository()); + services.AddSingleton(provider=> TelemetryDataCache.GetInstance(configuration)); + services.AddSingleton(provider=> TelemetryDataCache.GetInstance(configuration)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/AsbCloudInfrastructure/EfCache/EfCacheDictionaryExtensions.cs b/AsbCloudInfrastructure/EfCache/EfCacheDictionaryExtensions.cs deleted file mode 100644 index 033a3d47..00000000 --- a/AsbCloudInfrastructure/EfCache/EfCacheDictionaryExtensions.cs +++ /dev/null @@ -1,439 +0,0 @@ -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 static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4); - - private class CacheItem - { - internal IDictionary? Data; - internal DateTime DateObsolete; - internal DateTime DateObsoleteTotal; - internal readonly SemaphoreSlim semaphore = new(1); - - - internal Dictionary GetData() - where TKey : notnull - { - if (Data is Dictionary typedData) - return typedData; - throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); - } - - internal Dictionary GetData(Func convert, int attempt = 1) - where TKey : notnull - { - if (Data is Dictionary typedData) - return typedData; - if (Data is Dictionary typedEntityData) - { - if (semaphore.Wait(0)) - { - try - { - var convertedData = typedEntityData.ToDictionary(i => i.Key, i => convert(i.Value)); - Data = convertedData; - return convertedData; - } - catch - { - throw; - } - finally - { - semaphore.Release(); - } - } - else - { - if (semaphore.Wait(semaphoreTimeout)) - { - semaphore.Release(); - } - else - { - semaphore.Release(); - throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while converting cache data"); - } - } - } - if (attempt > 0) - return GetData(convert, --attempt); - throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); - } - } - - private static CacheItem GetOrAddCache(string tag, Func valueFactory, TimeSpan obsolete) - { - CacheItem cache; - while (!caches.ContainsKey(tag)) - { - if (semaphore.Wait(0)) - { - try - { - cache = new CacheItem(); - 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(); - 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<, >. С тегом typeof(TEntity).Name и ключом int Id - /// - /// - /// - /// - public static Dictionary FromCacheDictionary( - this IQueryable query) - where TEntity : class, AsbCloudDb.Model.IId - { - var tag = typeof(TEntity).Name; - return FromCacheDictionary(query, tag, defaultObsolescence, e => e.Id); - } - - /// - /// Кешировать запрос в Dictionary<, >. С тегом typeof(TEntity).Name - /// - /// - /// - /// - /// Делегат получения ключа из записи - /// - public static Dictionary FromCacheDictionary( - this IQueryable query, - Func keySelector) - where TEntity : class - where TKey : notnull - { - var tag = typeof(TEntity).Name; - return FromCacheDictionary(query, tag, defaultObsolescence, keySelector); - } - - /// - /// Кешировать запрос в Dictionary<, >. - /// - /// тип ключа - /// тип значения - /// - /// Метка кеша - /// Период устаревания данных - /// Делегат получения ключа из записи - /// - public static Dictionary FromCacheDictionary( - this IQueryable query, - string tag, - TimeSpan obsolescence, - Func keySelector) - where TEntity : class - where TKey : notnull - { - IDictionary factory() - => query.AsNoTracking().ToDictionary(keySelector); - var cache = GetOrAddCache(tag, factory, obsolescence); - return cache.GetData(); - } - - /// - /// Кешировать запрос с последующим преобразованием из в .
- /// Преобразование выполняется после получения из БД, результат кешируется в 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() - => query.AsNoTracking().ToDictionary(keySelector); - var cache = GetOrAddCache(tag, factory, obsolescence); - return cache.GetData(convert); - } - - /// - /// Асинхронно кешировать запрос в Dictionary<, >. С тегом typeof(TEntity).Name и ключом int Id - /// - /// - /// - /// - /// - public static Task> FromCacheDictionaryAsync( - this IQueryable query, - CancellationToken token = default) - where TEntity : class, AsbCloudDb.Model.IId - { - var tag = typeof(TEntity).Name; - return FromCacheDictionaryAsync(query, tag, defaultObsolescence, e => e.Id, token); - } - - /// - /// Асинхронно кешировать запрос в Dictionary<, >. С тегом typeof(TEntity).Name - /// - /// - /// - /// - /// Делегат получения ключа из записи - /// - /// - public static Task> FromCacheDictionaryAsync( - this IQueryable query, - Func keySelector, - CancellationToken token = default) - where TEntity : class - where TKey : notnull - { - var tag = typeof(TEntity).Name; - return FromCacheDictionaryAsync(query, tag, defaultObsolescence, keySelector, token); - } - - - /// - /// Асинхронно кешировать запрос в 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) - => await query.AsNoTracking().ToDictionaryAsync(keySelector, token); - var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); - return cache.GetData(); - } - - /// - /// Асинхронно кешировать запрос с последующим преобразованием из в .
- /// Преобразование выполняется после получения из БД, результат кешируется в 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) - => await query.AsNoTracking().ToDictionaryAsync(keySelector, token); - var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); - return cache.GetData(convert); - } - - /// - /// drops cache with tag = typeof(T).Name - /// - /// - /// - public static void DropCacheDictionary(this IQueryable query) - { - var tag = typeof(T).Name; - DropCacheDictionary(query, tag); - } - - /// - /// Очистить кеш - /// - /// - /// - /// Метка кеша - public static void DropCacheDictionary(this IQueryable query, string tag) - { - caches.Remove(tag, out var _); - } - } -#nullable disable -} diff --git a/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs b/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs index 0a93c70c..c618534d 100644 --- a/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs +++ b/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs @@ -349,7 +349,7 @@ namespace AsbCloudInfrastructure.EfCache return cache.GetData(convert); } - public static Task> FromCacheAsync(this IQueryable query, CancellationToken token = default) + public static Task> FromCacheAsync(this IQueryable query, CancellationToken token) where TEntity : class { var tag = typeof(TEntity).Name; @@ -365,7 +365,7 @@ namespace AsbCloudInfrastructure.EfCache /// Период устаревания данных /// /// - public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) + public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token) where TEntity : class { async Task factory(CancellationToken token) @@ -386,7 +386,7 @@ namespace AsbCloudInfrastructure.EfCache /// Преобразование данных БД в DTO /// /// - public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func convert, CancellationToken token = default) + public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func convert, CancellationToken token) where TEntity : class { async Task factory(CancellationToken token) diff --git a/AsbCloudInfrastructure/Repository/CrudCacheServiceBase.cs b/AsbCloudInfrastructure/Repository/CrudCacheServiceBase.cs index 94142e9d..ae9c1fed 100644 --- a/AsbCloudInfrastructure/Repository/CrudCacheServiceBase.cs +++ b/AsbCloudInfrastructure/Repository/CrudCacheServiceBase.cs @@ -54,7 +54,7 @@ namespace AsbCloudInfrastructure.Repository public override async Task> GetAllAsync(CancellationToken token) { var cache = await GetCacheAsync(token); - return cache.Values; + return cache; } /// @@ -65,14 +65,14 @@ namespace AsbCloudInfrastructure.Repository public override TDto? GetOrDefault(int id) { var cache = GetCache(); - return cache.GetValueOrDefault(id); + return cache.FirstOrDefault(d => d.Id == id); } /// public override async Task GetOrDefaultAsync(int id, CancellationToken token) { var cache = await GetCacheAsync(token); - return cache.GetValueOrDefault(id); + return cache.FirstOrDefault(d => d.Id == id); } /// @@ -102,17 +102,17 @@ namespace AsbCloudInfrastructure.Repository return result; } - protected virtual Task> GetCacheAsync(CancellationToken token) + protected virtual Task> GetCacheAsync(CancellationToken token) => GetQuery() - .FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token); + .FromCacheAsync(CacheTag, CacheOlescence, Convert, token); - protected virtual Dictionary GetCache() + protected virtual IEnumerable GetCache() => GetQuery() - .FromCacheDictionary(CacheTag, CacheOlescence, KeySelector, Convert); + .FromCache(CacheTag, CacheOlescence, Convert); protected virtual void DropCache() - => dbSet.DropCacheDictionary(CacheTag); + => dbSet.DropCache(CacheTag); } #nullable disable } \ No newline at end of file diff --git a/AsbCloudInfrastructure/Repository/CrudWellRelatedCacheServiceBase.cs b/AsbCloudInfrastructure/Repository/CrudWellRelatedCacheServiceBase.cs index f4e60abd..77729a13 100644 --- a/AsbCloudInfrastructure/Repository/CrudWellRelatedCacheServiceBase.cs +++ b/AsbCloudInfrastructure/Repository/CrudWellRelatedCacheServiceBase.cs @@ -23,25 +23,25 @@ namespace AsbCloudInfrastructure.Repository public CrudWellRelatedCacheServiceBase(IAsbCloudDbContext context, Func, IQueryable> makeQuery) : base(context, makeQuery) { } - public async Task> GetByIdWellAsync(int idWell, CancellationToken token) + public async Task?> GetByIdWellAsync(int idWell, CancellationToken token) { var cache = await GetCacheAsync(token); - var dtos = cache.Values + var dtos = cache .Where(e => e.IdWell == idWell) .ToList(); return dtos; } - public async Task> GetByIdWellAsync(IEnumerable idsWells, CancellationToken token) + public async Task?> GetByIdWellAsync(IEnumerable idsWells, CancellationToken token) { if (!idsWells.Any()) return Enumerable.Empty(); var cache = await GetCacheAsync(token); - var dtos = cache.Values + var dtos = cache .Where(e => idsWells.Contains(e.IdWell)) .ToList(); return dtos; diff --git a/AsbCloudInfrastructure/Repository/CrudWellRelatedServiceBase.cs b/AsbCloudInfrastructure/Repository/CrudWellRelatedServiceBase.cs index 49bab949..0e9a3ad8 100644 --- a/AsbCloudInfrastructure/Repository/CrudWellRelatedServiceBase.cs +++ b/AsbCloudInfrastructure/Repository/CrudWellRelatedServiceBase.cs @@ -23,7 +23,7 @@ namespace AsbCloudInfrastructure.Repository public CrudWellRelatedServiceBase(IAsbCloudDbContext context, Func, IQueryable> makeQuery) : base(context, makeQuery) { } - public async Task> GetByIdWellAsync(int idWell, CancellationToken token) + public async Task?> GetByIdWellAsync(int idWell, CancellationToken token) { var entities = await GetQuery() .Where(e => e.IdWell == idWell) @@ -32,7 +32,7 @@ namespace AsbCloudInfrastructure.Repository return dtos; } - public async Task> GetByIdWellAsync(IEnumerable idsWells, CancellationToken token) + public async Task?> GetByIdWellAsync(IEnumerable idsWells, CancellationToken token) { if (!idsWells.Any()) return Enumerable.Empty(); diff --git a/AsbCloudInfrastructure/Services/DailyReport/DailyReportService.cs b/AsbCloudInfrastructure/Services/DailyReport/DailyReportService.cs index 1ba5b36d..67e66eb5 100644 --- a/AsbCloudInfrastructure/Services/DailyReport/DailyReportService.cs +++ b/AsbCloudInfrastructure/Services/DailyReport/DailyReportService.cs @@ -12,6 +12,7 @@ using AsbCloudApp.Data.DailyReport; using AsbCloudApp.Requests; using AsbCloudInfrastructure.Services.DetectOperations; using AsbCloudApp.Data.DetectedOperation; +using AsbCloudApp.Exceptions; namespace AsbCloudInfrastructure.Services.DailyReport { @@ -34,7 +35,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport { var well = wellService.GetOrDefault(idWell); if (well is null || well.Timezone is null) - return null; + throw new ArgumentInvalidException("idWell doesn`t exist", nameof(idWell)); + var query = db.DailyReports.Where(r => r.IdWell == idWell); DateTimeOffset ExtractDate(DateTime dateTime) @@ -65,9 +67,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport public async Task GetOrGenerateAsync(int idWell, DateTime date, CancellationToken token) { var dateOnly = DateTime.SpecifyKind(date.Date, DateTimeKind.Utc); - var dailyReportDto = await GetAsync(idWell, dateOnly, token); - if (dailyReportDto is null) - dailyReportDto = await MakeDefaultDailyReportAsync(idWell, dateOnly, token); + var dailyReportDto = await GetOrDefaultAsync(idWell, dateOnly, token); + dailyReportDto ??= await MakeDefaultDailyReportAsync(idWell, dateOnly, token); return dailyReportDto; } @@ -105,7 +106,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport public async Task MakeReportAsync(int idWell, DateTime date, CancellationToken token = default) { - var dailyReportDto = await GetAsync(idWell, date, token); + var dailyReportDto = await GetOrDefaultAsync(idWell, date, token); if (dailyReportDto is null) return null; @@ -113,7 +114,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport return memoryStream; } - private async Task GetAsync(int idWell, DateTime date, CancellationToken token) + private async Task GetOrDefaultAsync(int idWell, DateTime date, CancellationToken token) { var dateOffset = date.Date; var entity = await db.DailyReports diff --git a/AsbCloudInfrastructure/Services/DrillParamsService.cs b/AsbCloudInfrastructure/Services/DrillParamsService.cs index 95528330..a2ac61c6 100644 --- a/AsbCloudInfrastructure/Services/DrillParamsService.cs +++ b/AsbCloudInfrastructure/Services/DrillParamsService.cs @@ -27,7 +27,7 @@ namespace AsbCloudInfrastructure.Services public async Task GetDefaultDrillParamsAsync(int idWell, double startDepth, double endDepth, CancellationToken token = default) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return null; diff --git a/AsbCloudInfrastructure/Services/FileCategoryService.cs b/AsbCloudInfrastructure/Services/FileCategoryService.cs index 2b30583e..b1976e64 100644 --- a/AsbCloudInfrastructure/Services/FileCategoryService.cs +++ b/AsbCloudInfrastructure/Services/FileCategoryService.cs @@ -15,15 +15,15 @@ namespace AsbCloudInfrastructure.Services : base(context) { } + //TODO: Перенести в сервис "дело скважины" public async Task> GetWellCaseCategoriesAsync(CancellationToken token) { var cache = await GetCacheAsync(token) .ConfigureAwait(false); var dtos = cache - .Where(kv => kv.Key >= 10000) - .Where(kv => kv.Key <= 20000) - .Select(kv => kv.Value); + .Where(f => f.Id >= 10000) + .Where(f => f.Id <= 20000); return dtos; } diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index 7d60f852..7143e063 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -113,7 +113,7 @@ namespace AsbCloudInfrastructure.Services public DatesRangeDto GetDatesRangeOrDefault(int idWell) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return null; var range = telemetryService.GetDatesRange((int)idTelemetry); diff --git a/AsbCloudInfrastructure/Services/SAUB/MessageService.cs b/AsbCloudInfrastructure/Services/SAUB/MessageService.cs index 541003e5..1911029a 100644 --- a/AsbCloudInfrastructure/Services/SAUB/MessageService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/MessageService.cs @@ -34,7 +34,7 @@ namespace AsbCloudInfrastructure.Services.SAUB int take = 32, CancellationToken token = default) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return null; diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataBaseService.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataBaseService.cs index a1881174..e918025a 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataBaseService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataBaseService.cs @@ -10,26 +10,31 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +#nullable enable namespace AsbCloudInfrastructure.Services.SAUB { - public abstract class TelemetryDataBaseService : ITelemetryDataService + public abstract class TelemetryDataBaseService : ITelemetryDataService where TDto : AsbCloudApp.Data.ITelemetryData - where TModel : class, ITelemetryData + where TEntity : class, ITelemetryData { protected readonly IAsbCloudDbContext db; private readonly ITelemetryService telemetryService; protected readonly CacheTable cacheTelemetryUsers; + private readonly TelemetryDataCache telemetryDataCache; public TelemetryDataBaseService( IAsbCloudDbContext db, ITelemetryService telemetryService, + TelemetryDataCache telemetryDataCache, CacheDb cacheDb) { this.db = db; this.telemetryService = telemetryService; cacheTelemetryUsers = cacheDb.GetCachedTable((AsbCloudDbContext)db); + this.telemetryDataCache = telemetryDataCache; } + /// public virtual async Task UpdateDataAsync(string uid, IEnumerable dtos, CancellationToken token = default) { if (dtos == default || !dtos.Any()) @@ -53,6 +58,8 @@ namespace AsbCloudInfrastructure.Services.SAUB var idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid); var timezone = telemetryService.GetTimezone(idTelemetry); + telemetryDataCache.AddRange(idTelemetry, dtos); + var entities = dtosList.Select(dto => { var entity = Convert(dto, timezone.Hours); @@ -63,7 +70,7 @@ namespace AsbCloudInfrastructure.Services.SAUB var entityMaxDate = entities.Max(e => e.DateTime); telemetryService.TelemetryTracker.SaveRequestDate(uid, entityMaxDate); - var dbset = db.Set(); + var dbset = db.Set(); var stopwatch = Stopwatch.StartNew(); try { @@ -83,11 +90,14 @@ namespace AsbCloudInfrastructure.Services.SAUB } } - public virtual async Task> GetAsync(int idWell, + + // TODO: It shouldn`t be nullable. Throw exceptions instead and return empty. + /// + public virtual async Task?> GetOrDefaultAsync(int idWell, DateTime dateBegin = default, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell) ?? -1; + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell) ?? -1; if (idTelemetry == -1) return null; @@ -110,8 +120,12 @@ namespace AsbCloudInfrastructure.Services.SAUB if (dateBeginUtc == default) dateBeginUtc = DateTime.UtcNow.AddSeconds(-intervalSec); + var cacheData = telemetryDataCache.GetOrDefault(idTelemetry, dateBeginUtc.ToRemoteDateTime(timezone.Hours), intervalSec, approxPointsCount); + if (cacheData is not null) + return cacheData; + var dateEnd = dateBeginUtc.AddSeconds(intervalSec); - var dbSet = db.Set(); + var dbSet = db.Set(); var query = dbSet .Where(d => d.IdTelemetry == idTelemetry @@ -144,9 +158,10 @@ namespace AsbCloudInfrastructure.Services.SAUB return dtos; } - public abstract TDto Convert(TModel src, double timezoneOffset); + public abstract TDto Convert(TEntity src, double timezoneOffset); - public abstract TModel Convert(TDto src, double timezoneOffset); + public abstract TEntity Convert(TDto src, double timezoneOffset); } } +#nullable disable \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs new file mode 100644 index 00000000..af2d471a --- /dev/null +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -0,0 +1,184 @@ +using AsbCloudDb.Model; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Mapster; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + +#nullable enable +namespace AsbCloudInfrastructure.Services.SAUB +{ + public class TelemetryDataCache + where TDto : AsbCloudApp.Data.ITelemetryData + { + private const int activeWellCapacity = 24 * 60 * 60; + private const int doneWellCapacity = 65 * 60; + + private readonly ConcurrentDictionary> caches; + private bool isLoading = false; + + private TelemetryDataCache() + { + caches = new(); + } + + private static TelemetryDataCache? instance; + + //TODO: Move initialize fromDB to bacground service task + public static TelemetryDataCache GetInstance(IConfiguration configuration) + where TEntity : class, ITelemetryData + { + if (instance is null) + { + instance = new TelemetryDataCache(); + _ = Task.Run(() => + { + using var db = MakeContext(configuration); + instance.InitializeCacheFromDB(db); + }); + } + return instance; + } + public static TelemetryDataCache GetInstance(IAsbCloudDbContext db, out Task initializationTask) + where TEntity : class, ITelemetryData + { + if (instance is null) + { + instance = new TelemetryDataCache(); + initializationTask = Task.Run(() => + { + instance.InitializeCacheFromDB(db); + }); + } + else + initializationTask = Task.CompletedTask; + return instance; + } + + /// + /// Добавить новые элементы в кеш + /// + /// + /// + public void AddRange(int idTelemetry, IEnumerable range) + { + CyclycArray cacheItem; + if (isLoading) + { + if (caches.TryGetValue(idTelemetry, out CyclycArray? localCacheItem)) + cacheItem = localCacheItem; + else + return; + } + else + { + cacheItem = caches.GetOrAdd(idTelemetry, _ => new CyclycArray(activeWellCapacity)); + } + + var newItems = range + .OrderBy(i => i.DateTime); + foreach (var item in newItems) + item.IdTelemetry = idTelemetry; + cacheItem.AddRange(newItems); + } + + /// + /// Получить данные из кеша.
+ /// Если dateBegin меньше минимального элемента в кеше, то вернется null. + /// Даже если intervalSec частично перекрыт данными из кеша. + ///
+ /// + /// + /// + /// кол-во элементов до которых эти данные прореживаются + /// + public IEnumerable? GetOrDefault(int idTelemetry, DateTime dateBegin, double intervalSec = 600d, int approxPointsCount = 1024) + { + if(!caches.TryGetValue(idTelemetry, out CyclycArray? cacheItem)) + return null; + + if (cacheItem is null || !cacheItem.Any() || cacheItem[0].DateTime > dateBegin) + return null; + + var dateEnd = dateBegin.AddSeconds(intervalSec); + var items = cacheItem + .Where(i => i.DateTime >= dateBegin && i.DateTime <= dateEnd); + + var ratio = items.Count() / approxPointsCount; + if (ratio > 1) + items = items + .Where((_, index) => index % ratio == 0); + + return items; + } + + private void InitializeCacheFromDB(IAsbCloudDbContext db) + where TEntity : class, ITelemetryData + { + if (isLoading) + throw new Exception("Multiple cache loading detected."); + + isLoading = true; + Well[] wells = Array.Empty(); + wells = db.Set() + .Include(well => well.Telemetry) + .Include(well => well.Cluster) + .Where(well => well.IdTelemetry != null) + .ToArray(); + + foreach (Well well in wells) + { + var capacity = well.IdState == 1 + ? activeWellCapacity + : doneWellCapacity; + + var idTelemetry = well.IdTelemetry!.Value; + var hoursOffset = well.Timezone.Hours; + + IEnumerable cacheItemData = GetCacheDataFromDb(db, idTelemetry, capacity, hoursOffset); + var cacheItem = new CyclycArray(capacity); + cacheItem.AddRange(cacheItemData); + caches.TryAdd(idTelemetry, cacheItem); + + System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} loaded"); + } + + System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> load complete"); + isLoading = false; + } + + private static IAsbCloudDbContext MakeContext(IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); + var options = new DbContextOptionsBuilder() + .UseNpgsql(connectionString) + .Options; + var db = new AsbCloudDbContext(options); + return db; + } + + private static IEnumerable GetCacheDataFromDb(IAsbCloudDbContext db, int idTelemetry, int capacity, double hoursOffset) + where TEntity : class, ITelemetryData + { + var entities = db.Set() + .Where(i => i.IdTelemetry == idTelemetry) + .OrderByDescending(i => i.DateTime) + .Take(capacity) + .ToArray() + .AsEnumerable() + .Reverse(); + + var dtos = entities.Select(entity => { + var dto = entity.Adapt(); + dto.DateTime = entity.DateTime.ToRemoteDateTime(hoursOffset); + return dto; + }); + + return dtos; + } + } +} +#nullable disable \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSaubService.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSaubService.cs index 419c5831..10a13913 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSaubService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSaubService.cs @@ -16,8 +16,9 @@ namespace AsbCloudInfrastructure.Services.SAUB public TelemetryDataSaubService( IAsbCloudDbContext db, ITelemetryService telemetryService, + TelemetryDataCache telemetryDataCache, CacheDb cacheDb) - : base(db, telemetryService, cacheDb) + : base(db, telemetryService, telemetryDataCache, cacheDb) { } public override TelemetryDataSaub Convert(TelemetryDataSaubDto src, double timezoneOffset) diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSpinService.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSpinService.cs index 5cd51161..14e84955 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSpinService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataSpinService.cs @@ -13,8 +13,9 @@ namespace AsbCloudInfrastructure.Services.SAUB public TelemetryDataSpinService( IAsbCloudDbContext db, ITelemetryService telemetryService, + TelemetryDataCache telemetryDataCache, CacheDb cacheDb) - : base(db, telemetryService, cacheDb) + : base(db, telemetryService, telemetryDataCache, cacheDb) { } public override TelemetryDataSpin Convert(TelemetryDataSpinDto src, double timezoneOffset) diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryService.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryService.cs index 21390596..f8574152 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryService.cs @@ -37,22 +37,22 @@ namespace AsbCloudInfrastructure.Services.SAUB this.timezoneService = timezoneService; } - private Dictionary GetTelemetryCache() + private IEnumerable GetTelemetryCache() { var cache = db.Telemetries .Include(t => t.Well) - .FromCacheDictionary(telemetryCacheTag, telemetryCacheObsolescence, t => t.Id); + .FromCache(telemetryCacheTag, telemetryCacheObsolescence); return cache; } private void DropTelemetryCache() { - db.Telemetries.DropCacheDictionary(telemetryCacheTag); + db.Telemetries.DropCache(telemetryCacheTag); } public DateTime GetLastTelemetryDate(int idTelemetry) { - var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry); + var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry); if (telemetry is null) throw new Exception($"Telemetry id:{idTelemetry} does not exist"); @@ -65,7 +65,7 @@ namespace AsbCloudInfrastructure.Services.SAUB public DatesRangeDto GetDatesRange(int idTelemetry) { - var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry); + var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry); if (telemetry is null) throw new Exception($"Telemetry id:{idTelemetry} does not exist"); @@ -107,7 +107,7 @@ namespace AsbCloudInfrastructure.Services.SAUB public SimpleTimezoneDto GetTimezone(int idTelemetry) { - var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry); + var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry); if (telemetry is null) throw new Exception($"Telemetry id: {idTelemetry} does not exist."); @@ -141,10 +141,10 @@ namespace AsbCloudInfrastructure.Services.SAUB throw new Exception($"Telemetry id: {idTelemetry} can't find timezone."); } - public int? GetIdTelemetryByIdWell(int idWell) + public int? GetOrDefaultIdTelemetryByIdWell(int idWell) { var telemetry = GetTelemetryCache() - .FirstOrDefault(t => t.Value.Well?.Id == idWell).Value; + .FirstOrDefault(t => t.Well?.Id == idWell); return telemetry?.Id; } @@ -156,7 +156,7 @@ namespace AsbCloudInfrastructure.Services.SAUB private Telemetry? GetOrDefaultTelemetryByUid(string uid) { - var telemetry = GetTelemetryCache().FirstOrDefault(kv => kv.Value.RemoteUid == uid).Value; + var telemetry = GetTelemetryCache().FirstOrDefault(t => t.RemoteUid == uid); return telemetry; } diff --git a/AsbCloudInfrastructure/Services/WellService.cs b/AsbCloudInfrastructure/Services/WellService.cs index 7107bda5..2b33d7bf 100644 --- a/AsbCloudInfrastructure/Services/WellService.cs +++ b/AsbCloudInfrastructure/Services/WellService.cs @@ -81,8 +81,7 @@ namespace AsbCloudInfrastructure.Services .Select(r => r.IdWell); var wellsDtos = (await GetCacheAsync(token)) - .Where(kv => wellsIds.Contains(kv.Key)) - .Select(kv => kv.Value); + .Where(w => wellsIds.Contains(w.Id)); return wellsDtos.ToList(); } @@ -95,7 +94,7 @@ namespace AsbCloudInfrastructure.Services if (dto.IdState is < 0 or > 2) throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto)); - if (dto.Id != 0 && (await GetCacheAsync(token)).ContainsKey(dto.Id)) + if (dto.Id != 0 && (await GetCacheAsync(token)).Any(w => w.Id == dto.Id)) throw new ArgumentInvalidException($"Нельзя повторно добавить скважину с id: {dto.Id}", nameof(dto)); var entity = Convert(dto); @@ -194,7 +193,7 @@ namespace AsbCloudInfrastructure.Services var cache = await GetCacheAsync(token); - var clusterWellsIds = cache.Values + var clusterWellsIds = cache .Where((w) => w.IdCluster == well.IdCluster) .Select(w => w.Id); @@ -247,7 +246,7 @@ namespace AsbCloudInfrastructure.Services public async Task EnshureTimezonesIsSetAsync(CancellationToken token) { var cache = await GetCacheAsync(token); - if (!cache.Values.Any(w => w.Timezone is null)) + if (!cache.Any(w => w.Timezone is null)) return; var defaultTimeZone = new SimpleTimezone diff --git a/AsbCloudWebApi.Tests/CacheTests/TelemetryDataCacheTest.cs b/AsbCloudWebApi.Tests/CacheTests/TelemetryDataCacheTest.cs new file mode 100644 index 00000000..5ae1d78c --- /dev/null +++ b/AsbCloudWebApi.Tests/CacheTests/TelemetryDataCacheTest.cs @@ -0,0 +1,93 @@ +using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.SAUB; +using Moq; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.CacheTests +{ + public class TelemetryDataCacheTest + { + private const int IdTelemetryOk = 10; + private static readonly DateTime baseDate = DateTime.Now; + private static readonly SimpleTimezone timezone = new() { Hours = TimeZoneInfo.Local.BaseUtcOffset.TotalHours }; + + private static readonly Well[] wellData = new Well[]{ + new(){ Id = 1, IdTelemetry = IdTelemetryOk, IdState = 1, Caption = "", Timezone = timezone} + }; + private static readonly TTelemetryData[] telemetryData = new TTelemetryData[]{ + new ( IdTelemetryOk, baseDate.AddSeconds(1), timezone.Hours ), + new ( IdTelemetryOk, baseDate.AddSeconds(2), timezone.Hours ), + new ( IdTelemetryOk, baseDate.AddSeconds(3), timezone.Hours ), + new ( IdTelemetryOk, baseDate.AddSeconds(4), timezone.Hours ), + }; + private readonly Mock dbMock; + + private TelemetryDataCache cacheTest; + + public class TTelemetryData : ITelemetryData, AsbCloudApp.Data.ITelemetryData + { + private DateTimeOffset _dateTime; + + public TTelemetryData(int idTelemetry, DateTime dateTime, double hoursOffset) + { + IdTelemetry = idTelemetry; + DateTime = dateTime; + var offset = TimeSpan.FromHours(hoursOffset); + var dateTimeUTC = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); + _dateTime = new DateTimeOffset(dateTimeUTC, offset) + .ToUniversalTime(); + } + + public int IdTelemetry { get; set; } + public DateTime DateTime { get; set; } + DateTimeOffset ITelemetryData.DateTime { get => _dateTime; set => _dateTime = value; } + } + + public TelemetryDataCacheTest() + { + dbMock = new Mock(); + dbMock + .AddDbSetMock(wellData) + .AddDbSetMock(telemetryData); + + cacheTest = TelemetryDataCache.GetInstance(dbMock.Object, out Task cacheInitialization); + cacheInitialization.Wait(); + } + + [Fact] + public void Get_existing_cache_returns_some() + { + var startDate = baseDate.AddSeconds(3); + var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600); + Assert.NotNull(data); + Assert.NotEmpty(data); + } + + [Fact] + public void Get_non_existing_cache_returns_null() + { + var startDate = baseDate.AddSeconds(-1); + var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600); + Assert.Null(data); + } + + [Fact] + public void Add_new_data_should_replace_IdTelemetry() + { + var startDate = baseDate.AddSeconds(4); + var newTelemetryData = new TTelemetryData[]{ + new ( 0, startDate, timezone.Hours), + new ( 0, startDate.AddSeconds(1), timezone.Hours), + }; + cacheTest.AddRange(IdTelemetryOk, newTelemetryData); + var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600); + + Assert.NotNull(data); + Assert.True(data?.Count() > 2); + Assert.All(data!, p => Assert.Equal(IdTelemetryOk, p.IdTelemetry)); + } + } +} diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs index b3ea33f8..9b2c99a7 100644 --- a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs +++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs @@ -9,7 +9,42 @@ namespace AsbCloudWebApi.Tests.Middlware { public class UserConnectionsLimitMiddlwareTest { - private readonly HttpClient httpClient; + const int iterations2Block = 8; + + //Данные в тестовой БД + //select + // tw.id, + // t_stat.minDate, + // t_stat.maxDate + //from( + // select + + // id_telemetry, + // count(1) as count, + // min("date") as minDate, + // max("date") as maxDate + + // from t_telemetry_data_saub + + // group by id_telemetry + //) as t_stat + //join t_well tw on tw.id_telemetry = t_stat.id_telemetry + //where tw is not null + //order by t_stat.count + //limit 10; + private readonly (int, DateTime, DateTime)[] wells = new[] + { + (191, new DateTime(2022, 09, 01, 21, 43, 00, DateTimeKind.Utc), new DateTime(2022, 09, 04, 07, 37, 31, DateTimeKind.Utc)), + (3 , new DateTime(2021, 09, 16, 06, 13, 33, DateTimeKind.Utc), new DateTime(2021, 09, 20, 00, 29, 28, DateTimeKind.Utc)), + (199, new DateTime(2022, 09, 15, 11, 27, 18, DateTimeKind.Utc), new DateTime(2022, 09, 20, 14, 00, 23, DateTimeKind.Utc)), + (6 , new DateTime(2021, 09, 20, 00, 35, 03, DateTimeKind.Utc), new DateTime(2021, 09, 25, 06, 46, 17, DateTimeKind.Utc)), + (41 , new DateTime(2021, 12, 10, 00, 59, 52, DateTimeKind.Utc), new DateTime(2022, 10, 31, 15, 29, 24, DateTimeKind.Utc)), + (100, new DateTime(2022, 04, 24, 03, 04, 05, DateTimeKind.Utc), new DateTime(2022, 04, 29, 11, 38, 36, DateTimeKind.Utc)), + (154, new DateTime(2022, 03, 28, 10, 09, 14, DateTimeKind.Utc), new DateTime(2022, 06, 14, 15, 01, 12, DateTimeKind.Utc)), + (5 , new DateTime(2021, 09, 25, 08, 09, 37, DateTimeKind.Utc), new DateTime(2021, 10, 01, 14, 39, 51, DateTimeKind.Utc)), + (1 , new DateTime(2021, 09, 10, 01, 32, 42, DateTimeKind.Utc), new DateTime(2021, 09, 18, 00, 35, 22, DateTimeKind.Utc)), + (112, new DateTime(2022, 04, 20, 16, 47, 51, DateTimeKind.Utc), new DateTime(2022, 04, 28, 15, 04, 33, DateTimeKind.Utc)), + }; public UserConnectionsLimitMiddlwareTest() { @@ -20,66 +55,53 @@ namespace AsbCloudWebApi.Tests.Middlware }) .Build(); host.Start(); - httpClient = new (); - httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM"); } [Fact] public async Task Send_n_requests_and_get_blocked() { - //Данные в тестовой БД - //select - // tw.id, - // t_stat.minDate, - // t_stat.maxDate - //from( - // select - - // id_telemetry, - // count(1) as count, - // min("date") as minDate, - // max("date") as maxDate - - // from t_telemetry_data_saub - - // group by id_telemetry - //) as t_stat - //join t_well tw on tw.id_telemetry = t_stat.id_telemetry - //where tw is not null - //order by t_stat.count - //limit 10; - - var wells = new [] - { - (191, new DateTime(2022, 09, 01, 21, 43, 00, DateTimeKind.Utc), new DateTime(2022, 09, 04, 07, 37, 31, DateTimeKind.Utc)), - (3 , new DateTime(2021, 09, 16, 06, 13, 33, DateTimeKind.Utc), new DateTime(2021, 09, 20, 00, 29, 28, DateTimeKind.Utc)), - (199, new DateTime(2022, 09, 15, 11, 27, 18, DateTimeKind.Utc), new DateTime(2022, 09, 20, 14, 00, 23, DateTimeKind.Utc)), - (6 , new DateTime(2021, 09, 20, 00, 35, 03, DateTimeKind.Utc), new DateTime(2021, 09, 25, 06, 46, 17, DateTimeKind.Utc)), - (41 , new DateTime(2021, 12, 10, 00, 59, 52, DateTimeKind.Utc), new DateTime(2022, 10, 31, 15, 29, 24, DateTimeKind.Utc)), - (100, new DateTime(2022, 04, 24, 03, 04, 05, DateTimeKind.Utc), new DateTime(2022, 04, 29, 11, 38, 36, DateTimeKind.Utc)), - (154, new DateTime(2022, 03, 28, 10, 09, 14, DateTimeKind.Utc), new DateTime(2022, 06, 14, 15, 01, 12, DateTimeKind.Utc)), - (5 , new DateTime(2021, 09, 25, 08, 09, 37, DateTimeKind.Utc), new DateTime(2021, 10, 01, 14, 39, 51, DateTimeKind.Utc)), - (1 , new DateTime(2021, 09, 10, 01, 32, 42, DateTimeKind.Utc), new DateTime(2021, 09, 18, 00, 35, 22, DateTimeKind.Utc)), - (112, new DateTime(2022, 04, 20, 16, 47, 51, DateTimeKind.Utc), new DateTime(2022, 04, 28, 15, 04, 33, DateTimeKind.Utc)), - }; - var i = 0; - for (; i < 5; i++) + for (; i < iterations2Block; i++) _ = Task.Run(async () => { var well = wells[i]; var url = MakeUrl(well.Item1, well.Item2, well.Item3); - var response = await httpClient.GetAsync(url); + var response = await MakeHttpClient().GetAsync(url); //await response.Content.ReadAsStringAsync(); await Task.Delay(1000); }); var well = wells[i]; var url = MakeUrl(well.Item1, well.Item2, well.Item3); - var response = await httpClient.GetAsync(url); + var response = await MakeHttpClient().GetAsync(url); Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, response.StatusCode); } + + [Fact] + public async Task Send_n_requests_and_get_blocked_then_restored() + { + var i = 0; + var tasks = new Task[iterations2Block]; + for (; i < iterations2Block; i++) + tasks[i] = Task.Run(async () => + { + var well = wells[i]; + var url = MakeUrl(well.Item1, well.Item2, well.Item3); + var response = await MakeHttpClient().GetAsync(url); + await Task.Delay(1000); + }); + + var well = wells[i]; + var url = MakeUrl(well.Item1, well.Item2, well.Item3); + var response = await MakeHttpClient().GetAsync(url); + Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, response.StatusCode); + + Task.WaitAll(tasks); + response = await MakeHttpClient().GetAsync(url); + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + } + private static string MakeUrl(int idWell, DateTime dateBegin, DateTime dateEnd) { var interval = (dateEnd - dateBegin).TotalSeconds; @@ -87,5 +109,13 @@ namespace AsbCloudWebApi.Tests.Middlware var url = $"http://127.0.0.1:5000/api/TelemetryDataSaub/{idWell}?begin={dateBeginString}&intervalSec={interval}&approxPointsCount={interval}"; return url; } + + private static HttpClient MakeHttpClient() + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM"); + return httpClient; + } } + } diff --git a/AsbCloudWebApi.Tests/ServicesTests/ClusterServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/ClusterServiceTest.cs index 6ee6ec44..44e7b481 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/ClusterServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/ClusterServiceTest.cs @@ -69,7 +69,7 @@ public class ClusterServiceTest public ClusterServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); wellService = new Mock(); context.Deposits.RemoveRange(context.Deposits); context.Clusters.RemoveRange(context.Clusters); diff --git a/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs index 790a806a..91664385 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs @@ -21,7 +21,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests protected override ICrudService MakeService() { - var dbContext = TestHelpter.MakeTestContext(); + var dbContext = TestHelpter.MakeRealTestContext(); return new CrudCacheServiceBase(dbContext); } } diff --git a/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs index 014680e1..f341a15a 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs @@ -107,7 +107,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests public DetectedOperationServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); context.SaveChanges(); context.Telemetries.Add(telemetry); diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs index 2c829eb4..e823189b 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs @@ -90,7 +90,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests { AsbCloudInfrastructure.DependencyInjection.MapsterSetup(); - db = TestHelpter.MakeTestContext(); + db = TestHelpter.MakeRealTestContext(); db.Wells.AddRange(wells); db.Companies.AddRange(companies); db.SaveChanges(); diff --git a/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs index dc8b9acf..0c406f1e 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs @@ -19,7 +19,7 @@ public class EventServiceTest public EventServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); cacheDb = new CacheDb(); var telemetryTracker = new Mock(); var imezoneServiceMock = new Mock(); diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs index 620261c5..345f2693 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs @@ -17,7 +17,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests public FileCategoryServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); context.SaveChanges(); service = new FileCategoryService(context); } diff --git a/AsbCloudWebApi.Tests/ServicesTests/ScheduleServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/ScheduleServiceTest.cs index 46d40eb0..ff009dea 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/ScheduleServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/ScheduleServiceTest.cs @@ -58,7 +58,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests public ScheduleServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); context.SaveChanges(); context.Deposits.Add(deposit); diff --git a/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs index 830edb3a..46d9136d 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs @@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests timezoneService.Setup(s => s.GetByCoordinates(It.IsAny(), It.IsAny())) .Returns(timezone); - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); cacheDb = new CacheDb(); telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object); @@ -64,7 +64,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests public async Task UpdateDataAsync() { // Arrange - var telemetryDataSaubService = new TelemetryDataSaubService(context, telemetryService, cacheDb); + var telemetryDataSaubService = new TelemetryDataSaubService(context, telemetryService, null, cacheDb); var now = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)).DateTime; var tuser = "Завулон"; diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs index eb0d0a89..d65e29c7 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs @@ -43,7 +43,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests public WellFinalDocumentsServiceTest() { - context = TestHelpter.MakeTestContext(); + context = TestHelpter.MakeRealTestContext(); context.SaveChanges(); fileServiceMock = new Mock(); diff --git a/AsbCloudWebApi.Tests/TestHelpter.cs b/AsbCloudWebApi.Tests/TestHelpter.cs index f7c8e8a9..94bf1392 100644 --- a/AsbCloudWebApi.Tests/TestHelpter.cs +++ b/AsbCloudWebApi.Tests/TestHelpter.cs @@ -1,7 +1,8 @@ -using AsbCloudApp.Services; -using AsbCloudDb.Model; +using AsbCloudDb.Model; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; +using Moq; +using System.Collections.Generic; +using System.Linq; namespace AsbCloudWebApi.Tests { @@ -9,7 +10,7 @@ namespace AsbCloudWebApi.Tests { // Попробовать когда-нибудь https://github.com/MichalJankowskii/Moq.EntityFrameworkCore - public static AsbCloudDbContext MakeTestContext() + public static AsbCloudDbContext MakeRealTestContext() { var options = new DbContextOptionsBuilder() //.UseInMemoryDatabase(System.Guid.NewGuid().ToString()) @@ -22,5 +23,38 @@ namespace AsbCloudWebApi.Tests context.Database.EnsureCreated(); return context; } + + public static Mock AddDbSetMock(this Mock contextMock, IEnumerable dbSetData) + where T : class + { + var dbSetMock = MakeDbSetMock(dbSetData); + contextMock.Setup(o => o.Set()) + .Returns(() => dbSetMock.Object); + + return contextMock; + } + + public static Mock> MakeDbSetMock() + where T : class + { + var dbSetData = new List(); + return MakeDbSetMock(dbSetData); + } + + public static Mock> MakeDbSetMock(IEnumerable dbSetData) + where T : class + { + var dbSetDataQueriable = dbSetData + .ToList() + .AsQueryable(); + + Mock> dbSetMock = new(); + dbSetMock.As>().Setup(o => o.Provider).Returns(() => dbSetDataQueriable.Provider); + dbSetMock.As>().Setup(o => o.Expression).Returns(() => dbSetDataQueriable.Expression); + dbSetMock.As>().Setup(o => o.ElementType).Returns(() => dbSetDataQueriable.ElementType); + dbSetMock.As>().Setup(o => o.GetEnumerator()).Returns(() => dbSetDataQueriable.GetEnumerator()); + + return dbSetMock; + } } } diff --git a/AsbCloudWebApi/Controllers/FileController.cs b/AsbCloudWebApi/Controllers/FileController.cs index 1340c55a..53b2fc52 100644 --- a/AsbCloudWebApi/Controllers/FileController.cs +++ b/AsbCloudWebApi/Controllers/FileController.cs @@ -37,7 +37,7 @@ namespace AsbCloudWebApi.Controllers /// id скважины /// id категории файла /// Коллекция файлов - /// dependency + /// dependency /// Токен отмены задачи /// [HttpPost] @@ -100,7 +100,6 @@ namespace AsbCloudWebApi.Controllers /// /// Возвращает файл с диска на сервере /// - /// id скважины /// id запрашиваемого файла /// Токен отмены задачи /// Запрашиваемый файл @@ -108,7 +107,7 @@ namespace AsbCloudWebApi.Controllers [Route("{idFile}")] [Permission] [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] - public async Task GetFileAsync([FromRoute] int idWell, + public async Task GetFileAsync( int idFile, CancellationToken token = default) { int? idCompany = User.GetCompanyId(); @@ -135,7 +134,7 @@ namespace AsbCloudWebApi.Controllers ///
/// id скважины /// id запрашиваемого файла - /// dependency + /// dependency /// Токен отмены задачи /// [HttpDelete("{idFile}")] diff --git a/AsbCloudWebApi/Controllers/SAUB/TelemetryDataBaseController.cs b/AsbCloudWebApi/Controllers/SAUB/TelemetryDataBaseController.cs index 345ecf71..132f5513 100644 --- a/AsbCloudWebApi/Controllers/SAUB/TelemetryDataBaseController.cs +++ b/AsbCloudWebApi/Controllers/SAUB/TelemetryDataBaseController.cs @@ -86,7 +86,7 @@ namespace AsbCloudWebApi.Controllers.SAUB if (!isCompanyOwnsWell) return Forbid(); - var content = await telemetryDataService.GetAsync(idWell, begin, + var content = await telemetryDataService.GetOrDefaultAsync(idWell, begin, intervalSec, approxPointsCount, token).ConfigureAwait(false); return Ok(content); diff --git a/AsbCloudWebApi/Controllers/SAUB/TelemetryInstantDataController.cs b/AsbCloudWebApi/Controllers/SAUB/TelemetryInstantDataController.cs index 844f81cd..0f673938 100644 --- a/AsbCloudWebApi/Controllers/SAUB/TelemetryInstantDataController.cs +++ b/AsbCloudWebApi/Controllers/SAUB/TelemetryInstantDataController.cs @@ -89,7 +89,7 @@ namespace AsbCloudWebApi.Controllers.SAUB if (!isCompanyOwnsWell) return Forbid(); - int? idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + int? idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return NoContent(); diff --git a/AsbCloudWebApi/Controllers/WITS/WitsControllerAbstract.cs b/AsbCloudWebApi/Controllers/WITS/WitsControllerAbstract.cs index 2077037f..f95512ab 100644 --- a/AsbCloudWebApi/Controllers/WITS/WitsControllerAbstract.cs +++ b/AsbCloudWebApi/Controllers/WITS/WitsControllerAbstract.cs @@ -72,7 +72,7 @@ namespace AsbCloudWebApi.Controllers.WITS [FromServices] IWitsRecordRepository witsRecordRepository, CancellationToken token) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return NoContent(); var dtos = await witsRecordRepository.GetLastAsync((int)idTelemetry, token); @@ -96,7 +96,7 @@ namespace AsbCloudWebApi.Controllers.WITS [FromServices] IWitsRecordRepository witsRecordRepository, CancellationToken token) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return NoContent(); var dtos = await witsRecordRepository.GetAsync((int)idTelemetry, begin, end, token); @@ -118,7 +118,7 @@ namespace AsbCloudWebApi.Controllers.WITS [FromServices] IWitsRecordRepository witsRecordRepository, CancellationToken token) { - var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell); + var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) return NoContent(); var dtos = await witsRecordRepository.GetStatAsync((int)idTelemetry, token); diff --git a/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs b/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs index 96b036e2..c1b108f8 100644 --- a/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs +++ b/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs @@ -14,25 +14,26 @@ namespace AsbCloudWebApi.Middlewares ///
class UserConnectionsLimitMiddlware { - private readonly RequestDelegate next; private readonly int parallelRequestsToController; - private readonly byte[] body; + private readonly RequestDelegate next; + private readonly byte[] responseBody; private readonly ConcurrentDictionary> stat = new (); private readonly IEnumerable? controllerNames; public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration) { + const int parallelRequestsToControllerDefault = 8; this.next = next; - var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue("parallelRequestsToController") ?? 5; + var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue("parallelRequestsToController") ?? parallelRequestsToControllerDefault; this.parallelRequestsToController = parallelRequestsToController > 0 ? parallelRequestsToController - : 5; + : parallelRequestsToControllerDefault; controllerNames = configuration.GetSection("userLimits")?.GetValue>("controllerNames"); var bodyText = $"Too Many Requests

Too Many Requests

I only allow {parallelRequestsToController} parallel requests per user. Try again soon.

"; - body = System.Text.Encoding.UTF8.GetBytes(bodyText); + responseBody = System.Text.Encoding.UTF8.GetBytes(bodyText); } public async Task InvokeAsync(HttpContext context, int idUser, string controllerName) @@ -41,14 +42,16 @@ namespace AsbCloudWebApi.Middlewares { await next(context); return; - } + } var userStat = stat.GetOrAdd(idUser, idUser => new()); - var count = userStat.AddOrUpdate(controllerName, 1, (key, value) => value + 1); - if(count < parallelRequestsToController) + var count = userStat.AddOrUpdate(controllerName, 0, (k, v) => v); + + if(count + 1 < parallelRequestsToController) { try { + userStat[controllerName]++; await next(context); } finally @@ -60,10 +63,9 @@ namespace AsbCloudWebApi.Middlewares { context.Response.Clear(); context.Response.StatusCode = (int)System.Net.HttpStatusCode.TooManyRequests; - context.Response.Headers.RetryAfter = "1000"; context.Response.Headers.ContentType = "text/html"; - await context.Response.BodyWriter.WriteAsync(body); + await context.Response.BodyWriter.WriteAsync(responseBody); } } } diff --git a/AsbCloudWebApi/wwwroot/index.html b/AsbCloudWebApi/wwwroot/index.html index 80927d26..74bafd67 100644 --- a/AsbCloudWebApi/wwwroot/index.html +++ b/AsbCloudWebApi/wwwroot/index.html @@ -1 +1,17 @@ -DDrilling
\ No newline at end of file + + + + + + + + + + + DDrilling + + + +
+ + diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs index b3d1d6fb..754e0194 100644 --- a/ConsoleApp1/Program.cs +++ b/ConsoleApp1/Program.cs @@ -2,7 +2,9 @@ using AsbCloudApp.Data.DailyReport; using AsbCloudDb; using AsbCloudDb.Model; +using AsbCloudInfrastructure; using AsbCloudInfrastructure.Services.DailyReport; +using AsbCloudInfrastructure.Services.SAUB; using ClosedXML.Excel; using DocumentFormat.OpenXml.Wordprocessing; using Microsoft.EntityFrameworkCore; @@ -17,22 +19,20 @@ using System.Threading.Tasks; namespace ConsoleApp1 { - class Program { private static AsbCloudDbContext db = ServiceFactory.Context; - + // use ServiceFactory to make services static void Main(/*string[] args*/) { - var h = new Hashtable(); - h.Add("name", 1); - h.Add("name2", "66"); - var v = h["v"]; + DependencyInjection.MapsterSetup(); + var sw = System.Diagnostics.Stopwatch.StartNew(); - var s = System.Text.Json.JsonSerializer.Serialize(h); - Console.WriteLine($"total time: ms"); + + sw.Stop(); + Console.WriteLine($"total time: {sw.ElapsedMilliseconds} ms"); Console.ReadLine(); } }