From 89e0495d099d9b68145cd68ef38d369b47c52183 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 14:48:23 +0500 Subject: [PATCH] BackgroundWorker adapt other services to this one. --- AsbCloudApp/Services/FileService.cs | 4 +- .../Services/IBackgroundWorkerService.cs | 50 ----- AsbCloudApp/Services/IReportService.cs | 5 +- AsbCloudInfrastructure/DependencyInjection.cs | 3 +- .../ReportDataSourcePgCloud.cs | 5 +- .../Background/BackgroundWorkerService.cs | 88 -------- .../Services/Background/WorkBase.cs | 69 ------- .../Services/Background/WorkPeriodic.cs | 36 ---- .../Services/Background/WorkQueue.cs | 101 --------- .../Services/BackgroundWorkerService.cs | 193 ------------------ .../DrillingProgram/DrillingProgramService.cs | 61 +++--- .../Services/Email/EmailService.cs | 46 +++-- .../Services/ReportService.cs | 77 +++---- .../DrillingProgramServiceTest.cs | 33 +-- .../Controllers/ReportController.cs | 2 +- 15 files changed, 125 insertions(+), 648 deletions(-) delete mode 100644 AsbCloudApp/Services/IBackgroundWorkerService.cs delete mode 100644 AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs delete mode 100644 AsbCloudInfrastructure/Services/Background/WorkBase.cs delete mode 100644 AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs delete mode 100644 AsbCloudInfrastructure/Services/Background/WorkQueue.cs delete mode 100644 AsbCloudInfrastructure/Services/BackgroundWorkerService.cs diff --git a/AsbCloudApp/Services/FileService.cs b/AsbCloudApp/Services/FileService.cs index 9b7ca156..d361cf38 100644 --- a/AsbCloudApp/Services/FileService.cs +++ b/AsbCloudApp/Services/FileService.cs @@ -73,7 +73,7 @@ namespace AsbCloudApp.Services /// /// /// - public async Task SaveAsync(int idWell, int? idUser, int idCategory, + public async Task SaveAsync(int idWell, int? idUser, int idCategory, string fileFullName, Stream fileStream, CancellationToken token) { //save info to db @@ -93,7 +93,7 @@ namespace AsbCloudApp.Services string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, fileFullName, fileId); await fileStorageRepository.SaveFileAsync(filePath, fileStream, token); - return await GetOrDefaultAsync(fileId, token); + return (await GetOrDefaultAsync(fileId, token))!; } /// diff --git a/AsbCloudApp/Services/IBackgroundWorkerService.cs b/AsbCloudApp/Services/IBackgroundWorkerService.cs deleted file mode 100644 index 2ccc47ab..00000000 --- a/AsbCloudApp/Services/IBackgroundWorkerService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudApp.Services -{ - /// - /// Сервис выстраивает очередь из фоновых задач. Ограничивает количество одновременно выполняющихся задач. - /// - public interface IBackgroundWorkerService - { - /// - /// Проверка, есть ли задача в очереди - /// - /// идентификатор задачи - /// - bool Contains(string id); - - /// - /// Добавляет в очередь задач новую задачу - /// - /// идентификатор задачи - /// делегат - /// id задачи в очереди - string Enqueue(string id, Func func); - - /// - /// Добавляет в очередь задач новую задачу - /// - /// - /// - string Enqueue(Func func); - - /// - /// Добавляет в очередь задач новую задачу - /// - /// идентификатор задачи - /// - /// - /// - string Enqueue(string id, Func func, Func onError); - - /// - /// Пробуем удалить задачу по идентификатору - /// - /// - /// - bool TryRemove(string id); - } -} \ No newline at end of file diff --git a/AsbCloudApp/Services/IReportService.cs b/AsbCloudApp/Services/IReportService.cs index c66861b7..1f4a7d35 100644 --- a/AsbCloudApp/Services/IReportService.cs +++ b/AsbCloudApp/Services/IReportService.cs @@ -16,7 +16,6 @@ namespace AsbCloudApp.Services /// int ReportCategoryId { get; } - // TODO: rename this method /// /// Поставить рапорт в очередь на формирование /// @@ -28,7 +27,7 @@ namespace AsbCloudApp.Services /// /// /// - string CreateReport(int idWell, int idUser, int stepSeconds, + string EnqueueCreateReportWork(int idWell, int idUser, int stepSeconds, int format, DateTime begin, DateTime end, Action handleReportProgress); @@ -49,7 +48,7 @@ namespace AsbCloudApp.Services /// /// /// - DatesRangeDto GetDatesRangeOrDefault(int idWell); + DatesRangeDto? GetDatesRangeOrDefault(int idWell); /// /// Список готовых рапортов diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 90c1bf5c..a66ebe9e 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -6,6 +6,7 @@ using AsbCloudApp.Services; using AsbCloudApp.Services.Subsystems; using AsbCloudDb.Model; using AsbCloudDb.Model.Subsystems; +using AsbCloudInfrastructure.Background; using AsbCloudInfrastructure.Repository; using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services.DailyReport; @@ -106,7 +107,7 @@ namespace AsbCloudInfrastructure services.AddSingleton(provider=> TelemetryDataCache.GetInstance(configuration)); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(provider => ReduceSamplingService.GetInstance(configuration)); services.AddTransient(); diff --git a/AsbCloudInfrastructure/ReportDataSourcePgCloud.cs b/AsbCloudInfrastructure/ReportDataSourcePgCloud.cs index b42f44cd..f85a3108 100644 --- a/AsbCloudInfrastructure/ReportDataSourcePgCloud.cs +++ b/AsbCloudInfrastructure/ReportDataSourcePgCloud.cs @@ -10,7 +10,7 @@ namespace AsbCloudInfrastructure { public class ReportDataSourcePgCloud : IReportDataSource { - private readonly AsbCloudDbContext context; + private readonly IAsbCloudDbContext context; private readonly int? idTelemetry; private readonly WellInfoReport info; @@ -25,7 +25,7 @@ namespace AsbCloudInfrastructure {3, "Информация"}, }; - public ReportDataSourcePgCloud(AsbCloudDbContext context, int idWell) + public ReportDataSourcePgCloud(IAsbCloudDbContext context, int idWell) { this.context = context; @@ -65,6 +65,7 @@ namespace AsbCloudInfrastructure public AnalyzeResult Analyze() { + // TODO: Replace by linq methods. var messagesStat = (from item in context.TelemetryMessages where item.IdTelemetry == idTelemetry group item.DateTime by item.IdTelemetry into g diff --git a/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs deleted file mode 100644 index 52bcbf6b..00000000 --- a/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudInfrastructure.Services.Background -{ -# nullable enable - /// - /// Сервис для фонового выполнения работы - /// - public class BackgroundWorkerService : BackgroundService - { - 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 BackgroundWorkerService(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } - - /// - /// Добавление задачи в очередь. - /// Не периодические задачи будут выполняться вперед. - /// - /// - /// Id mast be unique - public void Push(WorkBase work) - { - workQueue.Push(work); - } - - /// - /// Удаление работы по ID - /// - /// - /// - public bool Delete(string id) - { - return workQueue.Delete(id); - } - - protected override async Task ExecuteAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - var dateStart = DateTime.Now; - var work = workQueue.Pop(); - if (work is null) - { - await Task.Delay(executePeriod, token); - continue; - } - - using (IServiceScope scope = serviceProvider.CreateScope()) - { - try - { - 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 Task.Delay(minDelay, token); - } - } - } -#nullable disable -} diff --git a/AsbCloudInfrastructure/Services/Background/WorkBase.cs b/AsbCloudInfrastructure/Services/Background/WorkBase.cs deleted file mode 100644 index 7f305bea..00000000 --- a/AsbCloudInfrastructure/Services/Background/WorkBase.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudInfrastructure.Services.Background -{ -#nullable enable - /// - /// Класс разовой работы. - /// Разовая работа приоритетнее периодической. - /// - public class WorkBase - { - /// - /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. - /// - public string Id { get; private set; } - - /// - /// Делегат работы. - /// - /// Параметры: - /// - /// - /// string - /// Id Идентификатор работы - /// - /// - /// IServiceProvider - /// Поставщик сервисов - /// - /// - /// CancellationToken - /// Токен отмены задачи - /// - /// - /// - /// - public Func ActionAsync { get; set; } - - /// - /// Делегат обработки ошибки. - /// Не должен выполняться долго. - /// - public Func? OnErrorAsync { get; set; } - - /// - /// максимально допустимое время выполнения работы - /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// Фактическое время успешного выполнения работы - /// - public TimeSpan? ExecutionTime { get; set; } - - /// - /// Время последнего запуска - /// - public DateTime LastStart { get; set; } - - public WorkBase(string id, Func actionAsync) - { - Id = id; - ActionAsync = actionAsync; - } - } -#nullable disable -} diff --git a/AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs b/AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs deleted file mode 100644 index 288a37ca..00000000 --- a/AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudInfrastructure.Services.Background -{ -#nullable enable - /// - /// Класс периодической работы. - /// - public class WorkPeriodic : WorkBase - { - /// - /// Период выполнения задачи - /// - public TimeSpan Period { get; set; } - - /// - /// Время следующего запуска - /// - public DateTime NextStart => LastStart + Period; - - /// - /// Класс периодической работы - /// - /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки - /// Делегат работы - /// Период выполнения задачи - public WorkPeriodic(string id, Func actionAsync, TimeSpan period) - :base(id, actionAsync) - { - Period = period; - } - } -#nullable disable -} diff --git a/AsbCloudInfrastructure/Services/Background/WorkQueue.cs b/AsbCloudInfrastructure/Services/Background/WorkQueue.cs deleted file mode 100644 index 90ce7ec7..00000000 --- a/AsbCloudInfrastructure/Services/Background/WorkQueue.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace AsbCloudInfrastructure.Services.Background -{ -#nullable enable - /// - /// - /// Очередь работ - /// - /// Не периодические задачи будут возвращаться первыми, как самые приоритетные. - /// - class WorkQueue - { - private Queue Primary = new (8); - private readonly List Periodic = new (8); - - /// - /// Добавление работы. - /// - /// - /// 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; - } - - /// - /// - /// Возвращает приоритетную задачу. - /// - /// - /// Если приоритетные закончились, то ищет ближайшую периодическую. - /// Если до старта ближайшей периодической работы меньше 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; - - work.LastStart = DateTime.Now; - return work; - } - - private WorkPeriodic? GetNextPeriodic() - { - var work = Periodic - .OrderBy(w => w.NextStart) - .ThenByDescending(w => w.Period) - .FirstOrDefault(); - return work; - } - } -#nullable disable -} diff --git a/AsbCloudInfrastructure/Services/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Services/BackgroundWorkerService.cs deleted file mode 100644 index ab4d723a..00000000 --- a/AsbCloudInfrastructure/Services/BackgroundWorkerService.cs +++ /dev/null @@ -1,193 +0,0 @@ -using AsbCloudApp.Services; -using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace AsbCloudInfrastructure.Services -{ - /// - /// Сервис выстраивает очередь из фоновых задач. Ограничивает количество одновременно выполняющихся задач. - /// - public class BackgroundWorkerService : IDisposable, IBackgroundWorkerService - { - private readonly Worker[] workers; - private readonly Dictionary works = new Dictionary(); - private bool isRunning = false; - private CancellationTokenSource cts; - private Task task; - - public BackgroundWorkerService(IConfiguration configuration) - { - var workersCount = configuration.GetValue("BackgroundWorkersCount", 4); - workers = new Worker[workersCount]; - for (int i = 0; i < workers.Length; i++) - workers[i] = new Worker(); - } - - ~BackgroundWorkerService() - { - Dispose(); - } - - public string Enqueue(Func func) - { - var work = new Work - { - ActionAsync = func - }; - return Enqueue(work); - } - - public string Enqueue(string id, Func func) - { - var work = new Work(id, func); - return Enqueue(work); - } - - public string Enqueue(string id, Func func, Func onError) - { - var work = new Work(id, func) - { - OnErrorAsync = onError - }; - return Enqueue(work); - } - - string Enqueue(Work work) - { - works[work.Id] = work; - if (!isRunning) - { - isRunning = true; - cts = new CancellationTokenSource(); - task = Task.Run(() => ExecuteAsync(cts.Token), cts.Token); - } - return work.Id; - } - - private Work Dequeue() - { - var item = works.First(); - works.Remove(item.Key); - return item.Value; - } - - public bool TryRemove(string id) - => works.Remove(id); - - public bool Contains(string id) - => works.ContainsKey(id); - - protected async Task ExecuteAsync(CancellationToken token) - { - while (works.Any() && !token.IsCancellationRequested) - { - var freeworker = workers.FirstOrDefault(w => !w.IsBusy); - if (freeworker is not null) - { - var work = Dequeue(); - freeworker.Start(work); - } - else - await Task.Delay(10, token).ConfigureAwait(false); - } - isRunning = false; - } - - public void Dispose() - { - cts?.Cancel(); - task?.Wait(1); - task?.Dispose(); - cts?.Dispose(); - task = null; - cts = null; - GC.SuppressFinalize(this); - } - } - - class Worker : IDisposable - { - private CancellationTokenSource cts; - private Task task; - public bool IsBusy { get; private set; } - - ~Worker() - { - Dispose(); - } - - public void Dispose() - { - Stop(); - GC.SuppressFinalize(this); - } - - public void Start(Work work) - { - IsBusy = true; - cts = new CancellationTokenSource(); - task = Task.Run(async () => - { - try - { - var actionTask = work.ActionAsync(work.Id, cts.Token); - await actionTask.WaitAsync(TimeSpan.FromMinutes(2), cts.Token); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - - if (work.OnErrorAsync is not null) - { - try - { - await work.OnErrorAsync(work.Id, ex, cts.Token).ConfigureAwait(false); - } - catch (Exception exOnErrorHandler) - { - Trace.TraceError(exOnErrorHandler.Message); - } - } - } - finally - { - cts?.Dispose(); - cts = null; - IsBusy = false; - } - }, cts.Token); - } - - public void Stop() - { - cts?.Cancel(); - task?.Wait(1); - task = null; - cts?.Dispose(); - cts = null; - IsBusy = false; - } - } - class Work - { - public string Id { get; private set; } - public Func ActionAsync { get; set; } - public Func OnErrorAsync { get; set; } - - public Work() - { - Id = Guid.NewGuid().ToString(); - } - - public Work(string id, Func actionAsync) - { - Id = id; - ActionAsync = actionAsync; - } - } -} diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index b1a6d0c6..9ef92fd8 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -3,10 +3,11 @@ using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Services; using AsbCloudDb.Model; -using AsbCloudInfrastructure.Repository; +using AsbCloudInfrastructure.Background; using Mapster; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; @@ -16,6 +17,7 @@ using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.DrillingProgram { +# nullable enable public class DrillingProgramService : IDrillingProgramService { private static readonly Dictionary drillingProgramCreateErrors = new Dictionary(); @@ -25,9 +27,8 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private readonly IUserRepository userRepository; private readonly IWellService wellService; private readonly IConfiguration configuration; - private readonly IBackgroundWorkerService backgroundWorker; + private readonly BackgroundWorker backgroundWorker; private readonly IEmailService emailService; - private readonly string connectionString; private const int idFileCategoryDrillingProgram = 1000; private const int idFileCategoryDrillingProgramPartsStart = 1001; @@ -55,7 +56,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram IUserRepository userRepository, IWellService wellService, IConfiguration configuration, - IBackgroundWorkerService backgroundWorker, + BackgroundWorker backgroundWorker, IEmailService emailService) { this.context = context; @@ -64,7 +65,6 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram this.wellService = wellService; this.configuration = configuration; this.backgroundWorker = backgroundWorker; - this.connectionString = configuration.GetConnectionString("DefaultConnection"); this.emailService = emailService; } @@ -127,7 +127,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram { Parts = parts, Program = files.FirstOrDefault(f => f.IdCategory == idFileCategoryDrillingProgram) - .Adapt(), + ?.Adapt(), PermissionToEdit = userRepository.HasPermission(idUser, "DrillingProgram.edit"), }; @@ -157,7 +157,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram else state.IdState = idStateNotInitialized; - await TryEnqueueMakeProgramAsync(idWell, state, token); + await EnqueueMakeProgramWorkAsync(idWell, state, token); return state; } @@ -299,7 +299,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram .AsNoTracking() .FirstOrDefaultAsync(p => p.IdWell == fileInfo.IdWell && p.IdFileCategory == fileInfo.IdCategory, token); - var user = part.RelatedUsers.FirstOrDefault(r => r.IdUser == idUser && r.IdUserRole == idUserRoleApprover)?.User; + var user = part?.RelatedUsers.FirstOrDefault(r => r.IdUser == idUser && r.IdUserRole == idUserRoleApprover)?.User; if (user is null) throw new ForbidException($"User {idUser} is not in the approvers list."); @@ -323,11 +323,11 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram else { // если все согласованты согласовали - оповещаем публикатора - var approvers = part.RelatedUsers + var approvers = part!.RelatedUsers .Where(u => u.IdUserRole == idUserRoleApprover); if (approvers .All(user => fileInfo.FileMarks - .Any(mark => (mark.IdMarkType == idMarkTypeApprove && mark.User.Id == user.IdUser && !mark.IsDeleted)) || + ?.Any(mark => (mark.IdMarkType == idMarkTypeApprove && mark.User.Id == user.IdUser && !mark.IsDeleted)) == true || (fileMarkDto.IdMarkType == idMarkTypeApprove && user.IdUser == idUser))) { await NotifyPublisherOnFullAccepAsync(fileMarkDto, token); @@ -359,7 +359,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken token) { var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, token); - var well = await wellService.GetOrDefaultAsync(file.IdWell, token); + var well = await wellService.GetOrDefaultAsync(file!.IdWell, token); var user = file.Author; var factory = new DrillingMailBodyFactory(configuration); var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован"); @@ -371,7 +371,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token) { var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, token); - var well = await wellService.GetOrDefaultAsync(file.IdWell, token); + var well = await wellService.GetOrDefaultAsync(file!.IdWell, token); var user = file.Author; var factory = new DrillingMailBodyFactory(configuration); var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен"); @@ -405,12 +405,12 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram emailService.EnqueueSend(user.Email, subject, body); } - private DrillingProgramPartDto ConvertPart(int idUser, List fileCategories, List files, DrillingProgramPart partEntity, double timezoneOffset) + private static DrillingProgramPartDto ConvertPart(int idUser, List fileCategories, List files, DrillingProgramPart partEntity, double timezoneOffset) { var part = new DrillingProgramPartDto { IdFileCategory = partEntity.IdFileCategory, - Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory).Name, + Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory)!.Name, Approvers = partEntity.RelatedUsers .Where(r => r.IdUserRole == idUserRoleApprover) .Select(r => r.User.Adapt()), @@ -464,31 +464,27 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram return part; } - private async Task TryEnqueueMakeProgramAsync(int idWell, DrillingProgramStateDto state, CancellationToken token) + private async Task EnqueueMakeProgramWorkAsync(int idWell, DrillingProgramStateDto state, CancellationToken token) { if (state.IdState == idStateCreating) { var workId = MakeWorkId(idWell); if (!backgroundWorker.Contains(workId)) { - var well = await wellService.GetOrDefaultAsync(idWell, token); + var well = (await wellService.GetOrDefaultAsync(idWell, token))!; var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx"; var tempResultFilePath = Path.Combine(Path.GetTempPath(), "drillingProgram", resultFileName); - async Task funcProgramMake(string id, CancellationToken token) + + var workAction = async (string workId, IServiceProvider serviceProvider, CancellationToken token) => { - var contextOptions = new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options; - using var context = new AsbCloudDbContext(contextOptions); - var fileRepository = new FileRepository(context); - var fileStorageRepository = new FileStorageRepository(); - var fileService = new FileService(fileRepository, fileStorageRepository); + var context = serviceProvider.GetRequiredService(); + var fileService = serviceProvider.GetRequiredService(); var files = state.Parts.Select(p => fileService.GetUrl(p.File)); DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath, state.Parts, well); await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token); - } + }; - Task funcOnErrorProgramMake(string workId, Exception exception, CancellationToken token) + var onErrorAction = (string workId, Exception exception, CancellationToken token) => { var message = $"Не удалось сформировать программу бурения по скважине {well?.Caption}"; drillingProgramCreateErrors[workId] = new() @@ -497,9 +493,15 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram Exception = exception.Message, }; return Task.CompletedTask; - } + }; - backgroundWorker.Enqueue(workId, funcProgramMake, funcOnErrorProgramMake); + var work = new WorkBase(workId, workAction) + { + ExecutionTime = TimeSpan.FromMinutes(1), + OnErrorAsync = onErrorAction + }; + + backgroundWorker.Push(work); } } } @@ -513,7 +515,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token) { var workId = MakeWorkId(idWell); - backgroundWorker.TryRemove(workId); + backgroundWorker.Delete(workId); var filesIds = await context.Files .Where(f => f.IdWell == idWell && @@ -529,4 +531,5 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private static string MakeWorkId(int idWell) => $"Make drilling program for wellId {idWell}"; } +#nullable disable } \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Email/EmailService.cs b/AsbCloudInfrastructure/Services/Email/EmailService.cs index 84a1f73f..568d4098 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailService.cs @@ -8,26 +8,28 @@ using System.Linq; using System.Net.Mail; using System.Threading; using System.Threading.Tasks; +using AsbCloudInfrastructure.Background; namespace AsbCloudInfrastructure.Services { +#nullable enable public class EmailService : IEmailService { - private readonly IBackgroundWorkerService backgroundWorker; + private readonly BackgroundWorker backgroundWorker; private readonly bool IsConfigured; private readonly string sender; private readonly string smtpServer; private readonly string smtpPassword; - public EmailService(IBackgroundWorkerService backgroundWorker, IConfiguration configuration) + public EmailService(BackgroundWorker backgroundWorker, IConfiguration configuration) { - sender = configuration.GetValue("email:sender", null); - smtpPassword = configuration.GetValue("email:password", null); - smtpServer = configuration.GetValue("email:smtpServer", null); + sender = configuration.GetValue("email:sender", string.Empty); + smtpPassword = configuration.GetValue("email:password", string.Empty); + smtpServer = configuration.GetValue("email:smtpServer", string.Empty); - var configError = (string.IsNullOrEmpty(sender) || + var configError = string.IsNullOrEmpty(sender) || string.IsNullOrEmpty(smtpPassword) || - string.IsNullOrEmpty(smtpServer)); + string.IsNullOrEmpty(smtpServer); IsConfigured = !configError; @@ -44,20 +46,21 @@ namespace AsbCloudInfrastructure.Services Trace.TraceWarning("smtp is not configured"); return; } - var jobId = CalcJobId(addresses, subject, htmlBody); - if (!backgroundWorker.Contains(jobId)) + var workId = MakeWorkId(addresses, subject, htmlBody); + if (!backgroundWorker.Contains(workId)) { - var action = MakeEmailSendJobAsync(addresses, subject, htmlBody); - backgroundWorker.Enqueue(jobId, action); + var workAction = MakeEmailSendWorkAction(addresses, subject, htmlBody); + var work = new WorkBase(workId, workAction); + backgroundWorker.Push(work); } } - private Func MakeEmailSendJobAsync(IEnumerable addresses, string subject, string htmlBody) + private Func MakeEmailSendWorkAction(IEnumerable addresses, string subject, string htmlBody) { var mailAddresses = new List(); foreach (var address in addresses) { - if (MailAddress.TryCreate(address, out MailAddress mailAddress)) + if (MailAddress.TryCreate(address, out MailAddress? mailAddress)) mailAddresses.Add(mailAddress); else Trace.TraceWarning($"Mail {address} is not correct."); @@ -69,16 +72,16 @@ namespace AsbCloudInfrastructure.Services if (string.IsNullOrEmpty(subject)) throw new ArgumentInvalidException($"{nameof(subject)} should be set", nameof(subject)); - var func = async (string id, CancellationToken token) => + var workAction = async (string id, IServiceProvider serviceProvider, CancellationToken token) => { var from = new MailAddress(sender); - - var message = new MailMessage(); - message.From = from; + var message = new MailMessage + { + From = from + }; foreach (var mailAddress in mailAddresses) message.To.Add(mailAddress); - //message.To.Add("support@digitaldrilling.ru"); message.BodyEncoding = System.Text.Encoding.UTF8; message.Body = htmlBody; @@ -91,12 +94,12 @@ namespace AsbCloudInfrastructure.Services client.Credentials = new System.Net.NetworkCredential(sender, smtpPassword); await client.SendMailAsync(message, token); - Trace.TraceInformation($"Send email to {string.Join(',', addresses)} subj:{subject} html body count {htmlBody.Count()}"); + Trace.TraceInformation($"Send email to {string.Join(',', addresses)} subj:{subject} html body count {htmlBody.Length}"); }; - return func; + return workAction; } - private string CalcJobId(IEnumerable addresses, string subject, string content) + private static string MakeWorkId(IEnumerable addresses, string subject, string content) { var hash = GetHashCode(addresses); hash ^= subject.GetHashCode(); @@ -114,4 +117,5 @@ namespace AsbCloudInfrastructure.Services return hash; } } +#nullable disable } diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index 7143e063..c46d4682 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -1,11 +1,11 @@ using AsbCloudApp.Data; using AsbCloudApp.Services; using AsbCloudDb.Model; -using AsbCloudInfrastructure.Repository; +using AsbCloudInfrastructure.Background; using AsbSaubReport; using Mapster; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; @@ -15,30 +15,32 @@ using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services { +#nullable enable public class ReportService : IReportService { private readonly IAsbCloudDbContext db; - private readonly string connectionString; private readonly ITelemetryService telemetryService; private readonly IWellService wellService; - private readonly IBackgroundWorkerService backgroundWorkerService; - - public ReportService(IAsbCloudDbContext db, IConfiguration configuration, - ITelemetryService telemetryService, IWellService wellService, IBackgroundWorkerService backgroundWorkerService) - { - this.db = db; - this.connectionString = configuration.GetConnectionString("DefaultConnection"); - this.wellService = wellService; - this.backgroundWorkerService = backgroundWorkerService; - this.telemetryService = telemetryService; - ReportCategoryId = db.FileCategories.AsNoTracking() - .FirstOrDefault(c => - c.Name.Equals("Рапорт")).Id; - } + private readonly BackgroundWorker backgroundWorkerService; public int ReportCategoryId { get; private set; } - public string CreateReport(int idWell, int idUser, int stepSeconds, int format, DateTime begin, + public ReportService(IAsbCloudDbContext db, + ITelemetryService telemetryService, + IWellService wellService, + BackgroundWorker backgroundWorkerService) + { + this.db = db; + this.wellService = wellService; + this.backgroundWorkerService = backgroundWorkerService; + this.telemetryService = telemetryService; + ReportCategoryId = db.FileCategories + .AsNoTracking() + .First(c => c.Name.Equals("Рапорт")) + .Id; + } + + public string EnqueueCreateReportWork(int idWell, int idUser, int stepSeconds, int format, DateTime begin, DateTime end, Action progressHandler) { var timezoneOffset = wellService.GetTimezone(idWell).Hours; @@ -47,12 +49,12 @@ namespace AsbCloudInfrastructure.Services var beginRemote = begin.ToTimeZoneOffsetHours(timezoneOffset); var endRemote = end.ToTimeZoneOffsetHours(timezoneOffset); - var newReportId = backgroundWorkerService.Enqueue(async (id, token) => + var workId = $"create report by wellid:{idWell} for userid:{idUser} requested at {DateTime.Now}"; + + var workAction = async (string id, IServiceProvider serviceProvider, CancellationToken token) => { - var contextOptions = new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options; - using var context = new AsbCloudDbContext(contextOptions); + using var context = serviceProvider.GetRequiredService(); + var fileService = serviceProvider.GetRequiredService(); var tempDir = Path.Combine(Path.GetTempPath(), "report"); @@ -65,11 +67,8 @@ namespace AsbCloudInfrastructure.Services progressHandler.Invoke(e.Adapt(), id); }; generator.Make(reportFileName); - - var fileRepository = new FileRepository(context); - var fileStorageRepository = new FileStorageRepository(); - var fileService = new FileService(fileRepository, fileStorageRepository); - var fileInfo = await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token); + + var fileInfo = (await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token))!; progressHandler.Invoke(new { @@ -91,13 +90,17 @@ namespace AsbCloudInfrastructure.Services }; context.ReportProperties.Add(newReportProperties); context.SaveChanges(); - }); + }; + + var work = new WorkBase(workId, workAction); + backgroundWorkerService.Push(work); + progressHandler.Invoke(new ReportProgressDto { Operation = "Ожидает начала в очереди.", Progress = 0f, - }, newReportId); - return newReportId; + }, workId); + return workId; } public int GetReportPagesCount(int idWell, DateTime begin, DateTime end, int stepSeconds, int format) @@ -106,12 +109,12 @@ namespace AsbCloudInfrastructure.Services var beginRemote = begin.ToTimeZoneOffsetHours(timezoneOffset); var endRemote = end.ToTimeZoneOffsetHours(timezoneOffset); - var generator = GetReportGenerator(idWell, beginRemote, endRemote, stepSeconds, format, (AsbCloudDbContext)db); + var generator = GetReportGenerator(idWell, beginRemote, endRemote, stepSeconds, format, db); var pagesCount = generator.GetPagesCount(); return pagesCount; } - public DatesRangeDto GetDatesRangeOrDefault(int idWell) + public DatesRangeDto? GetDatesRangeOrDefault(int idWell) { var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); if (idTelemetry is null) @@ -128,8 +131,8 @@ namespace AsbCloudInfrastructure.Services .OrderBy(o => o.File.UploadDate) .AsNoTracking() .Take(1024); - var properties = await propertiesQuery.ToListAsync(token); - return properties.Select(p => new ReportPropertiesDto + var entities = await propertiesQuery.ToListAsync(token); + var dtos = entities.Select(p => new ReportPropertiesDto { Id = p.Id, Name = p.File.Name, @@ -151,10 +154,11 @@ namespace AsbCloudInfrastructure.Services Step = p.Step, Format = p.Format == 0 ? ".pdf" : ".las" }); + return dtos; } private static IReportGenerator GetReportGenerator(int idWell, DateTime begin, - DateTime end, int stepSeconds, int format, AsbCloudDbContext context) + DateTime end, int stepSeconds, int format, IAsbCloudDbContext context) { var dataSource = new ReportDataSourcePgCloud(context, idWell); IReportGenerator generator = format switch @@ -173,4 +177,5 @@ namespace AsbCloudInfrastructure.Services return generator; } } +#nullable disable } diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs index e823189b..05833333 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs @@ -2,7 +2,7 @@ using AsbCloudApp.Repositories; using AsbCloudApp.Services; using AsbCloudDb.Model; -using AsbCloudInfrastructure.Repository; +using AsbCloudInfrastructure.Background; using AsbCloudInfrastructure.Services.DrillingProgram; using Mapster; using Microsoft.Extensions.Configuration; @@ -83,8 +83,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests private readonly Mock userRepositoryMock; private readonly Mock wellServiceMock; private readonly Mock configurationMock; - private readonly Mock backgroundWorkerMock; - private readonly Mock emailService; + private readonly Mock backgroundWorkerMock; + private readonly Mock emailServiceMock; public DrillingProgramServiceTest() { @@ -102,7 +102,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests userRepositoryMock = new Mock(); wellServiceMock = new Mock(); configurationMock = new Mock(); - backgroundWorkerMock = new Mock(); + backgroundWorkerMock = new Mock(); + emailServiceMock = new Mock(); } [Fact] @@ -115,7 +116,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var users = await service.GetAvailableUsers(idWell, CancellationToken.None); @@ -132,7 +133,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var result = await service.AddPartsAsync(idWell, new int[] { 1001, 1002 }, CancellationToken.None); @@ -151,7 +152,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var result = await service.RemovePartsAsync(idWell, new int[] { 1005 }, CancellationToken.None); @@ -174,7 +175,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var result = await service.AddUserAsync(idWell, 1001, publisher1.Id, 1, CancellationToken.None); @@ -209,7 +210,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var result = await service.RemoveUserAsync(idWell, idFileCategory, publisher1.Id, idUserRole, CancellationToken.None); @@ -235,7 +236,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var fileMark = new FileMarkDto { @@ -266,7 +267,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var fileMark = new FileMarkDto { IdFile = file1001.Id, @@ -304,7 +305,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var fileMark = new FileMarkDto { @@ -331,7 +332,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); @@ -358,12 +359,12 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); Assert.Equal(2, state.IdState); - backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny>())); + backgroundWorkerMock.Verify(s => s.Push(It.IsAny())); } [Fact] @@ -388,7 +389,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests wellServiceMock.Object, configurationMock.Object, backgroundWorkerMock.Object, - emailService.Object); + emailServiceMock.Object); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); diff --git a/AsbCloudWebApi/Controllers/ReportController.cs b/AsbCloudWebApi/Controllers/ReportController.cs index d3df1092..8b1081ab 100644 --- a/AsbCloudWebApi/Controllers/ReportController.cs +++ b/AsbCloudWebApi/Controllers/ReportController.cs @@ -68,7 +68,7 @@ namespace AsbCloudWebApi.Controllers ).ConfigureAwait(false); }, token); - var id = reportService.CreateReport(idWell, (int)idUser, + var id = reportService.EnqueueCreateReportWork(idWell, (int)idUser, stepSeconds, format, begin, end, HandleReportProgressAsync); return Ok(id);