From 67113878a3b121bbd285f6740de68857554d33b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=94?= =?UTF-8?q?=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 5 Oct 2023 09:33:38 +0500 Subject: [PATCH 1/7] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Добавил проверку при удалении РТК 2. Сделал небольшой рефакторинг контроллеров --- .../Controllers/ProcessMapController.cs | 67 ++++++++++--------- ...ProcessMapWellboreDevelopmentController.cs | 37 ++++++---- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/AsbCloudWebApi/Controllers/ProcessMapController.cs b/AsbCloudWebApi/Controllers/ProcessMapController.cs index 9b749340..d90131d7 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapController.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using AsbCloudApp.Exceptions; using Microsoft.AspNetCore.Http; namespace AsbCloudWebApi.Controllers @@ -141,13 +142,13 @@ namespace AsbCloudWebApi.Controllers /// /// /// - [HttpPost] public override async Task> InsertAsync([FromBody] ProcessMapPlanDto value, CancellationToken token) { - if (!await CanUserEditProcessMapAsync(value.IdWell, token)) - return Forbid(); + value.IdUser = User.GetUserId() + ?? throw new ForbidException("Неизвестный пользователь"); + + await AssertUserHasAccessToProcessMapAsync(value.IdWell, token); - value.IdUser = User.GetUserId() ?? -1; var result = await base.InsertAsync(value, token); await NotifyUsersBySignalR(value.IdWell, token); return result; @@ -159,18 +160,25 @@ namespace AsbCloudWebApi.Controllers /// запись /// /// 1 - успешно отредактировано, 0 - нет - [HttpPut] public override async Task> UpdateAsync([FromBody] ProcessMapPlanDto value, CancellationToken token) { - if (!await CanUserEditProcessMapAsync(value.IdWell, token)) - return Forbid(); - - value.IdUser = User.GetUserId() ?? -1; + value.IdUser = User.GetUserId() + ?? throw new ForbidException("Неизвестный пользователь"); + + await AssertUserHasAccessToProcessMapAsync(value.IdWell, token); + var result = await base.UpdateAsync(value, token); await NotifyUsersBySignalR(value.IdWell, token); return result; } + public override async Task> DeleteAsync(int id, CancellationToken token) + { + await AssertUserHasAccessToProcessMapAsync(id, token); + + return await base.DeleteAsync(id, token); + } + /// /// Возвращает шаблон файла импорта плановой РТК /// @@ -199,13 +207,12 @@ namespace AsbCloudWebApi.Controllers [Required] IFormFile file, CancellationToken cancellationToken) { - int? idUser = User.GetUserId(); + var idUser = User.GetUserId(); - if (idUser is null) - return Forbid(); - - if (!await CanUserEditProcessMapAsync(idWell, cancellationToken)) - return Forbid(); + if (!idUser.HasValue) + throw new ForbidException("Неизвестный пользователь"); + + await AssertUserHasAccessToProcessMapAsync(idWell, cancellationToken); if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") return this.ValidationBadRequest(nameof(file), "Требуется xlsx файл."); @@ -239,11 +246,6 @@ namespace AsbCloudWebApi.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ExportAsync(int idWell, CancellationToken cancellationToken) { - int? idUser = User.GetUserId(); - - if (idUser is null) - return Forbid(); - var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken); if (well is null) @@ -254,23 +256,22 @@ namespace AsbCloudWebApi.Controllers return File(stream, "application/octet-stream", fileName); } - private async Task CanUserEditProcessMapAsync(int idWell, CancellationToken token) + private async Task AssertUserHasAccessToProcessMapAsync(int idWell, CancellationToken cancellationToken) { var idUser = User.GetUserId(); - - if (!idUser.HasValue) - return false; - var idCompany = User.GetCompanyId(); - if (!idCompany.HasValue || !await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, token)) - return false; - - var well = await wellService.GetOrDefaultAsync(idWell, token); - if (well is null) - return false; - - return well.IdState != 2 || userRepository.HasPermission(idUser.Value, "ProcessMap.editCompletedWell"); + if (!idCompany.HasValue || !idUser.HasValue) + throw new ForbidException("Неизвестный пользователь"); + + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) + ?? throw new ForbidException($"Скважины с {idWell} не существует"); + + if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, cancellationToken)) + throw new ForbidException("Нет доступа к скважине"); + + if (well.IdState == 2 && !userRepository.HasPermission(idUser.Value, "ProcessMap.editCompletedWell")) + throw new ForbidException("Недостаточно прав для редактирования РТК завершённой скважины"); } private async Task NotifyUsersBySignalR(int idWell, CancellationToken token) diff --git a/AsbCloudWebApi/Controllers/ProcessMapWellboreDevelopmentController.cs b/AsbCloudWebApi/Controllers/ProcessMapWellboreDevelopmentController.cs index 07df4cbe..5ba60684 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapWellboreDevelopmentController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapWellboreDevelopmentController.cs @@ -36,18 +36,17 @@ public class ProcessMapWellboreDevelopmentController : CrudWellRelatedController /// /// /// - /// public override async Task> InsertAsync(ProcessMapWellboreDevelopmentDto value, CancellationToken token) { value.IdUser = User.GetUserId() ?? throw new ForbidException("Неизвестный пользователь"); - await AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(value.IdWell, value.IdUser, token); + await AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(value.IdWell, token); return await processMapWellboreDevelopmentService.InsertAsync(value, token); } - /// + /// /// Обновить запись проработки /// /// @@ -58,12 +57,19 @@ public class ProcessMapWellboreDevelopmentController : CrudWellRelatedController value.IdUser = User.GetUserId() ?? throw new ForbidException("Неизвестный пользователь"); - await AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(value.IdWell, value.IdUser, token); + await AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(value.IdWell, token); return await processMapWellboreDevelopmentService.UpdateAsync(value, token); } - - /// + + public override async Task> DeleteAsync(int id, CancellationToken token) + { + await AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(id, token); + + return await base.DeleteAsync(id, token); + } + + /// /// Возвращает проработки по uid телеметрии /// /// Уникальный ключ телеметрии @@ -81,16 +87,21 @@ public class ProcessMapWellboreDevelopmentController : CrudWellRelatedController return Ok(dto); } - private async Task AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(int idUser, int idWell, CancellationToken cancellationToken) - { - var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) + private async Task AssertUserHasAccessToProcessMapWellboreDevelopmentAsync(int idWell, CancellationToken cancellationToken) + { + var idUser = User.GetUserId(); + var idCompany = User.GetCompanyId(); + + if (!idCompany.HasValue || !idUser.HasValue) + throw new ForbidException("Неизвестный пользователь"); + + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) ?? throw new ForbidException($"Скважины с {idWell} не существует"); - - var idCompany = User.GetCompanyId(); - if (!idCompany.HasValue || !await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, cancellationToken)) + + if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, cancellationToken)) throw new ForbidException("Нет доступа к скважине"); - if (well.IdState == 2 && !userRepository.HasPermission(idUser, "ProcessMap.editCompletedWell")) + if (well.IdState == 2 && !userRepository.HasPermission(idUser.Value, "ProcessMap.editCompletedWell")) throw new ForbidException("Недостаточно прав для редактирования РТК завершённой скважины"); } } \ No newline at end of file From cf5b1eea2513ae48b0387308f7446d9eaab36f6f Mon Sep 17 00:00:00 2001 From: Olga Nemt Date: Thu, 5 Oct 2023 17:46:39 +0500 Subject: [PATCH 2/7] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=20=D0=BD=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=20=D0=BE=D1=82=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BF=D0=BE=20=D1=83=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B2=20=D0=BA=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D1=8C=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AsbCloudWebApi/Controllers/SlipsStatController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/AsbCloudWebApi/Controllers/SlipsStatController.cs b/AsbCloudWebApi/Controllers/SlipsStatController.cs index 124622a3..1049749d 100644 --- a/AsbCloudWebApi/Controllers/SlipsStatController.cs +++ b/AsbCloudWebApi/Controllers/SlipsStatController.cs @@ -34,7 +34,6 @@ namespace AsbCloudWebApi.Controllers /// Токен отмены задачи /// Список бурильщиков [HttpGet] - [Permission] [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] public async Task GetAllAsync( [FromQuery] OperationStatRequest request, From 01f04c7ea5d44ab8ae55723dc944bbe8286030a2 Mon Sep 17 00:00:00 2001 From: Frolov-Nikita Date: Fri, 6 Oct 2023 15:19:02 +0500 Subject: [PATCH 3/7] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20WellboreService.GetWellb?= =?UTF-8?q?oresAsync()=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=20WellOperationRepository.GetSectionsAsync()=20=D0=9E=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=20WellOperationRepository.FirstOperationDate()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AsbCloudApp/Data/SectionByOperationsDto.cs | 47 + .../Repositories/IWellOperationRepository.cs | 8 + AsbCloudInfrastructure/Background/todo.md | 5 + .../Repository/WellOperationRepository.cs | 828 ++++++++++-------- .../Services/WellboreService.cs | 59 +- 5 files changed, 532 insertions(+), 415 deletions(-) create mode 100644 AsbCloudApp/Data/SectionByOperationsDto.cs create mode 100644 AsbCloudInfrastructure/Background/todo.md diff --git a/AsbCloudApp/Data/SectionByOperationsDto.cs b/AsbCloudApp/Data/SectionByOperationsDto.cs new file mode 100644 index 00000000..80d2a3b4 --- /dev/null +++ b/AsbCloudApp/Data/SectionByOperationsDto.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace AsbCloudApp.Data; + +/// +/// Параметры секции определяемые по операциям из ГГД +/// +public class SectionByOperationsDto +{ + /// + /// Id скважины + /// + public int IdWell { get; set; } + + /// + /// 0 = план или 1 = факт или прогноз = 2 + /// + public int IdType { get; set; } + + /// + /// id секции скважины + /// + public int IdWellSectionType { get; set; } + + /// + /// Глубина начала первой операции в секции, м + /// + [Range(0, 50_000)] + public double DepthStart { get; set; } + + /// + /// Дата начала первой операции в секции + /// + public DateTimeOffset DateStart { get; set; } + + /// + /// Глубина после завершения последней операции операции в секции, м + /// + [Range(0, 50_000)] + public double DepthEnd { get; set; } + + /// + /// Дата после завершения последней операции операции в секции + /// + public DateTimeOffset DateEnd { get; set; } +} diff --git a/AsbCloudApp/Repositories/IWellOperationRepository.cs b/AsbCloudApp/Repositories/IWellOperationRepository.cs index b02d3e42..faab324f 100644 --- a/AsbCloudApp/Repositories/IWellOperationRepository.cs +++ b/AsbCloudApp/Repositories/IWellOperationRepository.cs @@ -97,5 +97,13 @@ namespace AsbCloudApp.Repositories /// /// Task DeleteAsync(IEnumerable ids, CancellationToken token); + + /// + /// Получить секции скважин из операций ГГД. Секцие поделены на плановые и фактические. + /// + /// + /// + /// + Task> GetSectionsAsync(IEnumerable idsWells, CancellationToken token); } } \ No newline at end of file diff --git a/AsbCloudInfrastructure/Background/todo.md b/AsbCloudInfrastructure/Background/todo.md new file mode 100644 index 00000000..f30ce6c4 --- /dev/null +++ b/AsbCloudInfrastructure/Background/todo.md @@ -0,0 +1,5 @@ +# +- +- . . + - / + - / diff --git a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs index f0eb5f6d..cd913980 100644 --- a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs +++ b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs @@ -13,394 +13,452 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Repository +namespace AsbCloudInfrastructure.Repository; + + +/// +/// репозиторий операций по скважине +/// +public class WellOperationRepository : IWellOperationRepository { + private const string KeyCacheSections = "OperationsBySectionSummarties"; + private readonly IAsbCloudDbContext db; + private readonly IMemoryCache memoryCache; + private readonly IWellService wellService; - /// - /// репозиторий операций по скважине - /// - public class WellOperationRepository : IWellOperationRepository + public WellOperationRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService) { - private readonly IAsbCloudDbContext db; - private readonly IMemoryCache memoryCache; - private readonly IWellService wellService; - private static Dictionary? firstOperationsCache = null; - - public WellOperationRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService) - { - this.db = db; - this.memoryCache = memoryCache; - this.wellService = wellService; - } - - /// - public IEnumerable GetCategories(bool includeParents) - { - var categories = memoryCache - .GetOrCreateBasic(db.Set()); - - if (!includeParents) - { - var parentIds = categories - .Select(o => o.IdParent) - .Distinct(); - - categories = categories - .Where(o => !parentIds.Contains(o.Id)); - } - - var result = categories - .OrderBy(o => o.Name) - .Adapt>(); - - return result; - } - - /// - public IEnumerable GetSectionTypes() => - memoryCache - .GetOrCreateBasic(db.Set()) - .OrderBy(s => s.Order) - .Select(s => s.Adapt()); - - - - public async Task GetOperationsPlanAsync(int idWell, DateTime? currentDate, CancellationToken token) - { - var timezone = wellService.GetTimezone(idWell); - var request = new WellOperationRequest() - { - IdWell = idWell, - OperationType = WellOperation.IdOperationTypePlan, - }; - - var entities = await BuildQuery(request) - .AsNoTracking() - .ToArrayAsync(token) - .ConfigureAwait(false); - - var dateLastAssosiatedPlanOperation = await GetDateLastAssosiatedPlanOperationAsync(idWell, currentDate, timezone.Hours, token); - - var result = new WellOperationPlanDto() - { - WellOperationsPlan = entities, - DateLastAssosiatedPlanOperation = dateLastAssosiatedPlanOperation - }; - - return result; - } - - private async Task GetDateLastAssosiatedPlanOperationAsync( - int idWell, - DateTime? lessThenDate, - double timeZoneHours, - CancellationToken token) - { - if (lessThenDate is null) - return null; - - var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours); - var timeZoneOffset = TimeSpan.FromHours(timeZoneHours); - - var lastFactOperation = await db.WellOperations - .Where(o => o.IdWell == idWell) - .Where(o => o.IdType == WellOperation.IdOperationTypeFact) - .Where(o => o.IdPlan != null) - .Where(o => o.DateStart < currentDateOffset) - .Include(x => x.OperationPlan) - .OrderByDescending(x => x.DateStart) - .FirstOrDefaultAsync(token) - .ConfigureAwait(false); - - if (lastFactOperation is not null) - return DateTime.SpecifyKind(lastFactOperation.OperationPlan.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified); - return null; - } - - /// - public DateTimeOffset? FirstOperationDate(int idWell) - { - if (firstOperationsCache is null) - { - var query = db.WellOperations - .GroupBy(o => o.IdWell) - .Select(g => new Tuple - ( - g.Key, - g.Where(o => o.IdType == WellOperation.IdOperationTypePlan).Min(o => o.DateStart), - g.Where(o => o.IdType == WellOperation.IdOperationTypeFact).Min(o => o.DateStart) - )); - - firstOperationsCache = query - .ToDictionary(f => f.Item1, f => f.Item3 ?? f.Item2); - } - - return firstOperationsCache?.GetValueOrDefault(idWell); - } - - /// - public async Task> GetAsync( - WellOperationRequest request, - CancellationToken token) - { - var query = BuildQuery(request) - .AsNoTracking(); - var result = await query.ToArrayAsync(token); - return result; - } - - /// - public async Task> GetPageAsync( - WellOperationRequest request, - CancellationToken token) - { - var query = BuildQuery(request) - .AsNoTracking(); - - var result = new PaginationContainer - { - Skip = request.Skip ?? 0, - Take = request.Take ?? 32, - Count = await query.CountAsync(token).ConfigureAwait(false), - }; - - query = query - .Skip(result.Skip) - .Take(result.Take); - - result.Items = await query.ToArrayAsync(token); - return result; - } - - /// - public async Task GetOrDefaultAsync(int id, - CancellationToken token) - { - var entity = await db.WellOperations - .Include(s => s.WellSectionType) - .Include(s => s.OperationCategory) - .FirstOrDefaultAsync(e => e.Id == id, token) - .ConfigureAwait(false); - - if (entity is null) - return null; - - var timezone = wellService.GetTimezone(entity.IdWell); - - var dto = entity.Adapt(); - dto.WellSectionTypeName = entity.WellSectionType.Caption; - dto.DateStart = entity.DateStart.ToRemoteDateTime(timezone.Hours); - dto.CategoryName = entity.OperationCategory.Name; - return dto; - } - - /// - public async Task> GetGroupOperationsStatAsync( - WellOperationRequest request, - CancellationToken token) - { - // TODO: Rename controller method - request.OperationType = WellOperation.IdOperationTypeFact; - var query = BuildQuery(request); - var entities = await query - .Select(o => new - { - o.IdCategory, - DurationMinutes = o.DurationHours * 60, - DurationDepth = o.DepthEnd - o.DepthStart - }) - .ToListAsync(token); - var parentRelationDictionary = GetCategories(true) - .ToDictionary(c => c.Id, c => new - { - c.Name, - c.IdParent - }); - - var dtos = entities - .GroupBy(o => o.IdCategory) - .Select(g => new WellGroupOpertionDto - { - IdCategory = g.Key, - Category = parentRelationDictionary[g.Key].Name, - Count = g.Count(), - MinutesAverage = g.Average(o => o.DurationMinutes), - MinutesMin = g.Min(o => o.DurationMinutes), - MinutesMax = g.Max(o => o.DurationMinutes), - TotalMinutes = g.Sum(o => o.DurationMinutes), - DeltaDepth = g.Sum(o => o.DurationDepth), - IdParent = parentRelationDictionary[g.Key].IdParent - }); - - while (dtos.All(x => x.IdParent != null)) - { - dtos = dtos - .GroupBy(o => o.IdParent!) - .Select(g => { - var idCategory = g.Key ?? int.MinValue; - var category = parentRelationDictionary.GetValueOrDefault(idCategory); - var newDto = new WellGroupOpertionDto - { - IdCategory = idCategory, - Category = category?.Name ?? "unknown", - Count = g.Sum(o => o.Count), - DeltaDepth = g.Sum(o => o.DeltaDepth), - TotalMinutes = g.Sum(o => o.TotalMinutes), - Items = g.ToList(), - IdParent = category?.IdParent, - }; - return newDto; - }); - } - return dtos; - } - - /// - public async Task InsertRangeAsync( - IEnumerable wellOperationDtos, - CancellationToken token) - { - var firstOperation = wellOperationDtos - .FirstOrDefault(); - if (firstOperation is null) - return 0; - - var idWell = firstOperation.IdWell; - - var timezone = wellService.GetTimezone(idWell); - foreach (var dto in wellOperationDtos) - { - var entity = dto.Adapt(); - entity.Id = default; - entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours); - entity.IdWell = idWell; - db.WellOperations.Add(entity); - } - - return await db.SaveChangesAsync(token) - .ConfigureAwait(false); - } - - /// - public async Task UpdateAsync( - WellOperationDto dto, CancellationToken token) - { - var timezone = wellService.GetTimezone(dto.IdWell); - var entity = dto.Adapt(); - entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours); - db.WellOperations.Update(entity); - return await db.SaveChangesAsync(token) - .ConfigureAwait(false); - } - - /// - public async Task DeleteAsync(IEnumerable ids, - CancellationToken token) - { - var query = db.WellOperations.Where(e => ids.Contains(e.Id)); - db.WellOperations.RemoveRange(query); - return await db.SaveChangesAsync(token) - .ConfigureAwait(false); - } - - /// - /// В результате попрежнему требуется конвертировать дату - /// - /// - /// - private IQueryable BuildQuery(WellOperationRequest request) - { - var timezone = wellService.GetTimezone(request.IdWell); - var timeZoneOffset = TimeSpan.FromHours(timezone.Hours); - - var query = db.WellOperations - .Include(s => s.WellSectionType) - .Include(s => s.OperationCategory) - .Where(o => o.IdWell == request.IdWell); - - - if (request.OperationType.HasValue) - query = query.Where(e => e.IdType == request.OperationType.Value); - - if (request.SectionTypeIds?.Any() == true) - query = query.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType)); - - if (request.OperationCategoryIds?.Any() == true) - query = query.Where(e => request.OperationCategoryIds.Contains(e.IdCategory)); - - if (request.GeDepth.HasValue) - query = query.Where(e => e.DepthEnd >= request.GeDepth.Value); - - if (request.LeDepth.HasValue) - query = query.Where(e => e.DepthEnd <= request.LeDepth.Value); - - if (request.GeDate.HasValue) - { - var geDateOffset = request.GeDate.Value.ToUtcDateTimeOffset(timezone.Hours); - query = query.Where(e => e.DateStart >= geDateOffset); - } - - if (request.LtDate.HasValue) - { - var ltDateOffset = request.LtDate.Value.ToUtcDateTimeOffset(timezone.Hours); - query = query.Where(e => e.DateStart < ltDateOffset); - } - - var currentWellOperations = db.WellOperations - .Where(subOp => subOp.IdWell == request.IdWell); - - var wellOperationsWithCategoryNPT = currentWellOperations - .Where(subOp => subOp.IdType == 1) - .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory)); - - var result = query.Select(o => new WellOperationDto - { - Id = o.Id, - IdPlan = o.IdPlan, - IdType = o.IdType, - IdWell = o.IdWell, - IdWellSectionType = o.IdWellSectionType, - IdCategory = o.IdCategory, - IdParentCategory = o.OperationCategory.IdParent, - - CategoryName = o.OperationCategory.Name, - WellSectionTypeName = o.WellSectionType.Caption, - - DateStart = DateTime.SpecifyKind(o.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified), - DepthStart = o.DepthStart, - DepthEnd = o.DepthEnd, - DurationHours = o.DurationHours, - CategoryInfo = o.CategoryInfo, - Comment = o.Comment, - - NptHours = wellOperationsWithCategoryNPT - .Where(subOp => subOp.DateStart <= o.DateStart) - .Select(subOp => subOp.DurationHours) - .Sum(), - - Day = (o.DateStart - currentWellOperations - .Where(subOp => subOp.IdType == o.IdType) - .Where(subOp => subOp.DateStart <= o.DateStart) - .Min(subOp => subOp.DateStart)) - .TotalDays, - IdUser = o.IdUser, - LastUpdateDate = o.LastUpdateDate.ToOffset(TimeSpan.FromHours(timezone.Hours)) - }); - - if (request.SortFields?.Any() == true) - { - result = result.SortBy(request.SortFields); - } - else - { - result = result - .OrderBy(e => e.DateStart) - .ThenBy(e => e.DepthEnd) - .ThenBy(e => e.Id); - }; - - return result; - } + this.db = db; + this.memoryCache = memoryCache; + this.wellService = wellService; } + /// + public IEnumerable GetCategories(bool includeParents) + { + var categories = memoryCache + .GetOrCreateBasic(db.Set()); + + if (!includeParents) + { + var parentIds = categories + .Select(o => o.IdParent) + .Distinct(); + + categories = categories + .Where(o => !parentIds.Contains(o.Id)); + } + + var result = categories + .OrderBy(o => o.Name) + .Adapt>(); + + return result; + } + + /// + public IEnumerable GetSectionTypes() => + memoryCache + .GetOrCreateBasic(db.Set()) + .OrderBy(s => s.Order) + .Select(s => s.Adapt()); + + public async Task GetOperationsPlanAsync(int idWell, DateTime? currentDate, CancellationToken token) + { + var timezone = wellService.GetTimezone(idWell); + var request = new WellOperationRequest() + { + IdWell = idWell, + OperationType = WellOperation.IdOperationTypePlan, + }; + + var entities = await BuildQuery(request) + .AsNoTracking() + .ToArrayAsync(token) + .ConfigureAwait(false); + + var dateLastAssosiatedPlanOperation = await GetDateLastAssosiatedPlanOperationAsync(idWell, currentDate, timezone.Hours, token); + + var result = new WellOperationPlanDto() + { + WellOperationsPlan = entities, + DateLastAssosiatedPlanOperation = dateLastAssosiatedPlanOperation + }; + + return result; + } + + private async Task GetDateLastAssosiatedPlanOperationAsync( + int idWell, + DateTime? lessThenDate, + double timeZoneHours, + CancellationToken token) + { + if (lessThenDate is null) + return null; + + var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours); + var timeZoneOffset = TimeSpan.FromHours(timeZoneHours); + + var lastFactOperation = await db.WellOperations + .Where(o => o.IdWell == idWell) + .Where(o => o.IdType == WellOperation.IdOperationTypeFact) + .Where(o => o.IdPlan != null) + .Where(o => o.DateStart < currentDateOffset) + .Include(x => x.OperationPlan) + .OrderByDescending(x => x.DateStart) + .FirstOrDefaultAsync(token) + .ConfigureAwait(false); + + if (lastFactOperation is not null) + return DateTime.SpecifyKind(lastFactOperation.OperationPlan.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified); + return null; + } + + /// + public async Task> GetSectionsAsync(IEnumerable idsWells, CancellationToken token) + { + var cache = await memoryCache.GetOrCreateAsync(KeyCacheSections, async (entry) => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); + + var query = db.Set() + .GroupBy(operation => new + { + operation.IdWell, + operation.IdType, + operation.IdWellSectionType, + }) + .Select(group => new + { + group.Key.IdWell, + group.Key.IdType, + group.Key.IdWellSectionType, + + First = group + .OrderBy(operation => operation.DateStart) + .Select(operation => new + { + operation.DateStart, + operation.DepthStart, + }) + .First(), + + Last = group + .OrderByDescending(operation => operation.DateStart) + .Select(operation => new + { + operation.DateStart, + operation.DurationHours, + operation.DepthEnd, + }) + .First(), + }); + var dbData = await query.ToArrayAsync(token); + var sections = dbData.Select( + item => new SectionByOperationsDto + { + IdWell = item.IdWell, + IdType = item.IdType, + IdWellSectionType = item.IdWellSectionType, + + DateStart = item.First.DateStart, + DepthStart = item.First.DepthStart, + + DateEnd = item.Last.DateStart.AddHours(item.Last.DurationHours), + DepthEnd = item.Last.DepthEnd, + }) + .ToArray() + .AsEnumerable(); + + entry.Value = sections; + return sections; + }); + + var sections = cache.Where(s => idsWells.Contains(s.IdWell)); + return sections; + } + + /// + public DateTimeOffset? FirstOperationDate(int idWell) + { + var sections = GetSectionsAsync(new[] { idWell }, CancellationToken.None).Result; + var first = sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypeFact) + ?? sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypePlan); + + return first?.DateStart; + } + + /// + public async Task> GetAsync( + WellOperationRequest request, + CancellationToken token) + { + var query = BuildQuery(request) + .AsNoTracking(); + var result = await query.ToArrayAsync(token); + return result; + } + + /// + public async Task> GetPageAsync( + WellOperationRequest request, + CancellationToken token) + { + var query = BuildQuery(request) + .AsNoTracking(); + + var result = new PaginationContainer + { + Skip = request.Skip ?? 0, + Take = request.Take ?? 32, + Count = await query.CountAsync(token).ConfigureAwait(false), + }; + + query = query + .Skip(result.Skip) + .Take(result.Take); + + result.Items = await query.ToArrayAsync(token); + return result; + } + + /// + public async Task GetOrDefaultAsync(int id, + CancellationToken token) + { + var entity = await db.WellOperations + .Include(s => s.WellSectionType) + .Include(s => s.OperationCategory) + .FirstOrDefaultAsync(e => e.Id == id, token) + .ConfigureAwait(false); + + if (entity is null) + return null; + + var timezone = wellService.GetTimezone(entity.IdWell); + + var dto = entity.Adapt(); + dto.WellSectionTypeName = entity.WellSectionType.Caption; + dto.DateStart = entity.DateStart.ToRemoteDateTime(timezone.Hours); + dto.CategoryName = entity.OperationCategory.Name; + return dto; + } + + /// + public async Task> GetGroupOperationsStatAsync( + WellOperationRequest request, + CancellationToken token) + { + // TODO: Rename controller method + request.OperationType = WellOperation.IdOperationTypeFact; + var query = BuildQuery(request); + var entities = await query + .Select(o => new + { + o.IdCategory, + DurationMinutes = o.DurationHours * 60, + DurationDepth = o.DepthEnd - o.DepthStart + }) + .ToListAsync(token); + var parentRelationDictionary = GetCategories(true) + .ToDictionary(c => c.Id, c => new + { + c.Name, + c.IdParent + }); + + var dtos = entities + .GroupBy(o => o.IdCategory) + .Select(g => new WellGroupOpertionDto + { + IdCategory = g.Key, + Category = parentRelationDictionary[g.Key].Name, + Count = g.Count(), + MinutesAverage = g.Average(o => o.DurationMinutes), + MinutesMin = g.Min(o => o.DurationMinutes), + MinutesMax = g.Max(o => o.DurationMinutes), + TotalMinutes = g.Sum(o => o.DurationMinutes), + DeltaDepth = g.Sum(o => o.DurationDepth), + IdParent = parentRelationDictionary[g.Key].IdParent + }); + + while (dtos.All(x => x.IdParent != null)) + { + dtos = dtos + .GroupBy(o => o.IdParent!) + .Select(g => { + var idCategory = g.Key ?? int.MinValue; + var category = parentRelationDictionary.GetValueOrDefault(idCategory); + var newDto = new WellGroupOpertionDto + { + IdCategory = idCategory, + Category = category?.Name ?? "unknown", + Count = g.Sum(o => o.Count), + DeltaDepth = g.Sum(o => o.DeltaDepth), + TotalMinutes = g.Sum(o => o.TotalMinutes), + Items = g.ToList(), + IdParent = category?.IdParent, + }; + return newDto; + }); + } + return dtos; + } + + /// + public async Task InsertRangeAsync( + IEnumerable wellOperationDtos, + CancellationToken token) + { + var firstOperation = wellOperationDtos + .FirstOrDefault(); + if (firstOperation is null) + return 0; + + var idWell = firstOperation.IdWell; + + var timezone = wellService.GetTimezone(idWell); + foreach (var dto in wellOperationDtos) + { + var entity = dto.Adapt(); + entity.Id = default; + entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours); + entity.IdWell = idWell; + db.WellOperations.Add(entity); + } + + var result = await db.SaveChangesAsync(token); + if (result > 0) + memoryCache.Remove(KeyCacheSections); + return result; + + } + + /// + public async Task UpdateAsync( + WellOperationDto dto, CancellationToken token) + { + var timezone = wellService.GetTimezone(dto.IdWell); + var entity = dto.Adapt(); + entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours); + db.WellOperations.Update(entity); + + var result = await db.SaveChangesAsync(token); + if (result > 0) + memoryCache.Remove(KeyCacheSections); + return result; + } + + /// + public async Task DeleteAsync(IEnumerable ids, + CancellationToken token) + { + var query = db.WellOperations.Where(e => ids.Contains(e.Id)); + db.WellOperations.RemoveRange(query); + + var result = await db.SaveChangesAsync(token); + if (result > 0) + memoryCache.Remove(KeyCacheSections); + return result; + } + + /// + /// В результате попрежнему требуется конвертировать дату + /// + /// + /// + private IQueryable BuildQuery(WellOperationRequest request) + { + var timezone = wellService.GetTimezone(request.IdWell); + var timeZoneOffset = TimeSpan.FromHours(timezone.Hours); + + var query = db.WellOperations + .Include(s => s.WellSectionType) + .Include(s => s.OperationCategory) + .Where(o => o.IdWell == request.IdWell); + + + if (request.OperationType.HasValue) + query = query.Where(e => e.IdType == request.OperationType.Value); + + if (request.SectionTypeIds?.Any() == true) + query = query.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType)); + + if (request.OperationCategoryIds?.Any() == true) + query = query.Where(e => request.OperationCategoryIds.Contains(e.IdCategory)); + + if (request.GeDepth.HasValue) + query = query.Where(e => e.DepthEnd >= request.GeDepth.Value); + + if (request.LeDepth.HasValue) + query = query.Where(e => e.DepthEnd <= request.LeDepth.Value); + + if (request.GeDate.HasValue) + { + var geDateOffset = request.GeDate.Value.ToUtcDateTimeOffset(timezone.Hours); + query = query.Where(e => e.DateStart >= geDateOffset); + } + + if (request.LtDate.HasValue) + { + var ltDateOffset = request.LtDate.Value.ToUtcDateTimeOffset(timezone.Hours); + query = query.Where(e => e.DateStart < ltDateOffset); + } + + var currentWellOperations = db.WellOperations + .Where(subOp => subOp.IdWell == request.IdWell); + + var wellOperationsWithCategoryNPT = currentWellOperations + .Where(subOp => subOp.IdType == 1) + .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory)); + + var result = query.Select(o => new WellOperationDto + { + Id = o.Id, + IdPlan = o.IdPlan, + IdType = o.IdType, + IdWell = o.IdWell, + IdWellSectionType = o.IdWellSectionType, + IdCategory = o.IdCategory, + IdParentCategory = o.OperationCategory.IdParent, + + CategoryName = o.OperationCategory.Name, + WellSectionTypeName = o.WellSectionType.Caption, + + DateStart = DateTime.SpecifyKind(o.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified), + DepthStart = o.DepthStart, + DepthEnd = o.DepthEnd, + DurationHours = o.DurationHours, + CategoryInfo = o.CategoryInfo, + Comment = o.Comment, + + NptHours = wellOperationsWithCategoryNPT + .Where(subOp => subOp.DateStart <= o.DateStart) + .Select(subOp => subOp.DurationHours) + .Sum(), + + Day = (o.DateStart - currentWellOperations + .Where(subOp => subOp.IdType == o.IdType) + .Where(subOp => subOp.DateStart <= o.DateStart) + .Min(subOp => subOp.DateStart)) + .TotalDays, + IdUser = o.IdUser, + LastUpdateDate = o.LastUpdateDate.ToOffset(TimeSpan.FromHours(timezone.Hours)) + }); + + if (request.SortFields?.Any() == true) + { + result = result.SortBy(request.SortFields); + } + else + { + result = result + .OrderBy(e => e.DateStart) + .ThenBy(e => e.DepthEnd) + .ThenBy(e => e.Id); + }; + + return result; + } } diff --git a/AsbCloudInfrastructure/Services/WellboreService.cs b/AsbCloudInfrastructure/Services/WellboreService.cs index 5e0e439e..396243d6 100644 --- a/AsbCloudInfrastructure/Services/WellboreService.cs +++ b/AsbCloudInfrastructure/Services/WellboreService.cs @@ -35,7 +35,7 @@ public class WellboreService : IWellboreService } public async Task> GetWellboresAsync(WellboreRequest request, - CancellationToken cancellationToken) + CancellationToken token) { var wellbores = new List(request.Ids.Count()); var skip = request.Skip ?? 0; @@ -44,26 +44,43 @@ public class WellboreService : IWellboreService var sections = wellOperationRepository.GetSectionTypes() .ToDictionary(w => w.Id, w => w); - var ids = request.Ids.GroupBy(i => i.idWell); + var ids = request.Ids.GroupBy(i => i.idWell, i => i.idSection); + + var idsWells = request.Ids.Select(i => i.idWell); + + var allSections = await wellOperationRepository.GetSectionsAsync(idsWells, token); foreach (var id in ids) { - var well = await wellService.GetOrDefaultAsync(id.Key, cancellationToken); + var well = await wellService.GetOrDefaultAsync(id.Key, token); if (well is null) continue; - var wellOperations = await GetFactOperationsAsync(well.Id, id.Select(i => i.idSection), cancellationToken); - var groupedOperations = wellOperations.GroupBy(o => o.IdWellSectionType); - var wellWellbores = groupedOperations.Select(group => new WellboreDto { - Id = group.Key, - Name = sections[group.Key].Caption, + var wellTimezoneOffset = TimeSpan.FromHours(well.Timezone.Hours); + + var wellFactSections = allSections + .Where(section => section.IdWell == id.Key) + .Where(section => section.IdType == WellOperation.IdOperationTypeFact); + + var idsSections = id + .Where(i => i.HasValue) + .Select(i => i!.Value); + + if (idsSections.Any()) + wellFactSections = wellFactSections + .Where(section => idsSections.Contains(section.IdWellSectionType)); + + var wellWellbores = wellFactSections.Select(section => new WellboreDto { + Id = section.IdWellSectionType, + Name = sections[section.IdWellSectionType].Caption, Well = well.Adapt(), - DateStart = group.Min(operation => operation.DateStart).ToUtcDateTimeOffset(well.Timezone.Hours).ToOffset(TimeSpan.FromHours(well.Timezone.Hours)), - DateEnd = group.Max(operation => operation.DateStart.AddHours(operation.DurationHours)).ToUtcDateTimeOffset(well.Timezone.Hours).ToOffset(TimeSpan.FromHours(well.Timezone.Hours)), - DepthStart = group.Min(operation => operation.DepthStart), - DepthEnd = group.Max(operation => operation.DepthEnd), + DateStart = section.DateStart.ToOffset(wellTimezoneOffset), + DateEnd = section.DateEnd.ToOffset(wellTimezoneOffset), + DepthStart = section.DepthStart, + DepthEnd = section.DepthEnd, }); + wellbores.AddRange(wellWellbores); } @@ -71,22 +88,4 @@ public class WellboreService : IWellboreService .OrderBy(w => w.Well.Id).ThenBy(w => w.Id) .Skip(skip).Take(take); } - - private async Task> GetFactOperationsAsync(int idWell, IEnumerable idsSections, - CancellationToken cancellationToken) - { - var request = new WellOperationRequest - { - IdWell = idWell, - OperationType = WellOperation.IdOperationTypeFact, - SortFields = new[] { "DateStart asc" }, - }; - - request.SectionTypeIds = idsSections.All(i => i.HasValue) - ? idsSections.Select(i => i!.Value) - : null; - - return (await wellOperationRepository.GetAsync(request, cancellationToken)) - .OrderBy(o => o.DateStart); - } } \ No newline at end of file From 673cb8960cd92ae5037700f362575618e1d387d8 Mon Sep 17 00:00:00 2001 From: Frolov-Nikita Date: Sun, 8 Oct 2023 13:09:09 +0500 Subject: [PATCH 4/7] =?UTF-8?q?WorkBase=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=86=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Background/BackgroundWorker.cs | 26 +--- AsbCloudInfrastructure/Background/WorkBase.cs | 113 ++++++++++++++++-- .../Background/WorkPeriodic.cs | 2 +- .../Background/WorkQueue.cs | 2 - AsbCloudInfrastructure/Background/todo.md | 9 +- .../DrillingProgram/DrillingProgramService.cs | 1 - .../BackgroundWorkerServiceTest.cs | 2 +- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index 7c88fade..4ba3d535 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -14,10 +14,10 @@ namespace AsbCloudInfrastructure.Background { private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); - private static readonly TimeSpan exceptionHandleTimeout = TimeSpan.FromSeconds(2); private readonly IServiceProvider serviceProvider; private readonly WorkQueue workQueue = new WorkQueue(); public string? CurrentWorkId; + public BackgroundWorker(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; @@ -58,36 +58,18 @@ namespace AsbCloudInfrastructure.Background { while (!token.IsCancellationRequested) { - var dateStart = DateTime.Now; var work = workQueue.Pop(); if (work is null) { await Task.Delay(executePeriod, token); continue; } + CurrentWorkId = work.Id; using var scope = serviceProvider.CreateScope(); - - try - { - Trace.TraceInformation($"Backgroud work:\"{work.Id}\" start."); - var task = work.ActionAsync(work.Id, scope.ServiceProvider, token); - await task.WaitAsync(work.Timeout, token); - work.ExecutionTime = DateTime.Now - dateStart; - Trace.TraceInformation($"Backgroud work:\"{work.Id}\" done. ExecutionTime: {work.ExecutionTime:hh\\:mm\\:ss\\.fff}"); - } - catch (Exception exception) - { - Trace.TraceError($"Backgroud work:\"{work.Id}\" throw exception: {exception.Message}"); - if (work.OnErrorAsync is not null) - { - using var task = Task.Run( - async () => await work.OnErrorAsync(work.Id, exception, token), - token); - await task.WaitAsync(exceptionHandleTimeout, token); - } - } + await work.Start(scope.ServiceProvider, token); + CurrentWorkId = null; await Task.Delay(minDelay, token); } diff --git a/AsbCloudInfrastructure/Background/WorkBase.cs b/AsbCloudInfrastructure/Background/WorkBase.cs index e8adf04c..ef238a85 100644 --- a/AsbCloudInfrastructure/Background/WorkBase.cs +++ b/AsbCloudInfrastructure/Background/WorkBase.cs @@ -1,10 +1,11 @@ using System; +using System.Diagnostics; +using System.Text; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Background { - /// /// Класс разовой работы. /// Разовая работа приоритетнее периодической. @@ -14,7 +15,7 @@ namespace AsbCloudInfrastructure.Background /// /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. /// - public string Id { get; private set; } + public string Id { get; } /// /// Делегат работы. @@ -36,7 +37,7 @@ namespace AsbCloudInfrastructure.Background /// /// /// - internal Func ActionAsync { get; set; } + internal Func ActionAsync { get; } /// /// Делегат обработки ошибки. @@ -50,20 +51,118 @@ namespace AsbCloudInfrastructure.Background public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); /// - /// Фактическое время успешного выполнения работы + /// Продолжительность последнего выполнения /// - public TimeSpan? ExecutionTime { get; internal set; } + public TimeSpan? LastExecutionTime { get; private set; } + + /// + /// Текущее время выполнения + /// + public TimeSpan? CurrentExecutionTime => CurrentStart.HasValue + ? DateTime.Now - CurrentStart.Value + : null; /// /// Время последнего запуска /// - public DateTime LastStart { get; set; } + public DateTime? CurrentStart { get; private set; } + + /// + /// Текстовое описание того, что происходит в задаче. + /// + public string? CurrentStatus { get; private set; } + + /// + /// Время последнего запуска + /// + public DateTime? CurrentStatusUpdate { get; private set; } + + /// + /// Последняя ошибка + /// + public string? LastErrorMessage { get; private set; } + + /// + /// Дата последнего запуска + /// + public DateTime? LastStart { get; private set; } + + /// + /// Дата последней ошибки + /// + public DateTime? LastError { get; private set; } + + /// + /// Дата завершения последнего выполнения + /// + public DateTime? LastComplete { get; private set; } + + /// + /// Кол-во завершений + /// + public int CountComplete { get; private set; } + + /// + /// Кол-во ошибок + /// + public int CountErrors { get; private set; } + + private string WorkNameForTrace => $"Backgroud work:\"{Id}\""; public WorkBase(string id, Func actionAsync) { Id = id; ActionAsync = actionAsync; } - } + + public async Task Start(IServiceProvider services, CancellationToken token) + { + CurrentStart = DateTime.Now; + LastStart = DateTime.Now; + try + { + SetStatus(" start"); + var task = ActionAsync(Id, services, token); + await task.WaitAsync(Timeout, token); + LastComplete = DateTime.Now; + CountComplete++; + SetStatus($" {task.Status}. ExecutionTime: {CurrentExecutionTime:hh\\:mm\\:ss\\.fff}"); + } + catch (Exception exception) + { + SetError(exception.Message); + if (OnErrorAsync is not null) + { + var task = Task.Run( + async () => await OnErrorAsync(Id, exception, token), + token); + await task.WaitAsync(Timeout, token); + } + } + LastExecutionTime = CurrentExecutionTime; + CurrentStart = null; + SetStatus(null); + } + + protected void SetStatus(string? newStatus) + { + CurrentStatus = newStatus; + if (newStatus is not null) + { + CurrentStatusUpdate = DateTime.Now; + Trace.TraceInformation($"{WorkNameForTrace} state: {newStatus}"); + } + else + CurrentStatusUpdate = null; + } + + private void SetError(string? errorMessage) + { + CountErrors++; + LastErrorMessage = errorMessage; + LastError = DateTime.Now; + Trace.TraceError($"{WorkNameForTrace} throw exception[{CountErrors}]: {errorMessage}"); + } + } } diff --git a/AsbCloudInfrastructure/Background/WorkPeriodic.cs b/AsbCloudInfrastructure/Background/WorkPeriodic.cs index cbd34fec..5c2b0e23 100644 --- a/AsbCloudInfrastructure/Background/WorkPeriodic.cs +++ b/AsbCloudInfrastructure/Background/WorkPeriodic.cs @@ -18,7 +18,7 @@ namespace AsbCloudInfrastructure.Background /// /// Время следующего запуска /// - public DateTime NextStart => LastStart + Period; + public DateTime NextStart => LastStart??DateTime.MinValue + Period; /// /// Класс периодической работы diff --git a/AsbCloudInfrastructure/Background/WorkQueue.cs b/AsbCloudInfrastructure/Background/WorkQueue.cs index ce77fa94..e925916b 100644 --- a/AsbCloudInfrastructure/Background/WorkQueue.cs +++ b/AsbCloudInfrastructure/Background/WorkQueue.cs @@ -4,7 +4,6 @@ using System.Linq; namespace AsbCloudInfrastructure.Background { - /// /// /// Очередь работ @@ -90,7 +89,6 @@ namespace AsbCloudInfrastructure.Background if (work is null || work.NextStart > DateTime.Now) return null; - work.LastStart = DateTime.Now; return work; } diff --git a/AsbCloudInfrastructure/Background/todo.md b/AsbCloudInfrastructure/Background/todo.md index f30ce6c4..b8b30852 100644 --- a/AsbCloudInfrastructure/Background/todo.md +++ b/AsbCloudInfrastructure/Background/todo.md @@ -1,5 +1,12 @@ # -- +- . + - , + - , + - , - . . - / - / + +# +- dto +- , . diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index adb410d6..9dc20155 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -541,7 +541,6 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram var work = new WorkBase(workId, workAction) { - ExecutionTime = TimeSpan.FromMinutes(1), OnErrorAsync = onErrorAction }; diff --git a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs index 133f6b36..f4a03f2f 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs @@ -88,7 +88,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(10); - Assert.True(work.ExecutionTime > TimeSpan.Zero); + Assert.True(work.LastExecutionTime > TimeSpan.Zero); } [Fact] From 724c7b0cd8028ff59c0b925a130d899a5b4fe87c Mon Sep 17 00:00:00 2001 From: Frolov-Nikita Date: Sun, 8 Oct 2023 19:45:21 +0500 Subject: [PATCH 5/7] BackgroudWork Add onprogres callback --- .../Background/BackgroudWorkDto.cs | 170 ++++++++++++++++++ .../Background/BackgroundWorker.cs | 2 +- .../Background/OrderedList.cs | 41 +++++ AsbCloudInfrastructure/Background/WorkBase.cs | 149 ++++----------- .../Background/WorkPeriodic.cs | 13 +- .../Background/{WorkQueue.cs => WorkStore.cs} | 42 +++-- .../OperationDetectionWorkFactory.cs | 8 +- .../DrillingProgram/DrillingProgramService.cs | 3 +- .../EmailNotificationTransportService.cs | 5 +- .../LimitingParameterBackgroundService.cs | 5 +- .../Services/ReportService.cs | 6 +- .../Services/SAUB/TelemetryDataCache.cs | 10 +- .../SubsystemOperationTimeCalcWorkFactory.cs | 5 +- .../Services/WellInfoService.cs | 7 +- AsbCloudInfrastructure/Startup.cs | 2 +- .../SignalRNotificationTransportService.cs | 4 +- 16 files changed, 321 insertions(+), 151 deletions(-) create mode 100644 AsbCloudInfrastructure/Background/BackgroudWorkDto.cs create mode 100644 AsbCloudInfrastructure/Background/OrderedList.cs rename AsbCloudInfrastructure/Background/{WorkQueue.cs => WorkStore.cs} (70%) diff --git a/AsbCloudInfrastructure/Background/BackgroudWorkDto.cs b/AsbCloudInfrastructure/Background/BackgroudWorkDto.cs new file mode 100644 index 00000000..873217eb --- /dev/null +++ b/AsbCloudInfrastructure/Background/BackgroudWorkDto.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics; + +namespace AsbCloudInfrastructure.Background +{ + public class BackgroudWorkDto + { + /// + /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. + /// + public string Id { get; init; } = null!; + + public class CurrentStateInfo + { + private string state = "start"; + + /// + /// Время последнего запуска + /// + public DateTime Start { get; } = DateTime.Now; + + /// + /// Текущее время выполнения + /// + public TimeSpan ExecutionTime => DateTime.Now - Start; + + /// + /// Текстовое описание того, что происходит в задаче. + /// + public string State + { + get => state; + internal set + { + state = value; + StateUpdate = DateTime.Now; + } + } + + public double Progress { get; internal set; } = 0; + + /// + /// Время последнего запуска + /// + public DateTime StateUpdate { get; private set; } = DateTime.Now; + } + + public class LastErrorInfo: LastCompleteInfo + { + public LastErrorInfo(CurrentStateInfo state, string errorText) + : base(state) + { + ErrorText = errorText; + } + + /// + /// Последняя ошибка + /// + public string ErrorText { get; init; } = null!; + } + + public class LastCompleteInfo + { + /// + /// Дата запуска + /// + public DateTime Start {get; init;} + + /// + /// Дата завершения + /// + public DateTime End { get; init; } + + /// + /// Продолжительность последнего выполнения + /// + public TimeSpan ExecutionTime => End - Start; + + /// + /// Состояние на момент завершения + /// + public string State { get; init; } + + public LastCompleteInfo(CurrentStateInfo state) + { + Start = state.Start; + End = DateTime.Now; + State = state.State; + } + } + + /// + /// Текущее состояние + /// + public CurrentStateInfo? CurrentState { get; private set; } + + /// + /// Последняя ошибка + /// + public LastErrorInfo? LastError { get; private set; } + + /// + /// Последняя завершенная + /// + public LastCompleteInfo? LastComplete { get; private set; } + + /// + /// Кол-во запусков + /// + public int CountStart { get; private set; } + + /// + /// Кол-во завершений + /// + public int CountComplete { get; private set; } + + /// + /// Кол-во ошибок + /// + public int CountErrors { get; private set; } + + /// + /// Максимально допустимое время выполнения работы + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); + + private string WorkNameForTrace => $"Backgroud work:\"{Id}\""; + + protected void SetStatusStart() + { + CurrentState = new(); + CountStart++; + Trace.TraceInformation($"{WorkNameForTrace} state: starting"); + } + + protected void UpdateStatus(string newState, double? progress) + { + if (CurrentState is null) + return; + + CurrentState.State = newState; + if (progress.HasValue) + CurrentState.Progress = progress.Value; + + Trace.TraceInformation($"{WorkNameForTrace} state: {newState}"); + } + + protected void SetStatusComplete(System.Threading.Tasks.TaskStatus status) + { + if (CurrentState is null) + return; + + LastComplete = new (CurrentState); + CurrentState = null; + CountComplete++; + Trace.TraceInformation($"{WorkNameForTrace} state: completed"); + } + + protected void SetLastError(string errorMessage) + { + if (CurrentState is null) + return; + + LastError = new LastErrorInfo(CurrentState, errorMessage); + CurrentState = null; + CountErrors++; + Trace.TraceError($"{WorkNameForTrace} throw exception[{CountErrors}]: {errorMessage}"); + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index 4ba3d535..fad61edf 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -15,7 +15,7 @@ namespace AsbCloudInfrastructure.Background private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); private readonly IServiceProvider serviceProvider; - private readonly WorkQueue workQueue = new WorkQueue(); + private readonly WorkStore workQueue = new WorkStore(); public string? CurrentWorkId; public BackgroundWorker(IServiceProvider serviceProvider) diff --git a/AsbCloudInfrastructure/Background/OrderedList.cs b/AsbCloudInfrastructure/Background/OrderedList.cs new file mode 100644 index 00000000..1f583a82 --- /dev/null +++ b/AsbCloudInfrastructure/Background/OrderedList.cs @@ -0,0 +1,41 @@ +using System.Linq; + +namespace System.Collections.Generic +{ + public class OrderedList: IEnumerable, ICollection + where T : notnull + { + private readonly List list = new List(); + + private readonly Func keySelector; + private readonly bool isDescending = false; + + private IOrderedEnumerable OrdredList => isDescending + ? list.OrderByDescending(keySelector) + : list.OrderBy(keySelector); + + public int Count => list.Count; + + public bool IsReadOnly => false; + + public OrderedList(Func keySelector, bool isDescending = false) + { + this.keySelector = keySelector; + this.isDescending = isDescending; + } + + public void Add(T item) => list.Add(item); + + public void Clear()=> list.Clear(); + + public bool Contains(T item)=> list.Contains(item); + + public void CopyTo(T[] array, int arrayIndex)=> list.CopyTo(array, arrayIndex); + + public bool Remove(T item)=> list.Remove(item); + + public IEnumerator GetEnumerator() => OrdredList.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/AsbCloudInfrastructure/Background/WorkBase.cs b/AsbCloudInfrastructure/Background/WorkBase.cs index ef238a85..e1b83053 100644 --- a/AsbCloudInfrastructure/Background/WorkBase.cs +++ b/AsbCloudInfrastructure/Background/WorkBase.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -10,14 +8,23 @@ namespace AsbCloudInfrastructure.Background /// Класс разовой работы. /// Разовая работа приоритетнее периодической. /// - public class WorkBase + public class WorkBase : BackgroudWorkDto { - /// - /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. - /// - public string Id { get; } + internal Func, CancellationToken, Task> ActionAsync { get; } /// + /// Делегат обработки ошибки. + /// Не должен выполняться долго. + /// + public Func? OnErrorAsync { get; set; } + + public TimeSpan OnErrorHandlerTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Базовая работа + /// + /// + /// /// Делегат работы. /// /// Параметры: @@ -31,138 +38,50 @@ namespace AsbCloudInfrastructure.Background /// Поставщик сервисов /// /// + /// Action<string, double?> + /// on progress callback. String - new state text. double? - optional progress 0-100%. + /// + /// /// CancellationToken /// Токен отмены задачи /// /// /// - /// - internal Func ActionAsync { get; } - - /// - /// Делегат обработки ошибки. - /// Не должен выполняться долго. - /// - public Func? OnErrorAsync { get; set; } - - /// - /// максимально допустимое время выполнения работы - /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// Продолжительность последнего выполнения - /// - public TimeSpan? LastExecutionTime { get; private set; } - - /// - /// Текущее время выполнения - /// - public TimeSpan? CurrentExecutionTime => CurrentStart.HasValue - ? DateTime.Now - CurrentStart.Value - : null; - - /// - /// Время последнего запуска - /// - public DateTime? CurrentStart { get; private set; } - - /// - /// Текстовое описание того, что происходит в задаче. - /// - public string? CurrentStatus { get; private set; } - - /// - /// Время последнего запуска - /// - public DateTime? CurrentStatusUpdate { get; private set; } - - /// - /// Последняя ошибка - /// - public string? LastErrorMessage { get; private set; } - - /// - /// Дата последнего запуска - /// - public DateTime? LastStart { get; private set; } - - /// - /// Дата последней ошибки - /// - public DateTime? LastError { get; private set; } - - /// - /// Дата завершения последнего выполнения - /// - public DateTime? LastComplete { get; private set; } - - /// - /// Кол-во завершений - /// - public int CountComplete { get; private set; } - - /// - /// Кол-во ошибок - /// - public int CountErrors { get; private set; } - - private string WorkNameForTrace => $"Backgroud work:\"{Id}\""; - - public WorkBase(string id, Func actionAsync) + /// + public WorkBase(string id, Func, CancellationToken, Task> actionAsync) { Id = id; ActionAsync = actionAsync; } - public async Task Start(IServiceProvider services, CancellationToken token) + /// + /// Запустить работу + /// + /// + /// + /// True - susess, False = Fail + public async Task Start(IServiceProvider services, CancellationToken token) { - CurrentStart = DateTime.Now; - LastStart = DateTime.Now; + SetStatusStart(); try { - SetStatus(" start"); - var task = ActionAsync(Id, services, token); + var task = ActionAsync(Id, services, UpdateStatus, token); await task.WaitAsync(Timeout, token); - LastComplete = DateTime.Now; - CountComplete++; - SetStatus($" {task.Status}. ExecutionTime: {CurrentExecutionTime:hh\\:mm\\:ss\\.fff}"); + SetStatusComplete(task.Status); + return true; } catch (Exception exception) { - SetError(exception.Message); + SetLastError(exception.Message); if (OnErrorAsync is not null) { var task = Task.Run( async () => await OnErrorAsync(Id, exception, token), token); - await task.WaitAsync(Timeout, token); + await task.WaitAsync(OnErrorHandlerTimeout, token); } } - - LastExecutionTime = CurrentExecutionTime; - CurrentStart = null; - SetStatus(null); - } - - protected void SetStatus(string? newStatus) - { - CurrentStatus = newStatus; - if (newStatus is not null) - { - CurrentStatusUpdate = DateTime.Now; - Trace.TraceInformation($"{WorkNameForTrace} state: {newStatus}"); - } - else - CurrentStatusUpdate = null; - } - - private void SetError(string? errorMessage) - { - CountErrors++; - LastErrorMessage = errorMessage; - LastError = DateTime.Now; - Trace.TraceError($"{WorkNameForTrace} throw exception[{CountErrors}]: {errorMessage}"); + return false; } } } diff --git a/AsbCloudInfrastructure/Background/WorkPeriodic.cs b/AsbCloudInfrastructure/Background/WorkPeriodic.cs index 5c2b0e23..71d580ed 100644 --- a/AsbCloudInfrastructure/Background/WorkPeriodic.cs +++ b/AsbCloudInfrastructure/Background/WorkPeriodic.cs @@ -18,7 +18,16 @@ namespace AsbCloudInfrastructure.Background /// /// Время следующего запуска /// - public DateTime NextStart => LastStart??DateTime.MinValue + Period; + public DateTime NextStart + { + get + { + var lastStart = LastComplete?.Start ?? DateTime.MinValue; + if (LastError?.Start > lastStart) + lastStart = LastError.Start; + return lastStart + Period; + } + } /// /// Класс периодической работы @@ -26,7 +35,7 @@ namespace AsbCloudInfrastructure.Background /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки /// Делегат работы /// Период выполнения задачи - public WorkPeriodic(string id, Func actionAsync, TimeSpan period) + public WorkPeriodic(string id, Func, CancellationToken, Task> actionAsync, TimeSpan period) : base(id, actionAsync) { Period = period; diff --git a/AsbCloudInfrastructure/Background/WorkQueue.cs b/AsbCloudInfrastructure/Background/WorkStore.cs similarity index 70% rename from AsbCloudInfrastructure/Background/WorkQueue.cs rename to AsbCloudInfrastructure/Background/WorkStore.cs index e925916b..590ad8a6 100644 --- a/AsbCloudInfrastructure/Background/WorkQueue.cs +++ b/AsbCloudInfrastructure/Background/WorkStore.cs @@ -10,11 +10,25 @@ namespace AsbCloudInfrastructure.Background /// /// Не периодические задачи будут возвращаться первыми, как самые приоритетные. /// - class WorkQueue + class WorkStore { - private Queue Primary = new(8); private readonly List Periodic = new(8); + /// + /// Работы выполняемые один раз + /// + public Queue RunOnceQueue { get; } = new(8); + + /// + /// Работы выполняемые периодически + /// + public IOrderedEnumerable Periodics => Periodic.OrderBy(work => work.NextStart); + + /// + /// Завершывшиеся с ошибкой + /// + public CyclycArray Falled { get; } = new(16); + /// /// Добавление работы. /// @@ -25,8 +39,8 @@ namespace AsbCloudInfrastructure.Background if (Periodic.Any(w => w.Id == work.Id)) throw new ArgumentException("work.Id is not unique", nameof(work)); - if (Primary.Any(w => w.Id == work.Id)) - throw new ArgumentException("work.Id is not unique", nameof(work)); + //if (Primary.Any(w => w.Id == work.Id)) + // throw new ArgumentException("work.Id is not unique", nameof(work)); if (work is WorkPeriodic workPeriodic) { @@ -34,7 +48,7 @@ namespace AsbCloudInfrastructure.Background return; } - Primary.Enqueue(work); + //Primary.Enqueue(work); } /// @@ -51,19 +65,19 @@ namespace AsbCloudInfrastructure.Background return true; } - var work = Primary.FirstOrDefault(w => w.Id == id); - if (work is not null) - { - Primary = new Queue(Primary.Where(w => w.Id != id)); - return true; - } + //var work = Primary.FirstOrDefault(w => w.Id == id); + //if (work is not null) + //{ + // Primary = new Queue(Primary.Where(w => w.Id != id)); + // return true; + //} return false; } public bool Contains(string id) { - var result = Periodic.Any(w => w.Id == id) || Primary.Any(w => w.Id == id); + var result = false;//Periodic.Any(w => w.Id == id) || Primary.Any(w => w.Id == id); return result; } @@ -82,8 +96,8 @@ namespace AsbCloudInfrastructure.Background /// public WorkBase? Pop() { - if (Primary.Any()) - return Primary.Dequeue(); + //if (Primary.Any()) + // return Primary.Dequeue(); var work = GetNextPeriodic(); if (work is null || work.NextStart > DateTime.Now) diff --git a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs index da171f13..350ad6b7 100644 --- a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs +++ b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs @@ -46,7 +46,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations } // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) + private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) { using var db = serviceProvider.GetRequiredService(); @@ -75,10 +75,14 @@ namespace AsbCloudInfrastructure.Services.DetectOperations }); var affected = 0; + var count = joinedlastDetectedDates.Count(); + var i = 0; foreach (var item in joinedlastDetectedDates) { var stopwatch = Stopwatch.StartNew(); - var newOperations = await DetectOperationsAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); + var startDate = item.LastDate ?? DateTimeOffset.MinValue; + onProgress($"start detecting telemetry: {item.IdTelemetry} from {startDate}", i++ / count); + var newOperations = await DetectOperationsAsync(item.IdTelemetry, startDate, db, token); stopwatch.Stop(); if (newOperations.Any()) { diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index 9dc20155..cf41ab7c 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -519,11 +519,12 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf"; var convertedFilesDir = Path.Combine(Path.GetTempPath(), "drillingProgram", $"{well.Cluster}_{well.Caption}"); var tempResultFilePath = Path.Combine(convertedFilesDir, resultFileName); - var workAction = async (string workId, IServiceProvider serviceProvider, CancellationToken token) => + var workAction = async (string workId, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) => { var context = serviceProvider.GetRequiredService(); var fileService = serviceProvider.GetRequiredService(); var files = state.Parts.Select(p => fileService.GetUrl(p.File!)); + onProgress($"Start converting {files.Count()} files to PDF.", null); await ConvertToPdf.GetConverteAndMergedFileAsync(files, tempResultFilePath, convertedFilesDir, token); await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token); }; diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index 26eed2be..412a4354 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -10,6 +10,7 @@ using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Services.Notifications; using AsbCloudInfrastructure.Background; +using DocumentFormat.OpenXml.Presentation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -70,9 +71,9 @@ namespace AsbCloudInfrastructure.Services.Email return Task.WhenAll(tasks); } - private Func MakeEmailSendWorkAction(NotificationDto notification) + private Func, CancellationToken, Task> MakeEmailSendWorkAction(NotificationDto notification) { - return async (_, serviceProvider, token) => + return async (_, serviceProvider, onProgress, token) => { var notificationRepository = serviceProvider.GetRequiredService(); var userRepository = serviceProvider.GetRequiredService(); diff --git a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs index 7c875e70..6c4f94c1 100644 --- a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs +++ b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs @@ -28,7 +28,7 @@ namespace AsbCloudInfrastructure.Services } // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) + private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) { using var db = serviceProvider.GetRequiredService(); var lastDetectedDates = await db.LimitingParameter @@ -55,8 +55,11 @@ namespace AsbCloudInfrastructure.Services inner.SingleOrDefault()?.LastDate, }); + var count = telemetryLastDetectedDates.Count(); + var i = 0; foreach (var item in telemetryLastDetectedDates) { + onProgress($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++/count); var newLimitingParameters = await GetLimitingParameterAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); if (newLimitingParameters?.Any() == true) { diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index eca8f84e..95851c74 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -51,7 +51,7 @@ namespace AsbCloudInfrastructure.Services var workId = $"create report by wellid:{idWell} for userid:{idUser} requested at {DateTime.Now}"; - var workAction = async (string id, IServiceProvider serviceProvider, CancellationToken token) => + var workAction = async (string id, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) => { using var context = serviceProvider.GetRequiredService(); var fileService = serviceProvider.GetRequiredService(); @@ -64,7 +64,9 @@ namespace AsbCloudInfrastructure.Services generator.OnProgress += (s, e) => { - progressHandler.Invoke(e.Adapt(), id); + var arg = e.Adapt(); + onProgress(arg.Operation?? string.Empty, arg.Progress); + progressHandler.Invoke(arg, id); }; generator.Make(reportFileName); diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs index d1a7fea1..8868fc24 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -48,9 +48,9 @@ namespace AsbCloudInfrastructure.Services.SAUB instance = new TelemetryDataCache(); var worker = provider.GetRequiredService(); var workId = $"Telemetry cache loading from DB {typeof(TEntity).Name}"; - var work = new WorkBase(workId, async (workId, provider, token) => { + var work = new WorkBase(workId, async (workId, provider, onProgress, token) => { var db = provider.GetRequiredService(); - await instance.InitializeCacheFromDBAsync(db, token); + await instance.InitializeCacheFromDBAsync(db, onProgress, token); }); worker.Push(work); @@ -150,7 +150,7 @@ namespace AsbCloudInfrastructure.Services.SAUB return new DatesRangeDto { From = from.Value, To = to }; } - private async Task InitializeCacheFromDBAsync(IAsbCloudDbContext db, CancellationToken token) + private async Task InitializeCacheFromDBAsync(IAsbCloudDbContext db, Action onProgress, CancellationToken token) where TEntity : class, AsbCloudDb.Model.ITelemetryData { if (isLoading) @@ -168,6 +168,8 @@ namespace AsbCloudInfrastructure.Services.SAUB .Where(well => well.IdTelemetry != null) .ToArrayAsync(token); + var count = wells.Count(); + var i = 0; foreach (Well well in wells) { var capacity = well.IdState == 1 @@ -176,7 +178,7 @@ namespace AsbCloudInfrastructure.Services.SAUB var idTelemetry = well.IdTelemetry!.Value; var hoursOffset = well.Timezone.Hours; - + // TODO: remove traces System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}>: Loading for well: {well.Cluster?.Caption}/{well.Caption} (capacity:{capacity}) idTelemetry:{idTelemetry}"); var cacheItem = await GetOrDefaultCacheDataFromDbAsync(db, idTelemetry, capacity, hoursOffset, token); if(cacheItem is not null) diff --git a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs index d5b86803..bbbc1fb5 100644 --- a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs +++ b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs @@ -36,7 +36,7 @@ namespace AsbCloudInfrastructure.Services.Subsystems } // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) + private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) { using var db = serviceProvider.GetRequiredService(); @@ -64,8 +64,11 @@ namespace AsbCloudInfrastructure.Services.Subsystems inner.SingleOrDefault()?.LastDate, }); + var count = telemetryLastDetectedDates.Count(); + var i = 0; foreach (var item in telemetryLastDetectedDates) { + onProgress($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++ / count); var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); if (newOperationsSaub?.Any() == true) { diff --git a/AsbCloudInfrastructure/Services/WellInfoService.cs b/AsbCloudInfrastructure/Services/WellInfoService.cs index eea573cc..0d6951b2 100644 --- a/AsbCloudInfrastructure/Services/WellInfoService.cs +++ b/AsbCloudInfrastructure/Services/WellInfoService.cs @@ -62,7 +62,7 @@ namespace AsbCloudInfrastructure.Services return workPeriodic; } - private static async Task WorkAction(string workName, IServiceProvider serviceProvider, CancellationToken token) + private static async Task WorkAction(string workName, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) { var wellService = serviceProvider.GetRequiredService(); var operationsStatService = serviceProvider.GetRequiredService(); @@ -90,11 +90,12 @@ namespace AsbCloudInfrastructure.Services var subsystemStat = await subsystemOperationTimeService .GetStatByActiveWells(wellsIds, token); - + var count = wells.Count(); + var i = 0; WellMapInfo = wells.Select(well => { var wellMapInfo = well.Adapt(); wellMapInfo.IdState = well.IdState; - + onProgress($"Start updating info by well({well.Id}): {well.Caption}", i++ / count); double? currentDepth = null; TelemetryDataSaubDto? lastSaubTelemetry = null; diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index c1c3cc8f..54d5d080 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -51,7 +51,7 @@ namespace AsbCloudInfrastructure static WorkPeriodic MakeMemoryMonitoringWork() { var workId = "Memory monitoring"; - var workAction = (string _, IServiceProvider _, CancellationToken _) => { + var workAction = (string _, IServiceProvider _, Action _, CancellationToken _) => { var bytes = GC.GetTotalMemory(false); var bytesString = FromatBytes(bytes); System.Diagnostics.Trace.TraceInformation($"Total memory allocated is {bytesString} bytes. DbContext count is:{AsbCloudDbContext.ReferenceCount}"); diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index b418c0b5..9633eb1c 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -39,9 +39,9 @@ public class SignalRNotificationTransportService : INotificationTransportService return Task.CompletedTask; } - private Func MakeSignalRSendWorkAction(IEnumerable notifications) + private Func, CancellationToken, Task> MakeSignalRSendWorkAction(IEnumerable notifications) { - return async (_, serviceProvider, cancellationToken) => + return async (_, serviceProvider, onProgress, cancellationToken) => { var notificationPublisher = serviceProvider.GetRequiredService(); From 1560c6bf91c75bb0f5cc35f0977a34ff018b862a Mon Sep 17 00:00:00 2001 From: Frolov-Nikita Date: Sun, 8 Oct 2023 21:20:28 +0500 Subject: [PATCH 6/7] Refactor webStore --- .../Data}/BackgroudWorkDto.cs | 41 +++- .../Background/BackgroundWorker.cs | 97 ++++------ .../Background/{WorkBase.cs => Work.cs} | 11 +- .../Background/WorkPeriodic.cs | 63 +++--- .../Background/WorkStore.cs | 183 +++++++----------- .../OperationDetectionWorkFactory.cs | 19 +- .../DrillingProgram/DrillingProgramService.cs | 8 +- .../EmailNotificationTransportService.cs | 6 +- .../LimitingParameterBackgroundService.cs | 7 +- .../Services/ReportService.cs | 4 +- .../Services/SAUB/TelemetryDataCache.cs | 6 +- .../SubsystemOperationTimeCalcWorkFactory.cs | 13 +- .../Services/WellInfoService.cs | 13 +- AsbCloudInfrastructure/Startup.cs | 17 +- .../DrillingProgramServiceTest.cs | 2 +- .../SignalRNotificationTransportService.cs | 6 +- 16 files changed, 219 insertions(+), 277 deletions(-) rename {AsbCloudInfrastructure/Background => AsbCloudApp/Data}/BackgroudWorkDto.cs (79%) rename AsbCloudInfrastructure/Background/{WorkBase.cs => Work.cs} (88%) diff --git a/AsbCloudInfrastructure/Background/BackgroudWorkDto.cs b/AsbCloudApp/Data/BackgroudWorkDto.cs similarity index 79% rename from AsbCloudInfrastructure/Background/BackgroudWorkDto.cs rename to AsbCloudApp/Data/BackgroudWorkDto.cs index 873217eb..64451d26 100644 --- a/AsbCloudInfrastructure/Background/BackgroudWorkDto.cs +++ b/AsbCloudApp/Data/BackgroudWorkDto.cs @@ -1,8 +1,11 @@ using System; using System.Diagnostics; -namespace AsbCloudInfrastructure.Background +namespace AsbCloudApp.Data { + /// + /// Информация о фоновой работе + /// public class BackgroudWorkDto { /// @@ -10,6 +13,9 @@ namespace AsbCloudInfrastructure.Background /// public string Id { get; init; } = null!; + /// + /// Класс описания состояния + /// public class CurrentStateInfo { private string state = "start"; @@ -37,6 +43,9 @@ namespace AsbCloudInfrastructure.Background } } + /// + /// Прогресс + /// public double Progress { get; internal set; } = 0; /// @@ -45,9 +54,12 @@ namespace AsbCloudInfrastructure.Background public DateTime StateUpdate { get; private set; } = DateTime.Now; } - public class LastErrorInfo: LastCompleteInfo + /// + /// Инфо о последней ошибке + /// + public class LastErrorInfo : LastCompleteInfo { - public LastErrorInfo(CurrentStateInfo state, string errorText) + public LastErrorInfo(CurrentStateInfo state, string errorText) : base(state) { ErrorText = errorText; @@ -59,12 +71,15 @@ namespace AsbCloudInfrastructure.Background public string ErrorText { get; init; } = null!; } + /// + /// Инфо о последнем завершении + /// public class LastCompleteInfo { /// /// Дата запуска /// - public DateTime Start {get; init;} + public DateTime Start { get; init; } /// /// Дата завершения @@ -123,9 +138,12 @@ namespace AsbCloudInfrastructure.Background /// Максимально допустимое время выполнения работы /// public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); - + private string WorkNameForTrace => $"Backgroud work:\"{Id}\""; + /// + /// Обновления состояния при запуске работы + /// protected void SetStatusStart() { CurrentState = new(); @@ -133,6 +151,9 @@ namespace AsbCloudInfrastructure.Background Trace.TraceInformation($"{WorkNameForTrace} state: starting"); } + /// + /// Обновления состояния в процессе работы + /// protected void UpdateStatus(string newState, double? progress) { if (CurrentState is null) @@ -145,17 +166,23 @@ namespace AsbCloudInfrastructure.Background Trace.TraceInformation($"{WorkNameForTrace} state: {newState}"); } - protected void SetStatusComplete(System.Threading.Tasks.TaskStatus status) + /// + /// Обновления состояния при успешном завершении работы + /// + protected void SetStatusComplete() { if (CurrentState is null) return; - LastComplete = new (CurrentState); + LastComplete = new(CurrentState); CurrentState = null; CountComplete++; Trace.TraceInformation($"{WorkNameForTrace} state: completed"); } + /// + /// Обновления состояния при ошибке в работе + /// protected void SetLastError(string errorMessage) { if (CurrentState is null) diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index fad61edf..f4ef8c0e 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -1,78 +1,49 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Background +namespace AsbCloudInfrastructure.Background; + +/// +/// Сервис для фонового выполнения работы +/// +public class BackgroundWorker : BackgroundService { - /// - /// Сервис для фонового выполнения работы - /// - public class BackgroundWorker : BackgroundService + private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); + private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); + private readonly IServiceProvider serviceProvider; + + public WorkStore WorkStore { get; } = new WorkStore(); + public Work? CurrentWork; + + public BackgroundWorker(IServiceProvider serviceProvider) { - private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); - private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); - private readonly IServiceProvider serviceProvider; - private readonly WorkStore workQueue = new WorkStore(); - public string? CurrentWorkId; + this.serviceProvider = serviceProvider; + } - public BackgroundWorker(IServiceProvider serviceProvider) + protected override async Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) { - this.serviceProvider = serviceProvider; - } - - /// - /// Добавление задачи в очередь. - /// Не периодические задачи будут выполняться вперед. - /// - /// - /// Id mast be unique - public void Push(WorkBase work) - { - workQueue.Push(work); - } - - /// - /// Проверяет наличие работы с указанным Id - /// - /// - /// - public bool Contains(string id) - { - return workQueue.Contains(id); - } - - /// - /// Удаление работы по ID - /// - /// - /// - public bool Delete(string id) - { - return workQueue.Delete(id); - } - - protected override async Task ExecuteAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) + var work = WorkStore.GetNext(); + if (work is null) { - var work = workQueue.Pop(); - if (work is null) - { - await Task.Delay(executePeriod, token); - continue; - } - - CurrentWorkId = work.Id; - using var scope = serviceProvider.CreateScope(); - - await work.Start(scope.ServiceProvider, token); - - CurrentWorkId = null; - await Task.Delay(minDelay, token); + await Task.Delay(executePeriod, token); + continue; } + + CurrentWork = work; + using var scope = serviceProvider.CreateScope(); + + var result = await work.Start(scope.ServiceProvider, token); + + if (!result) + WorkStore.Falled.Add(work); + + CurrentWork = null; + await Task.Delay(minDelay, token); } } } diff --git a/AsbCloudInfrastructure/Background/WorkBase.cs b/AsbCloudInfrastructure/Background/Work.cs similarity index 88% rename from AsbCloudInfrastructure/Background/WorkBase.cs rename to AsbCloudInfrastructure/Background/Work.cs index e1b83053..84135707 100644 --- a/AsbCloudInfrastructure/Background/WorkBase.cs +++ b/AsbCloudInfrastructure/Background/Work.cs @@ -1,4 +1,5 @@ -using System; +using AsbCloudApp.Data; +using System; using System.Threading; using System.Threading.Tasks; @@ -8,9 +9,9 @@ namespace AsbCloudInfrastructure.Background /// Класс разовой работы. /// Разовая работа приоритетнее периодической. /// - public class WorkBase : BackgroudWorkDto + public class Work : BackgroudWorkDto { - internal Func, CancellationToken, Task> ActionAsync { get; } + private Func, CancellationToken, Task> ActionAsync { get; } /// /// Делегат обработки ошибки. @@ -48,7 +49,7 @@ namespace AsbCloudInfrastructure.Background /// /// /// - public WorkBase(string id, Func, CancellationToken, Task> actionAsync) + public Work(string id, Func, CancellationToken, Task> actionAsync) { Id = id; ActionAsync = actionAsync; @@ -67,7 +68,7 @@ namespace AsbCloudInfrastructure.Background { var task = ActionAsync(Id, services, UpdateStatus, token); await task.WaitAsync(Timeout, token); - SetStatusComplete(task.Status); + SetStatusComplete(); return true; } catch (Exception exception) diff --git a/AsbCloudInfrastructure/Background/WorkPeriodic.cs b/AsbCloudInfrastructure/Background/WorkPeriodic.cs index 71d580ed..4cd22af6 100644 --- a/AsbCloudInfrastructure/Background/WorkPeriodic.cs +++ b/AsbCloudInfrastructure/Background/WorkPeriodic.cs @@ -1,45 +1,42 @@ using System; -using System.Threading; -using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Background +namespace AsbCloudInfrastructure.Background; + +/// +/// Класс периодической работы. +/// +public class WorkPeriodic { + public Work Work { get; } /// - /// Класс периодической работы. + /// Период выполнения задачи /// - public class WorkPeriodic : WorkBase + public TimeSpan Period { get; set; } + + /// + /// Время следующего запуска + /// + public DateTime NextStart { - /// - /// Период выполнения задачи - /// - public TimeSpan Period { get; set; } - - /// - /// Время следующего запуска - /// - public DateTime NextStart + get { - get - { - var lastStart = LastComplete?.Start ?? DateTime.MinValue; - if (LastError?.Start > lastStart) - lastStart = LastError.Start; - return lastStart + Period; - } - } - - /// - /// Класс периодической работы - /// - /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки - /// Делегат работы - /// Период выполнения задачи - public WorkPeriodic(string id, Func, CancellationToken, Task> actionAsync, TimeSpan period) - : base(id, actionAsync) - { - Period = period; + var lastStart = Work.LastComplete?.Start ?? DateTime.MinValue; + if (Work.LastError?.Start > lastStart) + lastStart = Work.LastError.Start; + return lastStart + Period; } } + /// + /// Класс периодической работы + /// + /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки + /// Делегат работы + /// Период выполнения задачи + public WorkPeriodic(Work work, TimeSpan period) + { + Work = work; + Period = period; + } } diff --git a/AsbCloudInfrastructure/Background/WorkStore.cs b/AsbCloudInfrastructure/Background/WorkStore.cs index 590ad8a6..dbb1e464 100644 --- a/AsbCloudInfrastructure/Background/WorkStore.cs +++ b/AsbCloudInfrastructure/Background/WorkStore.cs @@ -2,118 +2,85 @@ using System.Collections.Generic; using System.Linq; -namespace AsbCloudInfrastructure.Background +namespace AsbCloudInfrastructure.Background; + +/// +/// +/// Очередь работ +/// +/// Не периодические задачи будут возвращаться первыми, как самые приоритетные. +/// +public class WorkStore { + private readonly List periodics = new(8); + /// - /// - /// Очередь работ - /// - /// Не периодические задачи будут возвращаться первыми, как самые приоритетные. + /// Список периодических задач /// - class WorkStore + public IEnumerable Periodics => periodics; + /// + /// Работы выполняемые один раз + /// + public Queue RunOnceQueue { get; private set; } = new(8); + + /// + /// Завершывшиеся с ошибкой + /// + public CyclycArray Falled { get; } = new(16); + + public void AddPeriodic(Work work, TimeSpan period) { - private readonly List Periodic = new(8); - - /// - /// Работы выполняемые один раз - /// - public Queue RunOnceQueue { get; } = new(8); - - /// - /// Работы выполняемые периодически - /// - public IOrderedEnumerable Periodics => Periodic.OrderBy(work => work.NextStart); - - /// - /// Завершывшиеся с ошибкой - /// - public CyclycArray Falled { get; } = new(16); - - /// - /// Добавление работы. - /// - /// - /// Id mast be unique - public void Push(WorkBase work) - { - if (Periodic.Any(w => w.Id == work.Id)) - throw new ArgumentException("work.Id is not unique", nameof(work)); - - //if (Primary.Any(w => w.Id == work.Id)) - // throw new ArgumentException("work.Id is not unique", nameof(work)); - - if (work is WorkPeriodic workPeriodic) - { - Periodic.Add(workPeriodic); - return; - } - - //Primary.Enqueue(work); - } - - /// - /// Удаление работы по ID - /// - /// - /// - public bool Delete(string id) - { - var workPeriodic = Periodic.FirstOrDefault(w => w.Id == id); - if (workPeriodic is not null) - { - Periodic.Remove(workPeriodic); - return true; - } - - //var work = Primary.FirstOrDefault(w => w.Id == id); - //if (work is not null) - //{ - // Primary = new Queue(Primary.Where(w => w.Id != id)); - // return true; - //} - - return false; - } - - public bool Contains(string id) - { - var result = false;//Periodic.Any(w => w.Id == id) || Primary.Any(w => w.Id == id); - return result; - } - - /// - /// - /// Возвращает приоритетную задачу. - /// - /// - /// Если приоритетные закончились, то ищет ближайшую периодическую. - /// Если до старта ближайшей периодической работы меньше 20 сек, - /// то этой задаче устанавливается время последнего запуска в now и она возвращается. - /// Если больше 20 сек, то возвращается null. - /// - /// - /// - /// - public WorkBase? Pop() - { - //if (Primary.Any()) - // return Primary.Dequeue(); - - var work = GetNextPeriodic(); - if (work is null || work.NextStart > DateTime.Now) - return null; - - return work; - } - - private WorkPeriodic? GetNextPeriodic() - { - var work = Periodic - .OrderBy(w => w.NextStart) - .ThenByDescending(w => w.Period) - .FirstOrDefault(); - return work; - } + var periodic = new WorkPeriodic(work, period); + periodics.Add(periodic); } + /// + /// Удаление работы по ID из одноразовой очереди + /// + /// + /// + public bool TryRemoveFromRunOnceQueue(string id) + { + var work = RunOnceQueue.FirstOrDefault(w => w.Id == id); + if (work is not null) + { + RunOnceQueue = new Queue(RunOnceQueue.Where(w => w.Id != id)); + return true; + } + + return false; + } + + /// + /// + /// Возвращает приоритетную задачу. + /// + /// + /// Если приоритетные закончились, то ищет ближайшую периодическую. + /// Если до старта ближайшей периодической работы меньше 20 сек, + /// то этой задаче устанавливается время последнего запуска в now и она возвращается. + /// Если больше 20 сек, то возвращается null. + /// + /// + /// + /// + public Work? GetNext() + { + if (RunOnceQueue.Any()) + return RunOnceQueue.Dequeue(); + + var work = GetNextPeriodic(); + if (work is null || work.NextStart > DateTime.Now) + return null; + + return work.Work; + } + + private WorkPeriodic? GetNextPeriodic() + { + var work = Periodics + .OrderBy(w => w.NextStart) + .FirstOrDefault(); + return work; + } } diff --git a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs index 350ad6b7..0c9bb3dd 100644 --- a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs +++ b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs @@ -16,7 +16,6 @@ namespace AsbCloudInfrastructure.Services.DetectOperations public static class OperationDetectionWorkFactory { private const string workId = "Operation detection"; - private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); private static string progress = "no progress"; private static readonly DetectorAbstract[] detectors = new DetectorAbstract[] @@ -32,18 +31,16 @@ namespace AsbCloudInfrastructure.Services.DetectOperations //new DetectorTemplatingWhileDrilling(), }; - public static WorkPeriodic MakeWork() - { - var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod); - workPeriodic.Timeout = TimeSpan.FromSeconds(15 * 60); - workPeriodic.OnErrorAsync = (id, exception, token) => + public static Work MakeWork() => new Work(workId, WorkAction) { - var text = $"work {id}, when {progress}, throw error:{exception.Message}"; - Trace.TraceWarning(text); - return Task.CompletedTask; + Timeout = TimeSpan.FromMinutes(20), + OnErrorAsync = (id, exception, token) => + { + var text = $"work {id}, when {progress}, throw error:{exception.Message}"; + Trace.TraceWarning(text); + return Task.CompletedTask; + } }; - return workPeriodic; - } // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index cf41ab7c..402a89ed 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -513,7 +513,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram if (state.IdState == idStateCreating) { var workId = MakeWorkId(idWell); - if (!backgroundWorker.Contains(workId)) + if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) { var well = (await wellService.GetOrDefaultAsync(idWell, token))!; var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf"; @@ -540,12 +540,12 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram return Task.CompletedTask; }; - var work = new WorkBase(workId, workAction) + var work = new Work(workId, workAction) { OnErrorAsync = onErrorAction }; - backgroundWorker.Push(work); + backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); } } } @@ -559,7 +559,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token) { var workId = MakeWorkId(idWell); - backgroundWorker.Delete(workId); + backgroundWorker.WorkStore.TryRemoveFromRunOnceQueue(workId); var filesIds = await context.Files .Where(f => f.IdWell == idWell && diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index 412a4354..33f011bf 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -52,12 +52,12 @@ namespace AsbCloudInfrastructure.Services.Email } var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message); - if (!backgroundWorker.Contains(workId)) + if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w=>w.Id==workId)) { var workAction = MakeEmailSendWorkAction(notification); - var work = new WorkBase(workId, workAction); - backgroundWorker.Push(work); + var work = new Work(workId, workAction); + backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); } return Task.CompletedTask; diff --git a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs index 6c4f94c1..07a17369 100644 --- a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs +++ b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs @@ -16,16 +16,11 @@ namespace AsbCloudInfrastructure.Services internal static class LimitingParameterCalcWorkFactory { private const string workId = "Limiting parameter calc"; - private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); - public static WorkPeriodic MakeWork() - { - var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod) + public static Work MakeWork() => new Work(workId, WorkAction) { Timeout = TimeSpan.FromMinutes(30) }; - return workPeriodic; - } // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index 95851c74..f3ab6cb8 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -94,8 +94,8 @@ namespace AsbCloudInfrastructure.Services context.SaveChanges(); }; - var work = new WorkBase(workId, workAction); - backgroundWorkerService.Push(work); + var work = new Work(workId, workAction); + backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work); progressHandler.Invoke(new ReportProgressDto { diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs index 8868fc24..daaa2dcf 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -48,12 +48,12 @@ namespace AsbCloudInfrastructure.Services.SAUB instance = new TelemetryDataCache(); var worker = provider.GetRequiredService(); var workId = $"Telemetry cache loading from DB {typeof(TEntity).Name}"; - var work = new WorkBase(workId, async (workId, provider, onProgress, token) => { + var work = new Work(workId, async (workId, provider, onProgress, token) => { var db = provider.GetRequiredService(); await instance.InitializeCacheFromDBAsync(db, onProgress, token); }); - worker.Push(work); + worker.WorkStore.RunOnceQueue.Enqueue(work); } instance.provider = provider; return instance; @@ -168,7 +168,7 @@ namespace AsbCloudInfrastructure.Services.SAUB .Where(well => well.IdTelemetry != null) .ToArrayAsync(token); - var count = wells.Count(); + var count = wells.Length; var i = 0; foreach (Well well in wells) { diff --git a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs index bbbc1fb5..451ca633 100644 --- a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs +++ b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs @@ -18,7 +18,6 @@ namespace AsbCloudInfrastructure.Services.Subsystems internal static class SubsystemOperationTimeCalcWorkFactory { private const string workId = "Subsystem operation time calc"; - private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); private const int idSubsytemTorqueMaster = 65537; private const int idSubsytemSpinMaster = 65536; @@ -26,14 +25,10 @@ namespace AsbCloudInfrastructure.Services.Subsystems private const int idSubsystemAPDSlide = 12; private const int idSubsytemMse = 2; - public static WorkPeriodic MakeWork() - { - var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod) - { - Timeout = TimeSpan.FromMinutes(30) - }; - return workPeriodic; - } + public static Work MakeWork() => new Work(workId, WorkAction) + { + Timeout = TimeSpan.FromMinutes(20) + }; // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/WellInfoService.cs b/AsbCloudInfrastructure/Services/WellInfoService.cs index 0d6951b2..80456db9 100644 --- a/AsbCloudInfrastructure/Services/WellInfoService.cs +++ b/AsbCloudInfrastructure/Services/WellInfoService.cs @@ -29,7 +29,6 @@ namespace AsbCloudInfrastructure.Services } private const string workId = "Well statistics update"; - private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); private readonly TelemetryDataCache telemetryDataSaubCache; private readonly TelemetryDataCache telemetryDataSpinCache; @@ -53,14 +52,10 @@ namespace AsbCloudInfrastructure.Services this.gtrRepository = gtrRepository; } - public static WorkPeriodic MakeWork() - { - var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod) - { - Timeout = TimeSpan.FromMinutes(30) - }; - return workPeriodic; - } + public static Work MakeWork() => new Work(workId, WorkAction) + { + Timeout = TimeSpan.FromMinutes(20) + }; private static async Task WorkAction(string workName, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) { diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index 54d5d080..604a4f69 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -15,7 +15,6 @@ using AsbCloudInfrastructure.Services.SAUB; namespace AsbCloudInfrastructure { - public class Startup { public static void BeforeRunHandler(IHost host) @@ -32,11 +31,11 @@ namespace AsbCloudInfrastructure _ = provider.GetRequiredService>(); var backgroundWorker = provider.GetRequiredService(); - backgroundWorker.Push(WellInfoService.MakeWork()); - backgroundWorker.Push(OperationDetectionWorkFactory.MakeWork()); - backgroundWorker.Push(SubsystemOperationTimeCalcWorkFactory.MakeWork()); - backgroundWorker.Push(LimitingParameterCalcWorkFactory.MakeWork()); - backgroundWorker.Push(MakeMemoryMonitoringWork()); + backgroundWorker.WorkStore.AddPeriodic(WellInfoService.MakeWork(), TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(OperationDetectionWorkFactory.MakeWork(), TimeSpan.FromMinutes(15)); + backgroundWorker.WorkStore.AddPeriodic(SubsystemOperationTimeCalcWorkFactory.MakeWork(), TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(LimitingParameterCalcWorkFactory.MakeWork(), TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); var notificationBackgroundWorker = provider.GetRequiredService(); @@ -48,17 +47,15 @@ namespace AsbCloudInfrastructure }); } - static WorkPeriodic MakeMemoryMonitoringWork() + static Work MakeMemoryMonitoringWork() { - var workId = "Memory monitoring"; var workAction = (string _, IServiceProvider _, Action _, CancellationToken _) => { var bytes = GC.GetTotalMemory(false); var bytesString = FromatBytes(bytes); System.Diagnostics.Trace.TraceInformation($"Total memory allocated is {bytesString} bytes. DbContext count is:{AsbCloudDbContext.ReferenceCount}"); return Task.CompletedTask; }; - var workPeriod = TimeSpan.FromMinutes(1); - var work = new WorkPeriodic(workId, workAction, workPeriod); + var work = new Work("Memory monitoring", workAction); return work; } diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs index 12bc81a2..54f5d19c 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs @@ -366,7 +366,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); Assert.Equal(2, state.IdState); - backgroundWorkerMock.Verify(s => s.Push(It.IsAny())); + backgroundWorkerMock.Verify(s => s.Push(It.IsAny())); } [Fact] diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index 9633eb1c..143055e5 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -29,12 +29,12 @@ public class SignalRNotificationTransportService : INotificationTransportService { var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x"); - if (backgroundWorker.Contains(workId)) + if (backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) return Task.CompletedTask; var workAction = MakeSignalRSendWorkAction(notifications); - var work = new WorkBase(workId, workAction); - backgroundWorker.Push(work); + var work = new Work(workId, workAction); + backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); return Task.CompletedTask; } From c28315b795488bb04febae5a302027e33b53f1ab Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Mon, 9 Oct 2023 13:12:45 +0500 Subject: [PATCH 7/7] Add backgroundController for monitoring works states --- ...ckgroudWorkDto.cs => BackgroundWorkDto.cs} | 11 +- AsbCloudApp/Data/WellboreDto.cs | 5 +- .../Notifications/NotificationService.cs | 1 - .../Background/BackgroundWorker.cs | 2 +- AsbCloudInfrastructure/Background/Work.cs | 160 +++++----- .../Background/WorkStore.cs | 25 +- .../AutoGeneratedDailyReportService.cs | 2 +- .../OperationDetectionWorkFactory.cs | 164 ---------- .../WorkOperationDetection.cs | 157 +++++++++ .../DrillingProgram/DrillingProgramService.cs | 7 +- .../EmailNotificationTransportService.cs | 2 +- .../LimitingParameterBackgroundService.cs | 149 --------- .../Services/ReportService.cs | 2 +- .../Services/SAUB/TelemetryDataCache.cs | 17 +- .../SubsystemOperationTimeCalcWorkFactory.cs | 298 ------------------ .../WorkSubsystemOperationTimeCalc.cs | 294 +++++++++++++++++ .../Services/WellInfoService.cs | 173 +++++----- .../Services/WorkLimitingParameterCalc.cs | 144 +++++++++ AsbCloudInfrastructure/Startup.cs | 10 +- .../UserConnectionsLimitMiddlwareTest.cs | 16 +- AsbCloudWebApi/Controllers/BackgroundWork.cs | 34 ++ .../SignalRNotificationTransportService.cs | 2 +- ConsoleApp1/DebugWellOperationsStatService.cs | 40 --- 23 files changed, 867 insertions(+), 848 deletions(-) rename AsbCloudApp/Data/{BackgroudWorkDto.cs => BackgroundWorkDto.cs} (95%) delete mode 100644 AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs create mode 100644 AsbCloudInfrastructure/Services/DetectOperations/WorkOperationDetection.cs delete mode 100644 AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs delete mode 100644 AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs create mode 100644 AsbCloudInfrastructure/Services/Subsystems/WorkSubsystemOperationTimeCalc.cs create mode 100644 AsbCloudInfrastructure/Services/WorkLimitingParameterCalc.cs create mode 100644 AsbCloudWebApi/Controllers/BackgroundWork.cs delete mode 100644 ConsoleApp1/DebugWellOperationsStatService.cs diff --git a/AsbCloudApp/Data/BackgroudWorkDto.cs b/AsbCloudApp/Data/BackgroundWorkDto.cs similarity index 95% rename from AsbCloudApp/Data/BackgroudWorkDto.cs rename to AsbCloudApp/Data/BackgroundWorkDto.cs index 64451d26..b2a3d161 100644 --- a/AsbCloudApp/Data/BackgroudWorkDto.cs +++ b/AsbCloudApp/Data/BackgroundWorkDto.cs @@ -6,7 +6,7 @@ namespace AsbCloudApp.Data /// /// Информация о фоновой работе /// - public class BackgroudWorkDto + public class BackgroundWorkDto { /// /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. @@ -59,6 +59,11 @@ namespace AsbCloudApp.Data /// public class LastErrorInfo : LastCompleteInfo { + /// + /// + /// + /// + /// public LastErrorInfo(CurrentStateInfo state, string errorText) : base(state) { @@ -96,6 +101,10 @@ namespace AsbCloudApp.Data /// public string State { get; init; } + /// + /// ctor + /// + /// public LastCompleteInfo(CurrentStateInfo state) { Start = state.Start; diff --git a/AsbCloudApp/Data/WellboreDto.cs b/AsbCloudApp/Data/WellboreDto.cs index dbd9b697..c153b14f 100644 --- a/AsbCloudApp/Data/WellboreDto.cs +++ b/AsbCloudApp/Data/WellboreDto.cs @@ -7,7 +7,10 @@ namespace AsbCloudApp.Data; /// public class WellboreDto { - public WellWithTimezoneDto Well { get; set; } + /// + /// Скважина + /// + public WellWithTimezoneDto Well { get; set; } = null!; /// /// Идентификатор diff --git a/AsbCloudApp/Services/Notifications/NotificationService.cs b/AsbCloudApp/Services/Notifications/NotificationService.cs index 85c521e3..4537f04e 100644 --- a/AsbCloudApp/Services/Notifications/NotificationService.cs +++ b/AsbCloudApp/Services/Notifications/NotificationService.cs @@ -90,7 +90,6 @@ public class NotificationService /// Отправка уведомлений, которые не были отправлены /// /// - /// /// /// public async Task RenotifyAsync(int idUser, CancellationToken cancellationToken) diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index f4ef8c0e..cacb5a60 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -40,7 +40,7 @@ public class BackgroundWorker : BackgroundService var result = await work.Start(scope.ServiceProvider, token); if (!result) - WorkStore.Falled.Add(work); + WorkStore.Felled.Add(work); CurrentWork = null; await Task.Delay(minDelay, token); diff --git a/AsbCloudInfrastructure/Background/Work.cs b/AsbCloudInfrastructure/Background/Work.cs index 84135707..9372343b 100644 --- a/AsbCloudInfrastructure/Background/Work.cs +++ b/AsbCloudInfrastructure/Background/Work.cs @@ -3,86 +3,96 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Background +namespace AsbCloudInfrastructure.Background; + +/// +/// Класс разовой работы. +/// Разовая работа приоритетнее периодической. +/// +public abstract class Work : BackgroundWorkDto { - /// - /// Класс разовой работы. - /// Разовая работа приоритетнее периодической. - /// - public class Work : BackgroudWorkDto + private sealed class WorkBase : Work { private Func, CancellationToken, Task> ActionAsync { get; } - - /// - /// Делегат обработки ошибки. - /// Не должен выполняться долго. - /// - public Func? OnErrorAsync { get; set; } - - public TimeSpan OnErrorHandlerTimeout { get; set; } = TimeSpan.FromSeconds(5); - - /// - /// Базовая работа - /// - /// - /// - /// Делегат работы. - /// - /// Параметры: - /// - /// - /// string - /// Id Идентификатор работы - /// - /// - /// IServiceProvider - /// Поставщик сервисов - /// - /// - /// Action<string, double?> - /// on progress callback. String - new state text. double? - optional progress 0-100%. - /// - /// - /// CancellationToken - /// Токен отмены задачи - /// - /// - /// - /// - public Work(string id, Func, CancellationToken, Task> actionAsync) + public WorkBase(string id, Func, CancellationToken, Task> actionAsync) + : base(id) { - Id = id; ActionAsync = actionAsync; } - - /// - /// Запустить работу - /// - /// - /// - /// True - susess, False = Fail - public async Task Start(IServiceProvider services, CancellationToken token) - { - SetStatusStart(); - try - { - var task = ActionAsync(Id, services, UpdateStatus, token); - await task.WaitAsync(Timeout, token); - SetStatusComplete(); - return true; - } - catch (Exception exception) - { - SetLastError(exception.Message); - if (OnErrorAsync is not null) - { - var task = Task.Run( - async () => await OnErrorAsync(Id, exception, token), - token); - await task.WaitAsync(OnErrorHandlerTimeout, token); - } - } - return false; - } + + protected override Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token) + => ActionAsync(id, services, onProgressCallback, token); } + + /// + /// Делегат обработки ошибки. + /// Не должен выполняться долго. + /// + public Func? OnErrorAsync { get; set; } + + /// + /// макс продолжительность обработки исключения + /// + public TimeSpan OnErrorHandlerTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Базовая работа + /// + /// + public Work(string id) + { + Id = id; + } + + /// + /// Создать работу на основе делегата + /// + /// + /// + /// + [Obsolete("Use implement Work class")] + public static Work CreateByDelegate(string id, Func, CancellationToken, Task> actionAsync) + { + return new WorkBase(id, actionAsync); + } + + /// + /// Запустить работу + /// + /// + /// + /// True - success, False = fail + public async Task Start(IServiceProvider services, CancellationToken token) + { + SetStatusStart(); + try + { + var task = Action(Id, services, UpdateStatus, token); + await task.WaitAsync(Timeout, token); + SetStatusComplete(); + return true; + } + catch (Exception exception) + { + SetLastError(exception.Message); + if (OnErrorAsync is not null) + { + var task = Task.Run( + async () => await OnErrorAsync(Id, exception, token), + token); + await task.WaitAsync(OnErrorHandlerTimeout, token); + } + } + return false; + } + + /// + /// делегат фоновой работы + /// + /// Идентификатор работы + /// Поставщик сервисов + /// on progress callback. String - new state text. double? - optional progress 0-100% + /// + /// + protected abstract Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token); } diff --git a/AsbCloudInfrastructure/Background/WorkStore.cs b/AsbCloudInfrastructure/Background/WorkStore.cs index dbb1e464..b57fc104 100644 --- a/AsbCloudInfrastructure/Background/WorkStore.cs +++ b/AsbCloudInfrastructure/Background/WorkStore.cs @@ -18,16 +18,35 @@ public class WorkStore /// Список периодических задач /// public IEnumerable Periodics => periodics; + /// /// Работы выполняемые один раз /// public Queue RunOnceQueue { get; private set; } = new(8); /// - /// Завершывшиеся с ошибкой + /// Завершившиеся с ошибкой /// - public CyclycArray Falled { get; } = new(16); - + public CyclycArray Felled { get; } = new(16); + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// + public void AddPeriodic(TimeSpan period) + where T : Work, new() + { + var work = new T(); + var periodic = new WorkPeriodic(work, period); + periodics.Add(periodic); + } + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// public void AddPeriodic(Work work, TimeSpan period) { var periodic = new WorkPeriodic(work, period); diff --git a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs index 2f402285..03ed6e07 100644 --- a/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs +++ b/AsbCloudInfrastructure/Services/AutoGeneratedDailyReports/AutoGeneratedDailyReportService.cs @@ -226,7 +226,7 @@ public class AutoGeneratedDailyReportService : IAutoGeneratedDailyReportService .OrderBy(w => w.DateStart); } - private Task?> GetSubsystemStatsAsync(int idWell, DateTime startDate, + private Task> GetSubsystemStatsAsync(int idWell, DateTime startDate, DateTime finishDate, CancellationToken cancellationToken) { var request = new SubsystemOperationTimeRequest diff --git a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs deleted file mode 100644 index 0c9bb3dd..00000000 --- a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs +++ /dev/null @@ -1,164 +0,0 @@ -using AsbCloudDb.Model; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AsbCloudInfrastructure.Services.DetectOperations.Detectors; -using AsbCloudInfrastructure.Background; -using Microsoft.Extensions.DependencyInjection; - -namespace AsbCloudInfrastructure.Services.DetectOperations -{ - - public static class OperationDetectionWorkFactory - { - private const string workId = "Operation detection"; - private static string progress = "no progress"; - - private static readonly DetectorAbstract[] detectors = new DetectorAbstract[] - { - new DetectorRotor(), - new DetectorSlide(), - //new DetectorDevelopment(), - //new DetectorTemplating(), - new DetectorSlipsTime(), - //new DetectorStaticSurveying(), - //new DetectorFlashingBeforeConnection(), - //new DetectorFlashing(), - //new DetectorTemplatingWhileDrilling(), - }; - - public static Work MakeWork() => new Work(workId, WorkAction) - { - Timeout = TimeSpan.FromMinutes(20), - OnErrorAsync = (id, exception, token) => - { - var text = $"work {id}, when {progress}, throw error:{exception.Message}"; - Trace.TraceWarning(text); - return Task.CompletedTask; - } - }; - - // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) - { - using var db = serviceProvider.GetRequiredService(); - - var lastDetectedDates = await db.DetectedOperations - .GroupBy(o => o.IdTelemetry) - .Select(g => new - { - IdTelemetry = g.Key, - LastDate = g.Max(o => o.DateEnd) - }) - .ToListAsync(token); - - var telemetryIds = await db.Telemetries - .Where(t => t.Info != null && t.TimeZone != null) - .Select(t => t.Id) - .ToListAsync(token); - - var joinedlastDetectedDates = telemetryIds - .GroupJoin(lastDetectedDates, - t => t, - o => o.IdTelemetry, - (outer, inner) => new - { - IdTelemetry = outer, - inner.SingleOrDefault()?.LastDate, - }); - - var affected = 0; - var count = joinedlastDetectedDates.Count(); - var i = 0; - foreach (var item in joinedlastDetectedDates) - { - var stopwatch = Stopwatch.StartNew(); - var startDate = item.LastDate ?? DateTimeOffset.MinValue; - onProgress($"start detecting telemetry: {item.IdTelemetry} from {startDate}", i++ / count); - var newOperations = await DetectOperationsAsync(item.IdTelemetry, startDate, db, token); - stopwatch.Stop(); - if (newOperations.Any()) - { - db.DetectedOperations.AddRange(newOperations); - affected += await db.SaveChangesAsync(token); - } - } - } - - private static async Task> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) - { - var query = db.TelemetryDataSaub - .AsNoTracking() - .Where(d => d.IdTelemetry == idTelemetry) - .Where(d => d.BlockPosition >= 0) - .Select(d => new DetectableTelemetry - { - DateTime = d.DateTime, - IdUser = d.IdUser, - WellDepth = d.WellDepth ?? float.NaN, - Pressure = d.Pressure ?? float.NaN, - HookWeight = d.HookWeight ?? float.NaN, - BlockPosition = d.BlockPosition ?? float.NaN, - BitDepth = d.BitDepth ?? float.NaN, - RotorSpeed = d.RotorSpeed ?? float.NaN, - }) - .OrderBy(d => d.DateTime); - - var take = 4 * 86_400; // 4 дня - var startDate = begin; - var detectedOperations = new List(8); - DetectedOperation? lastDetectedOperation = null; - const int minOperationLength = 5; - const int maxDetectorsInterpolationFrameLength = 30; - const int gap = maxDetectorsInterpolationFrameLength + minOperationLength; - - while (true) - { - var data = await query - .Where(d => d.DateTime > startDate) - .Take(take) - .ToArrayAsync(token); - - if (data.Length < gap) - break; - - var isDetected = false; - var positionBegin = 0; - var positionEnd = data.Length - gap; - var step = 10; - while (positionEnd > positionBegin) - { - step ++; - for (int i = 0; i < detectors.Length; i++) - { - progress = $"telemetry:{idTelemetry}, date:{startDate}, pos:{positionBegin}, detector:{detectors[i]}"; - if (detectors[i].TryDetect(idTelemetry, data, positionBegin, positionEnd, lastDetectedOperation, out OperationDetectorResult? result)) - { - detectedOperations.Add(result!.Operation); - lastDetectedOperation = result.Operation; - isDetected = true; - step = 1; - positionBegin = result.TelemetryEnd; - break; - } - } - if (step > 20) - step = 10; - positionBegin += step; - } - - if (isDetected) - startDate = lastDetectedOperation!.DateEnd; - else - startDate = data[positionEnd].DateTime; - } - - return detectedOperations; - } - } - -} diff --git a/AsbCloudInfrastructure/Services/DetectOperations/WorkOperationDetection.cs b/AsbCloudInfrastructure/Services/DetectOperations/WorkOperationDetection.cs new file mode 100644 index 00000000..eef423b9 --- /dev/null +++ b/AsbCloudInfrastructure/Services/DetectOperations/WorkOperationDetection.cs @@ -0,0 +1,157 @@ +using AsbCloudDb.Model; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudInfrastructure.Services.DetectOperations.Detectors; +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; + +namespace AsbCloudInfrastructure.Services.DetectOperations; + +public class WorkOperationDetection: Work +{ + private static readonly DetectorAbstract[] detectors = new DetectorAbstract[] + { + new DetectorRotor(), + new DetectorSlide(), + //new DetectorDevelopment(), + //new DetectorTemplating(), + new DetectorSlipsTime(), + //new DetectorStaticSurveying(), + //new DetectorFlashingBeforeConnection(), + //new DetectorFlashing(), + //new DetectorTemplatingWhileDrilling(), + }; + + public WorkOperationDetection() + :base("Operation detection") + { + Timeout = TimeSpan.FromMinutes(20); + OnErrorAsync = (id, exception, token) => + { + var text = $"work {id}, when {CurrentState?.State}, throw error:{exception.Message}"; + Trace.TraceWarning(text); + return Task.CompletedTask; + }; + } + + protected override async Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token) + { + using var db = services.GetRequiredService(); + + var lastDetectedDates = await db.DetectedOperations + .GroupBy(o => o.IdTelemetry) + .Select(g => new + { + IdTelemetry = g.Key, + LastDate = g.Max(o => o.DateEnd) + }) + .ToListAsync(token); + + var telemetryIds = await db.Telemetries + .Where(t => t.Info != null && t.TimeZone != null) + .Select(t => t.Id) + .ToListAsync(token); + + var joinedlastDetectedDates = telemetryIds + .GroupJoin(lastDetectedDates, + t => t, + o => o.IdTelemetry, + (outer, inner) => new + { + IdTelemetry = outer, + inner.SingleOrDefault()?.LastDate, + }); + + var affected = 0; + var count = joinedlastDetectedDates.Count(); + var i = 0d; + foreach (var item in joinedlastDetectedDates) + { + var stopwatch = Stopwatch.StartNew(); + var startDate = item.LastDate ?? DateTimeOffset.MinValue; + onProgressCallback($"start detecting telemetry: {item.IdTelemetry} from {startDate}", i++ / count); + var newOperations = await DetectOperationsAsync(item.IdTelemetry, startDate, db, token); + stopwatch.Stop(); + if (newOperations.Any()) + { + db.DetectedOperations.AddRange(newOperations); + affected += await db.SaveChangesAsync(token); + } + } + } + + private static async Task> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) + { + var query = db.TelemetryDataSaub + .AsNoTracking() + .Where(d => d.IdTelemetry == idTelemetry) + .Where(d => d.BlockPosition >= 0) + .Select(d => new DetectableTelemetry + { + DateTime = d.DateTime, + IdUser = d.IdUser, + WellDepth = d.WellDepth ?? float.NaN, + Pressure = d.Pressure ?? float.NaN, + HookWeight = d.HookWeight ?? float.NaN, + BlockPosition = d.BlockPosition ?? float.NaN, + BitDepth = d.BitDepth ?? float.NaN, + RotorSpeed = d.RotorSpeed ?? float.NaN, + }) + .OrderBy(d => d.DateTime); + + var take = 4 * 86_400; // 4 дня + var startDate = begin; + var detectedOperations = new List(8); + DetectedOperation? lastDetectedOperation = null; + const int minOperationLength = 5; + const int maxDetectorsInterpolationFrameLength = 30; + const int gap = maxDetectorsInterpolationFrameLength + minOperationLength; + + while (true) + { + var data = await query + .Where(d => d.DateTime > startDate) + .Take(take) + .ToArrayAsync(token); + + if (data.Length < gap) + break; + + var isDetected = false; + var positionBegin = 0; + var positionEnd = data.Length - gap; + var step = 10; + while (positionEnd > positionBegin) + { + step ++; + for (int i = 0; i < detectors.Length; i++) + { + if (detectors[i].TryDetect(idTelemetry, data, positionBegin, positionEnd, lastDetectedOperation, out OperationDetectorResult? result)) + { + detectedOperations.Add(result!.Operation); + lastDetectedOperation = result.Operation; + isDetected = true; + step = 1; + positionBegin = result.TelemetryEnd; + break; + } + } + if (step > 20) + step = 10; + positionBegin += step; + } + + if (isDetected) + startDate = lastDetectedOperation!.DateEnd; + else + startDate = data[positionEnd].DateTime; + } + + return detectedOperations; + } +} diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index 402a89ed..228a3564 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -540,11 +540,8 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram return Task.CompletedTask; }; - var work = new Work(workId, workAction) - { - OnErrorAsync = onErrorAction - }; - + var work = Work.CreateByDelegate(workId, workAction); + work.OnErrorAsync = onErrorAction; backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); } } diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index 33f011bf..bbbc8196 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -56,7 +56,7 @@ namespace AsbCloudInfrastructure.Services.Email { var workAction = MakeEmailSendWorkAction(notification); - var work = new Work(workId, workAction); + var work = Work.CreateByDelegate(workId, workAction); backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); } diff --git a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs deleted file mode 100644 index 07a17369..00000000 --- a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs +++ /dev/null @@ -1,149 +0,0 @@ -using AsbCloudDb.Model; -using Microsoft.EntityFrameworkCore; -using System; -using System.Data.Common; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; -using AsbCloudInfrastructure.Background; -using Microsoft.Extensions.DependencyInjection; - -namespace AsbCloudInfrastructure.Services -{ - - internal static class LimitingParameterCalcWorkFactory - { - private const string workId = "Limiting parameter calc"; - - public static Work MakeWork() => new Work(workId, WorkAction) - { - Timeout = TimeSpan.FromMinutes(30) - }; - - // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) - { - using var db = serviceProvider.GetRequiredService(); - var lastDetectedDates = await db.LimitingParameter - .GroupBy(o => o.IdTelemetry) - .Select(g => new - { - IdTelemetry = g.Key, - LastDate = g.Max(o => o.DateEnd) - }) - .ToListAsync(token); - - var telemetryIds = await db.Telemetries - .Where(t => t.Info != null && t.TimeZone != null) - .Select(t => t.Id) - .ToListAsync(token); - - var telemetryLastDetectedDates = telemetryIds - .GroupJoin(lastDetectedDates, - t => t, - o => o.IdTelemetry, - (outer, inner) => new - { - IdTelemetry = outer, - inner.SingleOrDefault()?.LastDate, - }); - - var count = telemetryLastDetectedDates.Count(); - var i = 0; - foreach (var item in telemetryLastDetectedDates) - { - onProgress($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++/count); - var newLimitingParameters = await GetLimitingParameterAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); - if (newLimitingParameters?.Any() == true) - { - db.LimitingParameter.AddRange(newLimitingParameters); - await db.SaveChangesAsync(token); - } - } - } - - private static async Task> GetLimitingParameterAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) - { - var query = - $"select " + - $"limiting_parameters.date, limiting_parameters.id_feed_regulator, limiting_parameters.well_depth " + - $"from ( " + - $"select " + - $"date, id_feed_regulator, well_depth, " + - $"lag(id_feed_regulator, 1) over (order by date) as id_feed_regulator_lag, " + - $"lead(id_feed_regulator, 1) over (order by date) as id_feed_regulator_lead " + - $"from t_telemetry_data_saub " + - $"where id_feed_regulator is not null " + - $"and id_telemetry = {idTelemetry}" + - $"and date >= '{begin:u}'" + - $"order by date) as limiting_parameters " + - $"where id_feed_regulator_lag is null " + - $"or (id_feed_regulator != id_feed_regulator_lag and id_feed_regulator_lead != id_feed_regulator_lag) " + - $"order by date;"; - - var limitingParameters = new List(32); - using (var result = await ExecuteReaderAsync(db, query, token)) - { - LimitingParameter? limitingLast = null; - while (result.Read()) - { - var date = result.GetFieldValue(0); - var idLimiting = result.GetFieldValue(1); - var wellDepth = result.GetFieldValue(2); - - if (limitingLast is null) - { - limitingLast = new LimitingParameter - { - DateStart = date, - DepthStart = wellDepth, - IdFeedRegulator = idLimiting - }; - } - - if (limitingLast.IdFeedRegulator != idLimiting || limitingLast.DepthStart < wellDepth) - { - limitingParameters.Add(new LimitingParameter { - IdTelemetry = idTelemetry, - IdFeedRegulator = limitingLast.IdFeedRegulator, - DateStart = limitingLast.DateStart, - DateEnd = date, - DepthStart = limitingLast.DepthStart, - DepthEnd = wellDepth - }); - - limitingLast = new LimitingParameter - { - DateStart = date, - DepthStart = wellDepth, - IdFeedRegulator = idLimiting - }; - } - } - } - - return limitingParameters; - } - - private static async Task ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) - { - var connection = db.Database.GetDbConnection(); - if ( - connection?.State is null || - connection.State == ConnectionState.Broken || - connection.State == ConnectionState.Closed) - { - await db.Database.OpenConnectionAsync(token); - connection = db.Database.GetDbConnection(); - } - using var command = connection.CreateCommand(); - command.CommandText = query; - - var result = await command.ExecuteReaderAsync(token); - return result; - } - } - -} diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index f3ab6cb8..6a66c30e 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -94,7 +94,7 @@ namespace AsbCloudInfrastructure.Services context.SaveChanges(); }; - var work = new Work(workId, workAction); + var work = Work.CreateByDelegate(workId, workAction); backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work); progressHandler.Invoke(new ReportProgressDto diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs index daaa2dcf..9bb2dc79 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -48,7 +48,7 @@ namespace AsbCloudInfrastructure.Services.SAUB instance = new TelemetryDataCache(); var worker = provider.GetRequiredService(); var workId = $"Telemetry cache loading from DB {typeof(TEntity).Name}"; - var work = new Work(workId, async (workId, provider, onProgress, token) => { + var work = Work.CreateByDelegate(workId, async (workId, provider, onProgress, token) => { var db = provider.GetRequiredService(); await instance.InitializeCacheFromDBAsync(db, onProgress, token); }); @@ -159,7 +159,6 @@ namespace AsbCloudInfrastructure.Services.SAUB isLoading = true; var defaultTimeout = db.Database.GetCommandTimeout(); - System.Diagnostics.Trace.TraceInformation($"cache loading starting. Setting CommandTimeout 90s ({defaultTimeout})"); db.Database.SetCommandTimeout(TimeSpan.FromSeconds(90)); Well[] wells = await db.Set() @@ -169,7 +168,7 @@ namespace AsbCloudInfrastructure.Services.SAUB .ToArrayAsync(token); var count = wells.Length; - var i = 0; + var i = 0d; foreach (Well well in wells) { var capacity = well.IdState == 1 @@ -178,21 +177,13 @@ namespace AsbCloudInfrastructure.Services.SAUB var idTelemetry = well.IdTelemetry!.Value; var hoursOffset = well.Timezone.Hours; - // TODO: remove traces - System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}>: Loading for well: {well.Cluster?.Caption}/{well.Caption} (capacity:{capacity}) idTelemetry:{idTelemetry}"); + + onProgress($"Loading for well: {well.Cluster?.Caption}/{well.Caption} (capacity:{capacity}) idTelemetry:{idTelemetry}", i++/count); var cacheItem = await GetOrDefaultCacheDataFromDbAsync(db, idTelemetry, capacity, hoursOffset, token); if(cacheItem is not null) - { caches.TryAdd(idTelemetry, cacheItem); - System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} loaded"); - } - else - { - System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} has no data"); - } } - System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> load complete"); isLoading = false; db.Database.SetCommandTimeout(defaultTimeout); } diff --git a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs deleted file mode 100644 index 451ca633..00000000 --- a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs +++ /dev/null @@ -1,298 +0,0 @@ -using AsbCloudDb.Model; -using AsbCloudDb.Model.Subsystems; -using AsbCloudInfrastructure.Background; -using AsbCloudInfrastructure.Services.Subsystems.Utils; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudInfrastructure.Services.Subsystems -{ - internal static class SubsystemOperationTimeCalcWorkFactory - { - private const string workId = "Subsystem operation time calc"; - - private const int idSubsytemTorqueMaster = 65537; - private const int idSubsytemSpinMaster = 65536; - private const int idSubsystemAPDRotor = 11; - private const int idSubsystemAPDSlide = 12; - private const int idSubsytemMse = 2; - - public static Work MakeWork() => new Work(workId, WorkAction) - { - Timeout = TimeSpan.FromMinutes(20) - }; - - // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. - private static async Task WorkAction(string _, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) - { - using var db = serviceProvider.GetRequiredService(); - - var lastDetectedDates = await db.SubsystemOperationTimes - .GroupBy(o => o.IdTelemetry) - .Select(g => new - { - IdTelemetry = g.Key, - LastDate = g.Max(o => o.DateEnd) - }) - .ToListAsync(token); - - var telemetryIds = await db.Telemetries - .Where(t => t.Info != null && t.TimeZone != null) - .Select(t => t.Id) - .ToListAsync(token); - - var telemetryLastDetectedDates = telemetryIds - .GroupJoin(lastDetectedDates, - t => t, - o => o.IdTelemetry, - (outer, inner) => new - { - IdTelemetry = outer, - inner.SingleOrDefault()?.LastDate, - }); - - var count = telemetryLastDetectedDates.Count(); - var i = 0; - foreach (var item in telemetryLastDetectedDates) - { - onProgress($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++ / count); - var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); - if (newOperationsSaub?.Any() == true) - { - db.SubsystemOperationTimes.AddRange(newOperationsSaub); - await db.SaveChangesAsync(token); - } - var newOperationsSpin = await OperationTimeSpinAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); - if (newOperationsSpin?.Any() == true) - { - db.SubsystemOperationTimes.AddRange(newOperationsSpin); - await db.SaveChangesAsync(token); - } - } - } - - private static async Task ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) - { - var connection = db.Database.GetDbConnection(); - if ( - connection?.State is null || - connection.State == ConnectionState.Broken || - connection.State == ConnectionState.Closed) - { - await db.Database.OpenConnectionAsync(token); - connection = db.Database.GetDbConnection(); - } - using var command = connection.CreateCommand(); - command.CommandText = query; - - var result = await command.ExecuteReaderAsync(token); - return result; - } - - private static async Task> OperationTimeSaubAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) - { - static bool isSubsytemAkbRotor(short? mode) => mode == 1; - - static bool isSubsytemAkbSlide(short? mode) => mode == 3; - - static bool IsSubsystemMse(short? state) => (state & 1) > 0; - - var query = - $"select tt.date, tt.mode, tt.well_depth, tt.mse_state " + - $"from ( " + - $" select " + - $" date, " + - $" mode, " + - $" mse_state, " + - $" well_depth, " + - $" lag(mode,1) over (order by date) as mode_lag, " + - $" lead(mode,1) over (order by date) as mode_lead " + - $" from t_telemetry_data_saub " + - $" where id_telemetry = {idTelemetry} and well_depth is not null and well_depth > 0" + - $" order by date ) as tt " + - $"where (tt.mode_lag is null or (tt.mode != tt.mode_lag and tt.mode_lead != tt.mode_lag)) and tt.date >= '{begin:u}' " + - $"order by tt.date;"; - - using var result = await ExecuteReaderAsync(db, query, token); - - var subsystemsOperationTimes = new List(); - var detectorRotor = new SubsystemDetector(idTelemetry, idSubsystemAPDRotor, isSubsytemAkbRotor, IsValid); - var detectorSlide = new SubsystemDetector(idTelemetry, idSubsystemAPDSlide, isSubsytemAkbSlide, IsValid); - var detectorMse = new SubsystemDetector(idTelemetry, idSubsytemMse, IsSubsystemMse, IsValid); - - while (result.Read()) - { - var mode = result.GetFieldValue(1); - var state = result.GetFieldValue(3); - - var isAkbRotorEnable = isSubsytemAkbRotor(mode); - var isAkbSlideEnable = isSubsytemAkbSlide(mode); - var isMseEnable = IsSubsystemMse(state); - var date = result.GetFieldValue(0); - var depth = result.GetFieldValue(2); - - if (detectorRotor.TryDetect(mode, date, depth, out var detectedRotor)) - subsystemsOperationTimes.Add(detectedRotor!); - - if (detectorSlide.TryDetect(mode, date, depth, out var detectedSlide)) - subsystemsOperationTimes.Add(detectedSlide!); - - if (detectorMse.TryDetect(mode, date, depth, out var detectedMse)) - subsystemsOperationTimes.Add(detectedMse!); - } - - return subsystemsOperationTimes; - } - - private static async Task> OperationTimeSpinAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) - { - static int? GetSubsytemId(short? mode, int? state) - { - // При изменении следующего кода сообщи в Vladimir.Sobolev@nedra.digital - if (state == 7 && (mode & 2) > 0) - return idSubsytemTorqueMaster;// демпфер - - if (state != 0 && state != 5 && state != 6 && state != 7) - return idSubsytemSpinMaster;// осцилляция - - return null; - } - - var querySpin = - $"select " + - $" tspin.date, " + - $" tspin.mode, " + - $" tspin.state " + - $"from ( " + - $" select " + - $" date, " + - $" mode, " + - $" lag(mode, 1) over (order by date) as mode_lag, " + - $" lead(mode, 1) over (order by date) as mode_lead, " + - $" state, " + - $" lag(state, 1) over (order by date) as state_lag " + - $" from t_telemetry_data_spin " + - $" where id_telemetry = {idTelemetry} and date >= '{begin:u}'" + - $" order by date ) as tspin " + - $"where mode_lag is null or state_lag is null or (mode != mode_lag and mode_lead != mode_lag) or state != state_lag " + - $"order by date;"; - - var rows = new List<(int? IdSubsystem, DateTimeOffset Date)>(32); - - using var resultSpin = await ExecuteReaderAsync(db, querySpin, token); - int? idSubsystemLast = null; - while (resultSpin.Read()) - { - var mode = resultSpin.GetFieldValue(1); - var state = resultSpin.GetFieldValue(2); - var idSubsystem = GetSubsytemId(mode, state); - if (idSubsystemLast != idSubsystem) - { - idSubsystemLast = idSubsystem; - var date = resultSpin.GetFieldValue(0); - rows.Add((idSubsystem, date)); - } - } - await resultSpin.DisposeAsync(); - - if (rows.Count < 2) - return Enumerable.Empty(); - - var minSpinDate = rows.Min(i => i.Date); - var maxSpinDate = rows.Max(i => i.Date); - var depthInterpolation = await GetInterpolation(db, idTelemetry, minSpinDate, maxSpinDate, token); - - if (depthInterpolation is null) - return Enumerable.Empty(); - - var subsystemsOperationTimes = new List(32); - - for (int i = 1; i < rows.Count; i++) - { - var r0 = rows[i - 1]; - var r1 = rows[i]; - if (r0.IdSubsystem is not null && r0.IdSubsystem != r1.IdSubsystem) - { - var subsystemOperationTime = new SubsystemOperationTime() - { - IdTelemetry = idTelemetry, - IdSubsystem = r0.IdSubsystem.Value, - DateStart = r0.Date, - DateEnd = r1.Date, - DepthStart = depthInterpolation.GetDepth(r0.Date), - DepthEnd = depthInterpolation.GetDepth(r1.Date), - }; - - if (IsValid(subsystemOperationTime)) - subsystemsOperationTimes.Add(subsystemOperationTime); - } - } - - return subsystemsOperationTimes; - } - - private static bool IsValid(SubsystemOperationTime item) - { - var validateCode = GetValidateErrorCode(item); - if (validateCode != 0) - { - var str = System.Text.Json.JsonSerializer.Serialize(item); - Trace.TraceWarning($"Wrong({validateCode}) SubsystemOperationTime: {str}"); - } - return validateCode == 0; - } - - private static int GetValidateErrorCode(SubsystemOperationTime item) - { - if (item.DateStart > item.DateEnd) - return -1; - if ((item.DateEnd - item.DateStart).TotalHours > 48) - return -2; - if (item.DepthEnd < item.DepthStart) - return -3; - if (item.DepthEnd - item.DepthStart > 2000d) - return -4; - if (item.DepthEnd < 0d) - return -5; - if (item.DepthStart < 0d) - return -6; - if (item.DepthEnd > 24_0000d) - return -7; - if (item.DepthStart > 24_0000d) - return -8; - return 0; - } - - private static async Task GetInterpolation(IAsbCloudDbContext db, int idTelemetry, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) - { - var dataDepthFromSaub = await db.TelemetryDataSaub - .Where(d => d.IdTelemetry == idTelemetry) - .Where(d => d.DateTime >= dateBegin) - .Where(d => d.DateTime <= dateEnd) - .Where(d => d.WellDepth != null) - .Where(d => d.WellDepth > 0) - .GroupBy(d => Math.Ceiling(d.WellDepth ?? 0 * 10)) - .Select(g => new { - DateMin = g.Min(d => d.DateTime), - DepthMin = g.Min(d => d.WellDepth) ?? 0, - }) - .OrderBy(i => i.DateMin) - .ToArrayAsync(token); - - if (!dataDepthFromSaub.Any()) - return null; - - var depthInterpolation = new DepthInterpolation(dataDepthFromSaub.Select(i => (i.DateMin, i.DepthMin))); - return depthInterpolation; - } - } - -} diff --git a/AsbCloudInfrastructure/Services/Subsystems/WorkSubsystemOperationTimeCalc.cs b/AsbCloudInfrastructure/Services/Subsystems/WorkSubsystemOperationTimeCalc.cs new file mode 100644 index 00000000..bda4bbe4 --- /dev/null +++ b/AsbCloudInfrastructure/Services/Subsystems/WorkSubsystemOperationTimeCalc.cs @@ -0,0 +1,294 @@ +using AsbCloudDb.Model; +using AsbCloudDb.Model.Subsystems; +using AsbCloudInfrastructure.Background; +using AsbCloudInfrastructure.Services.Subsystems.Utils; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Services.Subsystems; + +public class WorkSubsystemOperationTimeCalc: Work +{ + private const int idSubsytemTorqueMaster = 65537; + private const int idSubsytemSpinMaster = 65536; + private const int idSubsystemAPDRotor = 11; + private const int idSubsystemAPDSlide = 12; + private const int idSubsytemMse = 2; + + public WorkSubsystemOperationTimeCalc() + : base("Subsystem operation time calc") + { + Timeout = TimeSpan.FromMinutes(20); + } + + protected override async Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token) + { + using var db = services.GetRequiredService(); + + var lastDetectedDates = await db.SubsystemOperationTimes + .GroupBy(o => o.IdTelemetry) + .Select(g => new + { + IdTelemetry = g.Key, + LastDate = g.Max(o => o.DateEnd) + }) + .ToListAsync(token); + + var telemetryIds = await db.Telemetries + .Where(t => t.Info != null && t.TimeZone != null) + .Select(t => t.Id) + .ToListAsync(token); + + var telemetryLastDetectedDates = telemetryIds + .GroupJoin(lastDetectedDates, + t => t, + o => o.IdTelemetry, + (outer, inner) => new + { + IdTelemetry = outer, + inner.SingleOrDefault()?.LastDate, + }); + + var count = telemetryLastDetectedDates.Count(); + var i = 0d; + foreach (var item in telemetryLastDetectedDates) + { + onProgressCallback($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++ / count); + var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); + if (newOperationsSaub?.Any() == true) + { + db.SubsystemOperationTimes.AddRange(newOperationsSaub); + await db.SaveChangesAsync(token); + } + var newOperationsSpin = await OperationTimeSpinAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); + if (newOperationsSpin?.Any() == true) + { + db.SubsystemOperationTimes.AddRange(newOperationsSpin); + await db.SaveChangesAsync(token); + } + } + } + + private static async Task ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) + { + var connection = db.Database.GetDbConnection(); + if ( + connection?.State is null || + connection.State == ConnectionState.Broken || + connection.State == ConnectionState.Closed) + { + await db.Database.OpenConnectionAsync(token); + connection = db.Database.GetDbConnection(); + } + using var command = connection.CreateCommand(); + command.CommandText = query; + + var result = await command.ExecuteReaderAsync(token); + return result; + } + + private static async Task> OperationTimeSaubAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) + { + static bool isSubsytemAkbRotor(short? mode) => mode == 1; + + static bool isSubsytemAkbSlide(short? mode) => mode == 3; + + static bool IsSubsystemMse(short? state) => (state & 1) > 0; + + var query = + $"select tt.date, tt.mode, tt.well_depth, tt.mse_state " + + $"from ( " + + $" select " + + $" date, " + + $" mode, " + + $" mse_state, " + + $" well_depth, " + + $" lag(mode,1) over (order by date) as mode_lag, " + + $" lead(mode,1) over (order by date) as mode_lead " + + $" from t_telemetry_data_saub " + + $" where id_telemetry = {idTelemetry} and well_depth is not null and well_depth > 0" + + $" order by date ) as tt " + + $"where (tt.mode_lag is null or (tt.mode != tt.mode_lag and tt.mode_lead != tt.mode_lag)) and tt.date >= '{begin:u}' " + + $"order by tt.date;"; + + using var result = await ExecuteReaderAsync(db, query, token); + + var subsystemsOperationTimes = new List(); + var detectorRotor = new SubsystemDetector(idTelemetry, idSubsystemAPDRotor, isSubsytemAkbRotor, IsValid); + var detectorSlide = new SubsystemDetector(idTelemetry, idSubsystemAPDSlide, isSubsytemAkbSlide, IsValid); + var detectorMse = new SubsystemDetector(idTelemetry, idSubsytemMse, IsSubsystemMse, IsValid); + + while (result.Read()) + { + var mode = result.GetFieldValue(1); + var state = result.GetFieldValue(3); + + var isAkbRotorEnable = isSubsytemAkbRotor(mode); + var isAkbSlideEnable = isSubsytemAkbSlide(mode); + var isMseEnable = IsSubsystemMse(state); + var date = result.GetFieldValue(0); + var depth = result.GetFieldValue(2); + + if (detectorRotor.TryDetect(mode, date, depth, out var detectedRotor)) + subsystemsOperationTimes.Add(detectedRotor!); + + if (detectorSlide.TryDetect(mode, date, depth, out var detectedSlide)) + subsystemsOperationTimes.Add(detectedSlide!); + + if (detectorMse.TryDetect(mode, date, depth, out var detectedMse)) + subsystemsOperationTimes.Add(detectedMse!); + } + + return subsystemsOperationTimes; + } + + private static async Task> OperationTimeSpinAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) + { + static int? GetSubsytemId(short? mode, int? state) + { + // При изменении следующего кода сообщи в Vladimir.Sobolev@nedra.digital + if (state == 7 && (mode & 2) > 0) + return idSubsytemTorqueMaster;// демпфер + + if (state != 0 && state != 5 && state != 6 && state != 7) + return idSubsytemSpinMaster;// осцилляция + + return null; + } + + var querySpin = + $"select " + + $" tspin.date, " + + $" tspin.mode, " + + $" tspin.state " + + $"from ( " + + $" select " + + $" date, " + + $" mode, " + + $" lag(mode, 1) over (order by date) as mode_lag, " + + $" lead(mode, 1) over (order by date) as mode_lead, " + + $" state, " + + $" lag(state, 1) over (order by date) as state_lag " + + $" from t_telemetry_data_spin " + + $" where id_telemetry = {idTelemetry} and date >= '{begin:u}'" + + $" order by date ) as tspin " + + $"where mode_lag is null or state_lag is null or (mode != mode_lag and mode_lead != mode_lag) or state != state_lag " + + $"order by date;"; + + var rows = new List<(int? IdSubsystem, DateTimeOffset Date)>(32); + + using var resultSpin = await ExecuteReaderAsync(db, querySpin, token); + int? idSubsystemLast = null; + while (resultSpin.Read()) + { + var mode = resultSpin.GetFieldValue(1); + var state = resultSpin.GetFieldValue(2); + var idSubsystem = GetSubsytemId(mode, state); + if (idSubsystemLast != idSubsystem) + { + idSubsystemLast = idSubsystem; + var date = resultSpin.GetFieldValue(0); + rows.Add((idSubsystem, date)); + } + } + await resultSpin.DisposeAsync(); + + if (rows.Count < 2) + return Enumerable.Empty(); + + var minSpinDate = rows.Min(i => i.Date); + var maxSpinDate = rows.Max(i => i.Date); + var depthInterpolation = await GetInterpolation(db, idTelemetry, minSpinDate, maxSpinDate, token); + + if (depthInterpolation is null) + return Enumerable.Empty(); + + var subsystemsOperationTimes = new List(32); + + for (int i = 1; i < rows.Count; i++) + { + var r0 = rows[i - 1]; + var r1 = rows[i]; + if (r0.IdSubsystem is not null && r0.IdSubsystem != r1.IdSubsystem) + { + var subsystemOperationTime = new SubsystemOperationTime() + { + IdTelemetry = idTelemetry, + IdSubsystem = r0.IdSubsystem.Value, + DateStart = r0.Date, + DateEnd = r1.Date, + DepthStart = depthInterpolation.GetDepth(r0.Date), + DepthEnd = depthInterpolation.GetDepth(r1.Date), + }; + + if (IsValid(subsystemOperationTime)) + subsystemsOperationTimes.Add(subsystemOperationTime); + } + } + + return subsystemsOperationTimes; + } + + private static bool IsValid(SubsystemOperationTime item) + { + var validateCode = GetValidateErrorCode(item); + if (validateCode != 0) + { + var str = System.Text.Json.JsonSerializer.Serialize(item); + Trace.TraceWarning($"Wrong({validateCode}) SubsystemOperationTime: {str}"); + } + return validateCode == 0; + } + + private static int GetValidateErrorCode(SubsystemOperationTime item) + { + if (item.DateStart > item.DateEnd) + return -1; + if ((item.DateEnd - item.DateStart).TotalHours > 48) + return -2; + if (item.DepthEnd < item.DepthStart) + return -3; + if (item.DepthEnd - item.DepthStart > 2000d) + return -4; + if (item.DepthEnd < 0d) + return -5; + if (item.DepthStart < 0d) + return -6; + if (item.DepthEnd > 24_0000d) + return -7; + if (item.DepthStart > 24_0000d) + return -8; + return 0; + } + + private static async Task GetInterpolation(IAsbCloudDbContext db, int idTelemetry, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) + { + var dataDepthFromSaub = await db.TelemetryDataSaub + .Where(d => d.IdTelemetry == idTelemetry) + .Where(d => d.DateTime >= dateBegin) + .Where(d => d.DateTime <= dateEnd) + .Where(d => d.WellDepth != null) + .Where(d => d.WellDepth > 0) + .GroupBy(d => Math.Ceiling(d.WellDepth ?? 0 * 10)) + .Select(g => new { + DateMin = g.Min(d => d.DateTime), + DepthMin = g.Min(d => d.WellDepth) ?? 0, + }) + .OrderBy(i => i.DateMin) + .ToArrayAsync(token); + + if (!dataDepthFromSaub.Any()) + return null; + + var depthInterpolation = new DepthInterpolation(dataDepthFromSaub.Select(i => (i.DateMin, i.DepthMin))); + return depthInterpolation; + } +} diff --git a/AsbCloudInfrastructure/Services/WellInfoService.cs b/AsbCloudInfrastructure/Services/WellInfoService.cs index 80456db9..7e4cd8ce 100644 --- a/AsbCloudInfrastructure/Services/WellInfoService.cs +++ b/AsbCloudInfrastructure/Services/WellInfoService.cs @@ -18,53 +18,26 @@ using System.Threading.Tasks; using AsbCloudApp.IntegrationEvents; using AsbCloudApp.IntegrationEvents.Interfaces; -namespace AsbCloudInfrastructure.Services +namespace AsbCloudInfrastructure.Services; + +public class WellInfoService { - public class WellInfoService + public class WorkWellInfoUpdate : Work { - class WellMapInfoWithComanies : WellMapInfoDto + public WorkWellInfoUpdate() + : base("Well statistics update") { - public int? IdTelemetry { get; set; } - public IEnumerable IdsCompanies { get; set; } = null!; + Timeout = TimeSpan.FromMinutes(20); } - private const string workId = "Well statistics update"; - - private readonly TelemetryDataCache telemetryDataSaubCache; - private readonly TelemetryDataCache telemetryDataSpinCache; - private readonly IWitsRecordRepository witsRecord7Repository; - private readonly IWitsRecordRepository witsRecord1Repository; - private readonly IGtrRepository gtrRepository; - private static IEnumerable WellMapInfo = Enumerable.Empty(); - - public WellInfoService( - TelemetryDataCache telemetryDataSaubCache, - TelemetryDataCache telemetryDataSpinCache, - IWitsRecordRepository witsRecord7Repository, - IWitsRecordRepository witsRecord1Repository, - IGtrRepository gtrRepository) + protected override async Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token) { - this.telemetryDataSaubCache = telemetryDataSaubCache; - this.telemetryDataSpinCache = telemetryDataSpinCache; - - this.witsRecord7Repository = witsRecord7Repository; - this.witsRecord1Repository = witsRecord1Repository; - this.gtrRepository = gtrRepository; - } - - public static Work MakeWork() => new Work(workId, WorkAction) - { - Timeout = TimeSpan.FromMinutes(20) - }; - - private static async Task WorkAction(string workName, IServiceProvider serviceProvider, Action onProgress, CancellationToken token) - { - var wellService = serviceProvider.GetRequiredService(); - var operationsStatService = serviceProvider.GetRequiredService(); - var processMapRepository = serviceProvider.GetRequiredService(); - var subsystemOperationTimeService = serviceProvider.GetRequiredService(); - var telemetryDataSaubCache = serviceProvider.GetRequiredService>(); - var messageHub = serviceProvider.GetRequiredService>(); + var wellService = services.GetRequiredService(); + var operationsStatService = services.GetRequiredService(); + var processMapRepository = services.GetRequiredService(); + var subsystemOperationTimeService = services.GetRequiredService(); + var telemetryDataSaubCache = services.GetRequiredService>(); + var messageHub = services.GetRequiredService>(); var wells = await wellService.GetAllAsync(token); @@ -82,30 +55,30 @@ namespace AsbCloudInfrastructure.Services }); var operationsStat = await operationsStatService.GetWellsStatAsync(wellsIds, token); - + var subsystemStat = await subsystemOperationTimeService .GetStatByActiveWells(wellsIds, token); var count = wells.Count(); - var i = 0; + var i = 0d; WellMapInfo = wells.Select(well => { var wellMapInfo = well.Adapt(); wellMapInfo.IdState = well.IdState; - onProgress($"Start updating info by well({well.Id}): {well.Caption}", i++ / count); + onProgressCallback($"Start updating info by well({well.Id}): {well.Caption}", i++ / count); double? currentDepth = null; TelemetryDataSaubDto? lastSaubTelemetry = null; - + if (well.IdTelemetry.HasValue) { wellMapInfo.IdTelemetry = well.IdTelemetry.Value; lastSaubTelemetry = telemetryDataSaubCache.GetLastOrDefault(well.IdTelemetry.Value); - if(lastSaubTelemetry is not null) + if (lastSaubTelemetry is not null) { currentDepth = lastSaubTelemetry.WellDepth; } } - var wellOperationsStat = operationsStat.FirstOrDefault(s => s.Id == well.Id); + var wellOperationsStat = operationsStat.FirstOrDefault(s => s.Id == well.Id); var wellLastFactSection = wellOperationsStat?.Sections.LastOrDefault(s => s.Fact is not null); currentDepth ??= wellLastFactSection?.Fact?.WellDepthEnd; @@ -120,7 +93,7 @@ namespace AsbCloudInfrastructure.Services { wellProcessMap = wellProcessMaps.FirstOrDefault(p => p.IdWellSectionType == idSection); } - else if(currentDepth.HasValue) + else if (currentDepth.HasValue) { wellProcessMap = wellProcessMaps.FirstOrDefault(p => p.DepthStart <= currentDepth.Value && p.DepthEnd >= currentDepth.Value); } @@ -130,8 +103,8 @@ namespace AsbCloudInfrastructure.Services planTotalDepth ??= wellOperationsStat?.Total.Plan?.WellDepthEnd; wellMapInfo.Section = wellLastFactSection?.Caption; - - wellMapInfo.FirstFactOperationDateStart = wellOperationsStat?.Total.Fact?.Start + + wellMapInfo.FirstFactOperationDateStart = wellOperationsStat?.Total.Fact?.Start ?? wellOperationsStat?.Total.Plan?.Start; wellMapInfo.LastPredictOperationDateEnd = wellOperationsStat?.Total.Plan?.End; @@ -159,7 +132,7 @@ namespace AsbCloudInfrastructure.Services Plan = wellProcessMap?.Pressure.Plan, Fact = lastSaubTelemetry?.Pressure }; - + wellMapInfo.PressureSp = lastSaubTelemetry?.PressureSp; wellMapInfo.WellDepth = new() @@ -191,51 +164,79 @@ namespace AsbCloudInfrastructure.Services return wellMapInfo; }).ToArray(); - var updateWellInfoEventTasks = wellsIds.Select(idWell => + var updateWellInfoEventTasks = wellsIds.Select(idWell => messageHub.HandleAsync(new UpdateWellInfoEvent(idWell), token)); - + await Task.WhenAll(updateWellInfoEventTasks); } + } - private WellMapInfoWithTelemetryStat Convert(WellMapInfoWithComanies wellInfo) + class WellMapInfoWithComanies : WellMapInfoDto + { + public int? IdTelemetry { get; set; } + public IEnumerable IdsCompanies { get; set; } = null!; + } + + private readonly TelemetryDataCache telemetryDataSaubCache; + private readonly TelemetryDataCache telemetryDataSpinCache; + private readonly IWitsRecordRepository witsRecord7Repository; + private readonly IWitsRecordRepository witsRecord1Repository; + private readonly IGtrRepository gtrRepository; + private static IEnumerable WellMapInfo = Enumerable.Empty(); + + public WellInfoService( + TelemetryDataCache telemetryDataSaubCache, + TelemetryDataCache telemetryDataSpinCache, + IWitsRecordRepository witsRecord7Repository, + IWitsRecordRepository witsRecord1Repository, + IGtrRepository gtrRepository) + { + this.telemetryDataSaubCache = telemetryDataSaubCache; + this.telemetryDataSpinCache = telemetryDataSpinCache; + + this.witsRecord7Repository = witsRecord7Repository; + this.witsRecord1Repository = witsRecord1Repository; + this.gtrRepository = gtrRepository; + } + + private WellMapInfoWithTelemetryStat Convert(WellMapInfoWithComanies wellInfo) + { + var result = wellInfo.Adapt(); + if (wellInfo.IdTelemetry.HasValue) { - var result = wellInfo.Adapt(); - if (wellInfo.IdTelemetry.HasValue) - { - var idTelemetry = wellInfo.IdTelemetry.Value; - result.LastDataSaub = telemetryDataSaubCache.GetLastOrDefault(idTelemetry); - result.LastDataSpin = telemetryDataSpinCache.GetLastOrDefault(idTelemetry); - result.LastDataDdsDate = GetLastOrDefaultDdsTelemetry(idTelemetry); - result.LastDataGtrDate = gtrRepository.GetLastData(wellInfo.Id) - .MaxOrDefault(item => item.Date); - result.LastDataDpcsDate = null; - result.LastDataDpcsDate = null; - } - - return result; + var idTelemetry = wellInfo.IdTelemetry.Value; + result.LastDataSaub = telemetryDataSaubCache.GetLastOrDefault(idTelemetry); + result.LastDataSpin = telemetryDataSpinCache.GetLastOrDefault(idTelemetry); + result.LastDataDdsDate = GetLastOrDefaultDdsTelemetry(idTelemetry); + result.LastDataGtrDate = gtrRepository.GetLastData(wellInfo.Id) + .MaxOrDefault(item => item.Date); + result.LastDataDpcsDate = null; + result.LastDataDpcsDate = null; } - private DateTime? GetLastOrDefaultDdsTelemetry(int idTelemetry) - { - var lastDdsRecord1Date = witsRecord1Repository.GetLastOrDefault(idTelemetry)?.DateTime; - var lastDdsRecord7Date = witsRecord7Repository.GetLastOrDefault(idTelemetry)?.DateTime; + return result; + } - if (lastDdsRecord1Date.HasValue && lastDdsRecord7Date.HasValue) - if (lastDdsRecord1Date.Value > lastDdsRecord7Date.Value) - return lastDdsRecord1Date.Value; - else - return lastDdsRecord7Date.Value; + private DateTime? GetLastOrDefaultDdsTelemetry(int idTelemetry) + { + var lastDdsRecord1Date = witsRecord1Repository.GetLastOrDefault(idTelemetry)?.DateTime; + var lastDdsRecord7Date = witsRecord7Repository.GetLastOrDefault(idTelemetry)?.DateTime; - return lastDdsRecord1Date ?? lastDdsRecord7Date; - } + if (lastDdsRecord1Date.HasValue && lastDdsRecord7Date.HasValue) + if (lastDdsRecord1Date.Value > lastDdsRecord7Date.Value) + return lastDdsRecord1Date.Value; + else + return lastDdsRecord7Date.Value; - public WellMapInfoWithTelemetryStat? FirstOrDefault(Func predicate) - { - var first = WellMapInfo.FirstOrDefault(predicate); - if (first is WellMapInfoWithComanies wellMapInfoWithComanies) - return Convert(wellMapInfoWithComanies); + return lastDdsRecord1Date ?? lastDdsRecord7Date; + } - return null; - } + public WellMapInfoWithTelemetryStat? FirstOrDefault(Func predicate) + { + var first = WellMapInfo.FirstOrDefault(predicate); + if (first is WellMapInfoWithComanies wellMapInfoWithComanies) + return Convert(wellMapInfoWithComanies); + + return null; } } diff --git a/AsbCloudInfrastructure/Services/WorkLimitingParameterCalc.cs b/AsbCloudInfrastructure/Services/WorkLimitingParameterCalc.cs new file mode 100644 index 00000000..3ab159e0 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WorkLimitingParameterCalc.cs @@ -0,0 +1,144 @@ +using AsbCloudDb.Model; +using Microsoft.EntityFrameworkCore; +using System; +using System.Data.Common; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; + +namespace AsbCloudInfrastructure.Services; + +public class WorkLimitingParameterCalc : Work +{ + public WorkLimitingParameterCalc() + : base("Limiting parameter calc") + { + Timeout = TimeSpan.FromMinutes(30); + } + + protected override async Task Action(string id, IServiceProvider services, Action onProgressCallback, CancellationToken token) + { + using var db = services.GetRequiredService(); + var lastDetectedDates = await db.LimitingParameter + .GroupBy(o => o.IdTelemetry) + .Select(g => new + { + IdTelemetry = g.Key, + LastDate = g.Max(o => o.DateEnd) + }) + .ToListAsync(token); + + var telemetryIds = await db.Telemetries + .Where(t => t.Info != null && t.TimeZone != null) + .Select(t => t.Id) + .ToListAsync(token); + + var telemetryLastDetectedDates = telemetryIds + .GroupJoin(lastDetectedDates, + t => t, + o => o.IdTelemetry, + (outer, inner) => new + { + IdTelemetry = outer, + inner.SingleOrDefault()?.LastDate, + }); + + var count = telemetryLastDetectedDates.Count(); + var i = 0d; + foreach (var item in telemetryLastDetectedDates) + { + onProgressCallback($"Start hanling telemetry: {item.IdTelemetry} from {item.LastDate}", i++/count); + var newLimitingParameters = await GetLimitingParameterAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); + if (newLimitingParameters?.Any() == true) + { + db.LimitingParameter.AddRange(newLimitingParameters); + await db.SaveChangesAsync(token); + } + } + } + + private static async Task> GetLimitingParameterAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) + { + var query = + $"select " + + $"limiting_parameters.date, limiting_parameters.id_feed_regulator, limiting_parameters.well_depth " + + $"from ( " + + $"select " + + $"date, id_feed_regulator, well_depth, " + + $"lag(id_feed_regulator, 1) over (order by date) as id_feed_regulator_lag, " + + $"lead(id_feed_regulator, 1) over (order by date) as id_feed_regulator_lead " + + $"from t_telemetry_data_saub " + + $"where id_feed_regulator is not null " + + $"and id_telemetry = {idTelemetry}" + + $"and date >= '{begin:u}'" + + $"order by date) as limiting_parameters " + + $"where id_feed_regulator_lag is null " + + $"or (id_feed_regulator != id_feed_regulator_lag and id_feed_regulator_lead != id_feed_regulator_lag) " + + $"order by date;"; + + var limitingParameters = new List(32); + using (var result = await ExecuteReaderAsync(db, query, token)) + { + LimitingParameter? limitingLast = null; + while (result.Read()) + { + var date = result.GetFieldValue(0); + var idLimiting = result.GetFieldValue(1); + var wellDepth = result.GetFieldValue(2); + + if (limitingLast is null) + { + limitingLast = new LimitingParameter + { + DateStart = date, + DepthStart = wellDepth, + IdFeedRegulator = idLimiting + }; + } + + if (limitingLast.IdFeedRegulator != idLimiting || limitingLast.DepthStart < wellDepth) + { + limitingParameters.Add(new LimitingParameter { + IdTelemetry = idTelemetry, + IdFeedRegulator = limitingLast.IdFeedRegulator, + DateStart = limitingLast.DateStart, + DateEnd = date, + DepthStart = limitingLast.DepthStart, + DepthEnd = wellDepth + }); + + limitingLast = new LimitingParameter + { + DateStart = date, + DepthStart = wellDepth, + IdFeedRegulator = idLimiting + }; + } + } + } + + return limitingParameters; + } + + private static async Task ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) + { + var connection = db.Database.GetDbConnection(); + if ( + connection?.State is null || + connection.State == ConnectionState.Broken || + connection.State == ConnectionState.Closed) + { + await db.Database.OpenConnectionAsync(token); + connection = db.Database.GetDbConnection(); + } + using var command = connection.CreateCommand(); + command.CommandText = query; + + var result = await command.ExecuteReaderAsync(token); + return result; + } +} diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index 604a4f69..d00de488 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -31,10 +31,10 @@ namespace AsbCloudInfrastructure _ = provider.GetRequiredService>(); var backgroundWorker = provider.GetRequiredService(); - backgroundWorker.WorkStore.AddPeriodic(WellInfoService.MakeWork(), TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(OperationDetectionWorkFactory.MakeWork(), TimeSpan.FromMinutes(15)); - backgroundWorker.WorkStore.AddPeriodic(SubsystemOperationTimeCalcWorkFactory.MakeWork(), TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(LimitingParameterCalcWorkFactory.MakeWork(), TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(15)); + backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); + backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); var notificationBackgroundWorker = provider.GetRequiredService(); @@ -55,7 +55,7 @@ namespace AsbCloudInfrastructure System.Diagnostics.Trace.TraceInformation($"Total memory allocated is {bytesString} bytes. DbContext count is:{AsbCloudDbContext.ReferenceCount}"); return Task.CompletedTask; }; - var work = new Work("Memory monitoring", workAction); + var work = Work.CreateByDelegate("Memory monitoring", workAction); return work; } diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs index 96556845..b118e179 100644 --- a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs +++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs @@ -1,4 +1,6 @@ -using AsbCloudApp.Data.SAUB; +using AsbCloudApp.Data; +using AsbCloudApp.Data.SAUB; +using AsbCloudApp.Requests; using AsbCloudApp.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -35,12 +37,22 @@ namespace AsbCloudWebApi.Tests.Middlware public class TelemetryDataSaubService : ITelemetryDataSaubService { - public async Task?> GetAsync(int idWell, DateTime dateBegin = default, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default) + public async Task> GetAsync(int idWell, DateTime dateBegin = default, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default) { await Task.Delay(1000, token); return Enumerable.Empty(); } + public Task> GetAsync(int idWell, TelemetryDataRequest request, CancellationToken token) + { + throw new NotImplementedException(); + } + + public Task GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token) + { + throw new NotImplementedException(); + } + public Task> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException(); public Task GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token) diff --git a/AsbCloudWebApi/Controllers/BackgroundWork.cs b/AsbCloudWebApi/Controllers/BackgroundWork.cs new file mode 100644 index 00000000..316d796e --- /dev/null +++ b/AsbCloudWebApi/Controllers/BackgroundWork.cs @@ -0,0 +1,34 @@ +using AsbCloudApp.Data; +using AsbCloudInfrastructure.Background; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Linq; + +namespace AsbCloudWebApi.Controllers +{ + [Route("api/[controller]")] + [Authorize] + [ApiController] + public class BackgroundWork : ControllerBase + { + private readonly BackgroundWorker backgroundWorker; + + public BackgroundWork(BackgroundWorker backgroundWorker) + { + this.backgroundWorker = backgroundWorker; + } + + [HttpGet] + //[ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] + public IActionResult GetAll() + { + var result = new { + CurrentWork = (BackgroundWorkDto?)backgroundWorker.CurrentWork, + RunOnceQueue = backgroundWorker.WorkStore.RunOnceQueue.Select(work => (BackgroundWorkDto)work), + Periodics = backgroundWorker.WorkStore.Periodics.Select(work => (BackgroundWorkDto)work.Work), + Felled = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work), + }; + return Ok(result); + } + } +} diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index 143055e5..962e5e0f 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -33,7 +33,7 @@ public class SignalRNotificationTransportService : INotificationTransportService return Task.CompletedTask; var workAction = MakeSignalRSendWorkAction(notifications); - var work = new Work(workId, workAction); + var work = Work.CreateByDelegate(workId, workAction); backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); return Task.CompletedTask; diff --git a/ConsoleApp1/DebugWellOperationsStatService.cs b/ConsoleApp1/DebugWellOperationsStatService.cs deleted file mode 100644 index acb6452c..00000000 --- a/ConsoleApp1/DebugWellOperationsStatService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using AsbCloudApp.Data; -using System; -using System.Collections.Generic; - -namespace ConsoleApp1 -{ - public static class DebugWellOperationsStatService - { - public static void Main(/*string[] args*/) - { - //var options = new DbContextOptionsBuilder() - // .UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True") - // .Options; - //using var db = new AsbCloudDbContext(options); - //var cacheDb = new CacheDb(); - //var telemetryService = new TelemetryService(db, new TelemetryTracker(cacheDb), cacheDb); - //var wellService = new WellService(db, telemetryService, cacheDb); - //var wellOptsStat = new OperationsStatService(db, cacheDb, wellService); - //var tvd = wellOptsStat.GetTvdAsync(1, default).Result; - //Print(tvd); - } - - private static void Print(IEnumerable> tvd) - { - Console.WriteLine("|\tplan\t|\tfact\t|\tprog\t|"); - Console.WriteLine("|:-------------:|:-------------:|:-------------:|"); - foreach (var item in tvd) - Print(item); - } - - private static void Print(PlanFactPredictBase item) - { - static string GetText(WellOperationDto item) - => (item is null) - ? " --------- " - : $"{item.IdCategory} d:{item.DepthStart} "; - Console.WriteLine($"|\t{GetText(item.Plan)}\t|\t{GetText(item.Fact)}\t|\t{GetText(item.Predict)}\t|"); - } - } -}