From f61db91dd25787f5379f5a1fd3dc81605e61d357 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Thu, 1 Dec 2022 17:48:35 +0500 Subject: [PATCH 1/9] Add new background service. --- .../Background/BackgroundWorkerService.cs | 247 ++++++++++++++++++ .../BackgroundWorkerServiceTest.cs | 120 +++++++++ .../BackgroundWorkerService_WorkQueue_Test.cs | 109 ++++++++ 3 files changed, 476 insertions(+) create mode 100644 AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs create mode 100644 AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs create mode 100644 AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs diff --git a/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs new file mode 100644 index 00000000..4f940cfd --- /dev/null +++ b/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs @@ -0,0 +1,247 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +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); + } + + 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); + } + } + } + + /// + /// + /// Очередь работ + /// + /// Не периодические задачи будут возвращаться первыми, как самые приоритетные. + /// + public class WorkQueue + { + private Queue Primary = new (8); + private readonly List Periodic = new (8); + internal TimeSpan MaxTimeToNextWork { get; set; } = TimeSpan.FromSeconds(20); + + /// + /// Добавление работы. + /// + /// + /// 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(TimeSpan? maxTimeToNextWork = null) + { + if (Primary.Any()) + return Primary.Dequeue(); + + var maxTimeToNextWorkLocal = maxTimeToNextWork ?? MaxTimeToNextWork; + var work = GetNextPeriodic(); + if (work is null || work.NextStart - DateTime.Now > maxTimeToNextWorkLocal) + return null; + + work.LastStart = DateTime.Now; + return work; + } + + private WorkPeriodic? GetNextPeriodic() + { + var work = Periodic + .OrderBy(w => w.NextStart) + .FirstOrDefault(); + return work; + } + } + + 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; + } + } + + 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/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs new file mode 100644 index 00000000..3aad83ad --- /dev/null +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System; +using AsbCloudInfrastructure.Services.Background; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Org.BouncyCastle.Asn1.X509.Qualified; + +namespace AsbCloudWebApi.Tests.ServicesTests +{ + public class BackgroundWorkerServiceTest + { + private readonly Mock mockServiceProvider; + private readonly Mock mockServiceScopeFactory; + + public BackgroundWorkerServiceTest() + { + var mockServiceScope = new Mock(); + mockServiceScopeFactory = new Mock(); + mockServiceProvider = new Mock(); + + mockServiceScope.SetReturnsDefault(mockServiceProvider.Object); + mockServiceProvider.SetReturnsDefault(mockServiceScopeFactory.Object); + mockServiceProvider.Setup(s=>s.GetService(It.IsAny())) + .Returns(mockServiceScopeFactory.Object); + mockServiceScopeFactory.SetReturnsDefault(mockServiceScope.Object); + } + + [Fact] + public async Task Push_makes_new_scope_after_start() + { + mockServiceScopeFactory.Invocations.Clear(); + + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var work = new WorkBase("", (_, _, _) => Task.CompletedTask ); + backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(10); + + mockServiceScopeFactory.Verify(f => f.CreateScope()); + } + + [Fact] + public async Task Push_makes_primary_work_done() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var workDone = false; + var work = new WorkBase("", (_, _, _) => + { + workDone = true; + return Task.CompletedTask; + }); + backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(10); + + Assert.True(workDone); + } + + [Fact] + public async Task Push_makes_pperiodic_work_done() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var workDone = false; + var work = new WorkPeriodic("", (_, _, _) => + { + workDone = true; + return Task.CompletedTask; + }, + TimeSpan.FromMilliseconds(10)); + backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(10); + + Assert.True(workDone); + } + + [Fact] + public async Task Aborts_long_work() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var workCanceled = false; + var work = new WorkBase("", async(_, _, _) => await Task.Delay(1000000)); + work.Timeout = TimeSpan.FromMilliseconds(1); + work.OnErrorAsync = async (id, ex, token) => + { + workCanceled = ex is System.TimeoutException; + await Task.CompletedTask; + }; + + backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(20*4); + + Assert.True(workCanceled); + } + + [Fact] + public async Task Execution_continues_after_work_exception() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var work2done = false; + var work1 = new WorkBase("1", (_, _, _) => throw new Exception()); + var work2 = new WorkBase("2", (_, _, _) => + { + work2done = true; + return Task.CompletedTask; + }); + + backgroundService.Push(work1); + backgroundService.Push(work2); + + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(2_100); + + Assert.True(work2done); + } + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs new file mode 100644 index 00000000..1807e24a --- /dev/null +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs @@ -0,0 +1,109 @@ +using AsbCloudInfrastructure.Services.Background; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.ServicesTests +{ + public class BackgroundWorkerService_WorkQueue_Test + { + private readonly TimeSpan period = TimeSpan.FromSeconds(10); + private readonly Func somAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask; + + [Fact] + public void Push_not_unique_id_should_throw() + { + var work1 = new WorkPeriodic("1", somAction, TimeSpan.FromSeconds(30)); + var work2 = new WorkBase("1", somAction); + + var queue = new WorkQueue(); + queue.Push(work1); + + Assert.Throws( + () => queue.Push(work2)); + } + + [Fact] + public void Pop_should_return_null() + { + var work1 = new WorkPeriodic("1", somAction, TimeSpan.FromSeconds(30)) + { LastStart = DateTime.Now }; + + var queue = new WorkQueue(); + queue.Push(work1); + var workpoPoped= queue.Pop(); + + Assert.Null(workpoPoped); + } + + [Fact] + public void Pop_primary_first() + { + var work1 = new WorkBase("1", somAction); + var work2 = new WorkPeriodic("1", somAction, period); + + var queue = new WorkQueue(); + queue.Push(work2); + queue.Push(work1); + var workpoPoped= queue.Pop(); + + Assert.Equal(work1, workpoPoped); + } + + [Fact] + public void Pop_second_after_delete_first() + { + var work1 = new WorkPeriodic("1", somAction, period); + var work2 = new WorkPeriodic("2", somAction, period); + + var queue = new WorkQueue(); + queue.Push(work1); + queue.Push(work2); + queue.Delete("1"); + + var workpoPoped= queue.Pop(); + + Assert.Equal(work2, workpoPoped); + } + + [Fact] + public void Pop_closest_to_nextStart() + { + var work1 = new WorkPeriodic("1", somAction, period) { + LastStart = DateTime.Now, + }; + var work2 = new WorkPeriodic("2", somAction, period); + + var queue = new WorkQueue(); + queue.Push(work1); + queue.Push(work2); + + var workpoPoped= queue.Pop(); + + Assert.Equal(work2, workpoPoped); + } + + [Fact] + public void Pop_closest_to_explicit_nextStart() + { + var baseTime = DateTime.Now - period; + var work1 = new WorkPeriodic("1", somAction, period) + { + LastStart = baseTime - TimeSpan.FromSeconds(-1), + }; + var work2 = new WorkPeriodic("2", somAction, period) + { + LastStart = baseTime, + }; + + var queue = new WorkQueue(); + queue.Push(work1); + queue.Push(work2); + + var workpoPoped= queue.Pop(); + + Assert.Equal(work2, workpoPoped); + } + } +} From 06fe0e09ff1664d5dba8e84af556920a9af73e17 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 10:57:27 +0500 Subject: [PATCH 2/9] BackgroundWorkerService cleanup and improve tests --- .../Background/BackgroundWorkerService.cs | 185 ++---------------- .../Services/Background/WorkBase.cs | 69 +++++++ .../Services/Background/WorkPeriodic.cs | 36 ++++ .../Services/Background/WorkQueue.cs | 101 ++++++++++ .../BackgroundWorkerServiceTest.cs | 116 ++++++++++- .../BackgroundWorkerService_WorkQueue_Test.cs | 109 ----------- 6 files changed, 327 insertions(+), 289 deletions(-) create mode 100644 AsbCloudInfrastructure/Services/Background/WorkBase.cs create mode 100644 AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs create mode 100644 AsbCloudInfrastructure/Services/Background/WorkQueue.cs delete mode 100644 AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs diff --git a/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs index 4f940cfd..52bcbf6b 100644 --- a/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs +++ b/AsbCloudInfrastructure/Services/Background/BackgroundWorkerService.cs @@ -1,15 +1,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; 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); @@ -34,6 +35,16 @@ namespace AsbCloudInfrastructure.Services.Background workQueue.Push(work); } + /// + /// Удаление работы по ID + /// + /// + /// + public bool Delete(string id) + { + return workQueue.Delete(id); + } + protected override async Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) @@ -73,175 +84,5 @@ namespace AsbCloudInfrastructure.Services.Background } } } - - /// - /// - /// Очередь работ - /// - /// Не периодические задачи будут возвращаться первыми, как самые приоритетные. - /// - public class WorkQueue - { - private Queue Primary = new (8); - private readonly List Periodic = new (8); - internal TimeSpan MaxTimeToNextWork { get; set; } = TimeSpan.FromSeconds(20); - - /// - /// Добавление работы. - /// - /// - /// 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(TimeSpan? maxTimeToNextWork = null) - { - if (Primary.Any()) - return Primary.Dequeue(); - - var maxTimeToNextWorkLocal = maxTimeToNextWork ?? MaxTimeToNextWork; - var work = GetNextPeriodic(); - if (work is null || work.NextStart - DateTime.Now > maxTimeToNextWorkLocal) - return null; - - work.LastStart = DateTime.Now; - return work; - } - - private WorkPeriodic? GetNextPeriodic() - { - var work = Periodic - .OrderBy(w => w.NextStart) - .FirstOrDefault(); - return work; - } - } - - 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; - } - } - - 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/WorkBase.cs b/AsbCloudInfrastructure/Services/Background/WorkBase.cs new file mode 100644 index 00000000..7f305bea --- /dev/null +++ b/AsbCloudInfrastructure/Services/Background/WorkBase.cs @@ -0,0 +1,69 @@ +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 new file mode 100644 index 00000000..288a37ca --- /dev/null +++ b/AsbCloudInfrastructure/Services/Background/WorkPeriodic.cs @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..90ce7ec7 --- /dev/null +++ b/AsbCloudInfrastructure/Services/Background/WorkQueue.cs @@ -0,0 +1,101 @@ +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/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs index 3aad83ad..a2feb973 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs @@ -5,7 +5,6 @@ using AsbCloudInfrastructure.Services.Background; using System.Threading; using System.Threading.Tasks; using Xunit; -using Org.BouncyCastle.Asn1.X509.Qualified; namespace AsbCloudWebApi.Tests.ServicesTests { @@ -13,6 +12,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests { private readonly Mock mockServiceProvider; private readonly Mock mockServiceScopeFactory; + private readonly TimeSpan period = TimeSpan.FromSeconds(10); + private readonly Func someAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask; public BackgroundWorkerServiceTest() { @@ -22,7 +23,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests mockServiceScope.SetReturnsDefault(mockServiceProvider.Object); mockServiceProvider.SetReturnsDefault(mockServiceScopeFactory.Object); - mockServiceProvider.Setup(s=>s.GetService(It.IsAny())) + mockServiceProvider.Setup(s => s.GetService(It.IsAny())) .Returns(mockServiceScopeFactory.Object); mockServiceScopeFactory.SetReturnsDefault(mockServiceScope.Object); } @@ -32,8 +33,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests { mockServiceScopeFactory.Invocations.Clear(); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); - var work = new WorkBase("", (_, _, _) => Task.CompletedTask ); + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var work = new WorkBase("", (_, _, _) => Task.CompletedTask); backgroundService.Push(work); await backgroundService.StartAsync(CancellationToken.None); await Task.Delay(10); @@ -42,7 +43,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests } [Fact] - public async Task Push_makes_primary_work_done() + public async Task Makes_primary_work_done() { var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); var workDone = false; @@ -59,7 +60,19 @@ namespace AsbCloudWebApi.Tests.ServicesTests } [Fact] - public async Task Push_makes_pperiodic_work_done() + public async Task Sets_ExecutionTime_after_work_done() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var work = new WorkBase("", someAction); + backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(10); + + Assert.True(work.ExecutionTime > TimeSpan.Zero); + } + + [Fact] + public async Task Makes_periodic_work_done() { var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); var workDone = false; @@ -70,6 +83,80 @@ namespace AsbCloudWebApi.Tests.ServicesTests }, TimeSpan.FromMilliseconds(10)); backgroundService.Push(work); + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(20); + + Assert.True(workDone); + } + + [Fact] + public async Task Does_not_start_periodic_work() + { + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var workDone = false; + var work = new WorkPeriodic("", (_, _, _) => + { + workDone = true; + return Task.CompletedTask; + }, + TimeSpan.FromSeconds(30)); + work.LastStart = DateTime.Now; + backgroundService.Push(work); + + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(20); + + Assert.False(workDone); + } + + [Fact] + public async Task Follows_work_priority() + { + var order = 0; + var work1Order = -1; + var work2Order = -1; + + var work1 = new WorkPeriodic("1", (_, _, _) => + { + work1Order = order++; + return Task.CompletedTask; + }, + TimeSpan.FromMilliseconds(1) + ); + + var work2 = new WorkBase("2", (_, _, _) => + { + work2Order = order++; + return Task.CompletedTask; + }); + + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + backgroundService.Push(work2); + backgroundService.Push(work1); + + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(2_100); + + Assert.True(work2Order < work1Order); + } + + [Fact] + public async Task Runs_second_after_delete_first() + { + var workDone = false; + + var work1 = new WorkBase("1", someAction); + var work2 = new WorkPeriodic("2", (_, _, _) => + { + workDone = true; + return Task.CompletedTask; + }, TimeSpan.FromMilliseconds(1)); + + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + backgroundService.Push(work1); + backgroundService.Push(work2); + backgroundService.Delete("1"); + await backgroundService.StartAsync(CancellationToken.None); await Task.Delay(10); @@ -81,7 +168,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests { var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); var workCanceled = false; - var work = new WorkBase("", async(_, _, _) => await Task.Delay(1000000)); + var work = new WorkBase("", async (_, _, _) => await Task.Delay(1000000)); work.Timeout = TimeSpan.FromMilliseconds(1); work.OnErrorAsync = async (id, ex, token) => { @@ -91,7 +178,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests backgroundService.Push(work); await backgroundService.StartAsync(CancellationToken.None); - await Task.Delay(20*4); + await Task.Delay(20 * 4); Assert.True(workCanceled); } @@ -116,5 +203,18 @@ namespace AsbCloudWebApi.Tests.ServicesTests Assert.True(work2done); } + + [Fact] + public void Push_not_unique_id_should_throw() + { + var work1 = new WorkPeriodic("1", someAction, TimeSpan.FromSeconds(30)); + var work2 = new WorkBase("1", someAction); + + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + backgroundService.Push(work1); + + Assert.Throws( + () => backgroundService.Push(work2)); + } } } diff --git a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs deleted file mode 100644 index 1807e24a..00000000 --- a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerService_WorkQueue_Test.cs +++ /dev/null @@ -1,109 +0,0 @@ -using AsbCloudInfrastructure.Services.Background; -using System; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace AsbCloudWebApi.Tests.ServicesTests -{ - public class BackgroundWorkerService_WorkQueue_Test - { - private readonly TimeSpan period = TimeSpan.FromSeconds(10); - private readonly Func somAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask; - - [Fact] - public void Push_not_unique_id_should_throw() - { - var work1 = new WorkPeriodic("1", somAction, TimeSpan.FromSeconds(30)); - var work2 = new WorkBase("1", somAction); - - var queue = new WorkQueue(); - queue.Push(work1); - - Assert.Throws( - () => queue.Push(work2)); - } - - [Fact] - public void Pop_should_return_null() - { - var work1 = new WorkPeriodic("1", somAction, TimeSpan.FromSeconds(30)) - { LastStart = DateTime.Now }; - - var queue = new WorkQueue(); - queue.Push(work1); - var workpoPoped= queue.Pop(); - - Assert.Null(workpoPoped); - } - - [Fact] - public void Pop_primary_first() - { - var work1 = new WorkBase("1", somAction); - var work2 = new WorkPeriodic("1", somAction, period); - - var queue = new WorkQueue(); - queue.Push(work2); - queue.Push(work1); - var workpoPoped= queue.Pop(); - - Assert.Equal(work1, workpoPoped); - } - - [Fact] - public void Pop_second_after_delete_first() - { - var work1 = new WorkPeriodic("1", somAction, period); - var work2 = new WorkPeriodic("2", somAction, period); - - var queue = new WorkQueue(); - queue.Push(work1); - queue.Push(work2); - queue.Delete("1"); - - var workpoPoped= queue.Pop(); - - Assert.Equal(work2, workpoPoped); - } - - [Fact] - public void Pop_closest_to_nextStart() - { - var work1 = new WorkPeriodic("1", somAction, period) { - LastStart = DateTime.Now, - }; - var work2 = new WorkPeriodic("2", somAction, period); - - var queue = new WorkQueue(); - queue.Push(work1); - queue.Push(work2); - - var workpoPoped= queue.Pop(); - - Assert.Equal(work2, workpoPoped); - } - - [Fact] - public void Pop_closest_to_explicit_nextStart() - { - var baseTime = DateTime.Now - period; - var work1 = new WorkPeriodic("1", somAction, period) - { - LastStart = baseTime - TimeSpan.FromSeconds(-1), - }; - var work2 = new WorkPeriodic("2", somAction, period) - { - LastStart = baseTime, - }; - - var queue = new WorkQueue(); - queue.Push(work1); - queue.Push(work2); - - var workpoPoped= queue.Pop(); - - Assert.Equal(work2, workpoPoped); - } - } -} From c6a1c4dae6c8b0333df22f0b6fb2e0891ac75be7 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 10:58:19 +0500 Subject: [PATCH 3/9] AsbCloudDbContext Add active reference count prop --- AsbCloudDb/Model/AsbCloudDbContext.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/AsbCloudDb/Model/AsbCloudDbContext.cs b/AsbCloudDb/Model/AsbCloudDbContext.cs index 4e91eafe..97df2748 100644 --- a/AsbCloudDb/Model/AsbCloudDbContext.cs +++ b/AsbCloudDb/Model/AsbCloudDbContext.cs @@ -60,20 +60,32 @@ namespace AsbCloudDb.Model public DbSet Record50 => Set(); public DbSet Record60 => Set(); public DbSet Record61 => Set(); - + + public int ReferenceCount { get; private set; } + public AsbCloudDbContext() : base() { + ReferenceCount++; } public AsbCloudDbContext(DbContextOptions options) : base(options) { + ReferenceCount++; + } + + ~AsbCloudDbContext() + { + ReferenceCount--; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + if (!optionsBuilder.IsConfigured) - optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True"); + optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True" + //, builder=>builder.EnableRetryOnFailure(2, System.TimeSpan.FromMinutes(1)) + ); } protected override void OnModelCreating(ModelBuilder modelBuilder) From 98c7599c4b672dcf05b827acc7e2f4e0d0b43618 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 14:45:20 +0500 Subject: [PATCH 4/9] BackgroundWorker move to toplevel folder --- .../Background/BackgroundWorkerService.cs | 100 ++++++++++++++++ AsbCloudInfrastructure/Background/WorkBase.cs | 69 +++++++++++ .../Background/WorkPeriodic.cs | 36 ++++++ .../Background/WorkQueue.cs | 107 ++++++++++++++++++ .../BackgroundWorkerServiceTest.cs | 42 +++++-- 5 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 AsbCloudInfrastructure/Background/BackgroundWorkerService.cs create mode 100644 AsbCloudInfrastructure/Background/WorkBase.cs create mode 100644 AsbCloudInfrastructure/Background/WorkPeriodic.cs create mode 100644 AsbCloudInfrastructure/Background/WorkQueue.cs diff --git a/AsbCloudInfrastructure/Background/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Background/BackgroundWorkerService.cs new file mode 100644 index 00000000..e823284d --- /dev/null +++ b/AsbCloudInfrastructure/Background/BackgroundWorkerService.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Background +{ +# nullable enable + /// + /// Сервис для фонового выполнения работы + /// + public class BackgroundWorker : 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 BackgroundWorker(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; +#warning move StartAsync(CancellationToken.None).Wait() to THE factory + Task.Delay(1_000) + .ContinueWith(_=> StartAsync(CancellationToken.None).Wait()); + } + + /// + /// Добавление задачи в очередь. + /// Не периодические задачи будут выполняться вперед. + /// + /// + /// 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 dateStart = DateTime.Now; + var work = workQueue.Pop(); + if (work is null) + { + await Task.Delay(executePeriod, token); + continue; + } + + using var 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/Background/WorkBase.cs b/AsbCloudInfrastructure/Background/WorkBase.cs new file mode 100644 index 00000000..ce07a2fa --- /dev/null +++ b/AsbCloudInfrastructure/Background/WorkBase.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Background +{ +#nullable enable + /// + /// Класс разовой работы. + /// Разовая работа приоритетнее периодической. + /// + public class WorkBase + { + /// + /// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки. + /// + public string Id { get; private set; } + + /// + /// Делегат работы. + /// + /// Параметры: + /// + /// + /// string + /// Id Идентификатор работы + /// + /// + /// IServiceProvider + /// Поставщик сервисов + /// + /// + /// CancellationToken + /// Токен отмены задачи + /// + /// + /// + /// + internal Func ActionAsync { get; set; } + + /// + /// Делегат обработки ошибки. + /// Не должен выполняться долго. + /// + public Func? OnErrorAsync { get; set; } + + /// + /// максимально допустимое время выполнения работы + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Фактическое время успешного выполнения работы + /// + public TimeSpan? ExecutionTime { get; internal set; } + + /// + /// Время последнего запуска + /// + public DateTime LastStart { get; set; } + + public WorkBase(string id, Func actionAsync) + { + Id = id; + ActionAsync = actionAsync; + } + } +#nullable disable +} diff --git a/AsbCloudInfrastructure/Background/WorkPeriodic.cs b/AsbCloudInfrastructure/Background/WorkPeriodic.cs new file mode 100644 index 00000000..ae29ee78 --- /dev/null +++ b/AsbCloudInfrastructure/Background/WorkPeriodic.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.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/Background/WorkQueue.cs b/AsbCloudInfrastructure/Background/WorkQueue.cs new file mode 100644 index 00000000..5521d373 --- /dev/null +++ b/AsbCloudInfrastructure/Background/WorkQueue.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AsbCloudInfrastructure.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; + } + + public bool Contains(string id) + { + var result = 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; + + 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/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs index a2feb973..24b8e999 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs @@ -12,7 +12,6 @@ namespace AsbCloudWebApi.Tests.ServicesTests { private readonly Mock mockServiceProvider; private readonly Mock mockServiceScopeFactory; - private readonly TimeSpan period = TimeSpan.FromSeconds(10); private readonly Func someAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask; public BackgroundWorkerServiceTest() @@ -28,13 +27,34 @@ namespace AsbCloudWebApi.Tests.ServicesTests mockServiceScopeFactory.SetReturnsDefault(mockServiceScope.Object); } + [Fact] + public void Contains_returns_true() + { + mockServiceScopeFactory.Invocations.Clear(); + + var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + const string work1Id = "long name 1"; + const string work2Id = "long name 2"; + + var work1 = new WorkBase(work1Id, someAction); + var work2 = new WorkPeriodic(work2Id, someAction, TimeSpan.Zero); + + backgroundService.Push(work1); + backgroundService.Push(work2); + + Assert.True(backgroundService.Contains(work1Id)); + Assert.True(backgroundService.Contains(work2Id)); + Assert.False(backgroundService.Contains(work2Id + work1Id)); + Assert.False(backgroundService.Contains(string.Empty)); + } + [Fact] public async Task Push_makes_new_scope_after_start() { mockServiceScopeFactory.Invocations.Clear(); var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); - var work = new WorkBase("", (_, _, _) => Task.CompletedTask); + var work = new WorkBase("", someAction); backgroundService.Push(work); await backgroundService.StartAsync(CancellationToken.None); await Task.Delay(10); @@ -99,8 +119,10 @@ namespace AsbCloudWebApi.Tests.ServicesTests workDone = true; return Task.CompletedTask; }, - TimeSpan.FromSeconds(30)); - work.LastStart = DateTime.Now; + TimeSpan.FromSeconds(30)) + { + LastStart = DateTime.Now + }; backgroundService.Push(work); await backgroundService.StartAsync(CancellationToken.None); @@ -168,12 +190,14 @@ namespace AsbCloudWebApi.Tests.ServicesTests { var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); var workCanceled = false; - var work = new WorkBase("", async (_, _, _) => await Task.Delay(1000000)); - work.Timeout = TimeSpan.FromMilliseconds(1); - work.OnErrorAsync = async (id, ex, token) => + var work = new WorkBase("", async (_, _, token) => await Task.Delay(1000000, token)) { - workCanceled = ex is System.TimeoutException; - await Task.CompletedTask; + Timeout = TimeSpan.FromMilliseconds(1), + OnErrorAsync = async (id, ex, token) => + { + workCanceled = ex is System.TimeoutException; + await Task.CompletedTask; + } }; backgroundService.Push(work); From 89e0495d099d9b68145cd68ef38d369b47c52183 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 14:48:23 +0500 Subject: [PATCH 5/9] 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); From 71aff8d1ec985831f9ad096f9fdae3f657164638 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 14:49:00 +0500 Subject: [PATCH 6/9] nit. comments and cleanup --- AsbCloudInfrastructure/Helper.cs | 55 ------------------- AsbCloudInfrastructure/MapsterExtension.cs | 12 ---- ...Extentions.cs => MemoryCacheExtentions.cs} | 0 AsbCloudInfrastructure/Startup.cs | 4 +- .../Middlewares/RequerstTrackerMiddleware.cs | 1 + .../SimplifyExceptionsMiddleware.cs | 2 +- AsbCloudWebApi/Program.cs | 41 +------------- 7 files changed, 4 insertions(+), 111 deletions(-) delete mode 100644 AsbCloudInfrastructure/Helper.cs delete mode 100644 AsbCloudInfrastructure/MapsterExtension.cs rename AsbCloudInfrastructure/{CacheExtentions.cs => MemoryCacheExtentions.cs} (100%) diff --git a/AsbCloudInfrastructure/Helper.cs b/AsbCloudInfrastructure/Helper.cs deleted file mode 100644 index 01fc97b5..00000000 --- a/AsbCloudInfrastructure/Helper.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace AsbCloudInfrastructure -{ - public static class Helper - { - public static T Max(params T[] items) - where T : IComparable - { - var count = items.Length; - if (count < 1) - throw new ArgumentException("Count of params must be greater than 1"); - - var max = items[0]; - for (var i = 1; i < count; i++) - if (max.CompareTo(items[i]) < 0) - max = items[i]; - - return max; - } - - public static T Min(params T[] items) - where T : IComparable - { - var count = items.Length; - if (count < 1) - throw new ArgumentException("Count of params must be greater than 1"); - - var min = items[0]; - for (var i = 1; i < count; i++) - if (min.CompareTo(items[i]) > 0) - min = items[i]; - - return min; - } - - public static (T min, T max) MinMax(params T[] items) - where T : IComparable - { - var count = items.Length; - if (count < 1) - throw new ArgumentException("Count of params must be greater than 1"); - - var min = items[0]; - var max = items[0]; - for (var i = 1; i < count; i++) - if (max.CompareTo(items[i]) < 0) - max = items[i]; - else if (min.CompareTo(items[i]) > 0) - min = items[i]; - - return (min, max); - } - } -} diff --git a/AsbCloudInfrastructure/MapsterExtension.cs b/AsbCloudInfrastructure/MapsterExtension.cs deleted file mode 100644 index 94ed630c..00000000 --- a/AsbCloudInfrastructure/MapsterExtension.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Mapster -{ - public static class MapsterExtension - { - //public static IEnumerable Adapt(this IEnumerable sourceList) - //{ - // return sourceList.Select(item => item.Adapt()); - //} - - - } -} diff --git a/AsbCloudInfrastructure/CacheExtentions.cs b/AsbCloudInfrastructure/MemoryCacheExtentions.cs similarity index 100% rename from AsbCloudInfrastructure/CacheExtentions.cs rename to AsbCloudInfrastructure/MemoryCacheExtentions.cs diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index 5ccb0df7..c8dc24a9 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -1,17 +1,15 @@ using AsbCloudApp.Services; using AsbCloudDb.Model; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; -using System.Linq; namespace AsbCloudInfrastructure { public class Startup { - public static void BeforeRunHandler(IHost host, IConfigurationRoot configuration) + public static void BeforeRunHandler(IHost host) { using var scope = host.Services.CreateScope(); var context = scope.ServiceProvider.GetService(); diff --git a/AsbCloudWebApi/Middlewares/RequerstTrackerMiddleware.cs b/AsbCloudWebApi/Middlewares/RequerstTrackerMiddleware.cs index b35b40e5..551b4d03 100644 --- a/AsbCloudWebApi/Middlewares/RequerstTrackerMiddleware.cs +++ b/AsbCloudWebApi/Middlewares/RequerstTrackerMiddleware.cs @@ -43,6 +43,7 @@ namespace AsbCloudWebApi.Middlewares sw.Stop(); requestLog.ElapsedMilliseconds = sw.ElapsedMilliseconds; requestLog.Status = context.Response.StatusCode; + // TODO: Add request params and body size. service.RegisterRequestError(requestLog, ex); throw; } diff --git a/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs b/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs index c09f9749..dd9fff9f 100644 --- a/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs +++ b/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs @@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Middlewares { Console.WriteLine(ex.Message); } - catch (Exception ex) + catch (Exception ex) // TODO: find explicit exception. Use Trace. Add body size to message. { if (ex.Message.Contains("Reading the request body timed out due to data arriving too slowly. See MinRequestBodyDataRate.")) Console.WriteLine("Reading the request body timed out due to data arriving too slowly."); diff --git a/AsbCloudWebApi/Program.cs b/AsbCloudWebApi/Program.cs index b21fa05f..18825902 100644 --- a/AsbCloudWebApi/Program.cs +++ b/AsbCloudWebApi/Program.cs @@ -1,10 +1,5 @@ -using DocumentFormat.OpenXml.InkML; using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using System; -using System.Linq; namespace AsbCloudWebApi { @@ -15,30 +10,8 @@ namespace AsbCloudWebApi public static void Main(string[] args) { - IConfigurationRoot configuration = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json") - .Build(); - - if (args?.Length > 0) - { - if (args.Contains("db_init")) - { - var connectionStringName = "DefaultConnection"; - - var context = AsbCloudInfrastructure.DependencyInjection.MakeContext(configuration.GetConnectionString(connectionStringName)); - context.Database.SetCommandTimeout(TimeSpan.FromSeconds(5 * 60)); - context.Database.Migrate(); - - Console.WriteLine(" ."); - return; - } - WriteHelp(); - return; - } - var host = CreateHostBuilder(args).Build(); - AsbCloudInfrastructure.Startup.BeforeRunHandler(host, configuration); + AsbCloudInfrastructure.Startup.BeforeRunHandler(host); host.Run(); } @@ -48,17 +21,5 @@ namespace AsbCloudWebApi { webBuilder.UseStartup(); }); - - private static void WriteHelp() - { - Console.WriteLine(" ."); - Console.WriteLine(" :"); - Console.WriteLine("db_init - ."); - Console.WriteLine(" \"DefaultConnection\""); - Console.WriteLine(" , "); - Console.WriteLine(" , ."); - Console.WriteLine(" public"); - Console.WriteLine(""); - } } } From 8e9baf22d8104283a78594f0f53ea3328fc8f793 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 17:18:16 +0500 Subject: [PATCH 7/9] BackgroundWorker Move periodic services --- AsbCloudApp/Services/IReportService.cs | 3 + ...ndWorkerService.cs => BackgroundWorker.cs} | 17 ++--- AsbCloudInfrastructure/DependencyInjection.cs | 3 - .../DetectedOperationService.cs | 2 +- ...ce.cs => OperationDetectionWorkFactory.cs} | 63 +++++-------------- .../LimitingParameterBackgroundService.cs | 62 ++++++------------ .../Services/SAUB/TelemetryTracker.cs | 1 + ... SubsystemOperationTimeCalcWorkFactory.cs} | 60 +++++------------- AsbCloudInfrastructure/Startup.cs | 22 +++++-- 9 files changed, 82 insertions(+), 151 deletions(-) rename AsbCloudInfrastructure/Background/{BackgroundWorkerService.cs => BackgroundWorker.cs} (89%) rename AsbCloudInfrastructure/Services/DetectOperations/{OperationDetectionBackgroundService.cs => OperationDetectionWorkFactory.cs} (69%) rename AsbCloudInfrastructure/Services/Subsystems/{SubsystemOperationTimeBackgroundService.cs => SubsystemOperationTimeCalcWorkFactory.cs} (86%) diff --git a/AsbCloudApp/Services/IReportService.cs b/AsbCloudApp/Services/IReportService.cs index 1f4a7d35..e5e24648 100644 --- a/AsbCloudApp/Services/IReportService.cs +++ b/AsbCloudApp/Services/IReportService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; namespace AsbCloudApp.Services { +#nullable enable /// /// Сервис рапортов /// @@ -57,5 +58,7 @@ namespace AsbCloudApp.Services /// /// Task> GetAllReportsByWellAsync(int idWell, CancellationToken token); + +#nullable disable } } diff --git a/AsbCloudInfrastructure/Background/BackgroundWorkerService.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs similarity index 89% rename from AsbCloudInfrastructure/Background/BackgroundWorkerService.cs rename to AsbCloudInfrastructure/Background/BackgroundWorker.cs index e823284d..531d775e 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorkerService.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.DependencyInjection; +using AsbCloudInfrastructure.Services.DetectOperations; +using AsbCloudInfrastructure.Services.Subsystems; +using AsbCloudInfrastructure.Services; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Diagnostics; @@ -18,13 +21,10 @@ namespace AsbCloudInfrastructure.Background 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; -#warning move StartAsync(CancellationToken.None).Wait() to THE factory - Task.Delay(1_000) - .ContinueWith(_=> StartAsync(CancellationToken.None).Wait()); } /// @@ -69,11 +69,12 @@ namespace AsbCloudInfrastructure.Background 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); @@ -90,8 +91,8 @@ namespace AsbCloudInfrastructure.Background token); await task.WaitAsync(exceptionHandleTimeout, token); } - } - + } + CurrentWorkId = null; await Task.Delay(minDelay, token); } } diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index a66ebe9e..35b956e2 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -98,9 +98,6 @@ namespace AsbCloudInfrastructure services.AddScoped(provider => provider.GetService()); services.AddScoped(); - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); services.AddSingleton(new WitsInfoService()); services.AddSingleton(new InstantDataRepository()); services.AddSingleton(provider=> TelemetryDataCache.GetInstance(configuration)); diff --git a/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs index 87decac8..7cca4f97 100644 --- a/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs +++ b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs @@ -268,7 +268,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations return query; } - private DetectedOperationDto Convert(DetectedOperation operation, WellDto well, IEnumerable operationValues, IEnumerable schedules) + private static DetectedOperationDto Convert(DetectedOperation operation, WellDto well, IEnumerable operationValues, IEnumerable schedules) { var dto = operation.Adapt(); dto.IdWell = well.Id; diff --git a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionBackgroundService.cs b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs similarity index 69% rename from AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionBackgroundService.cs rename to AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs index e4b7c450..8ff6ebb8 100644 --- a/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionBackgroundService.cs +++ b/AsbCloudInfrastructure/Services/DetectOperations/OperationDetectionWorkFactory.cs @@ -1,7 +1,5 @@ using AsbCloudDb.Model; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,14 +7,16 @@ 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 { #nullable enable - public class OperationDetectionBackgroundService : BackgroundService + public static class OperationDetectionWorkFactory { - private readonly string connectionString; - private readonly TimeSpan period = TimeSpan.FromHours(1); + private const string workId = "Operation detection"; + private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); private static readonly DetectorAbstract[] detectors = new DetectorAbstract[] { @@ -31,49 +31,18 @@ namespace AsbCloudInfrastructure.Services.DetectOperations new DetectorTemplatingWhileDrilling(), }; - public OperationDetectionBackgroundService(IConfiguration configuration) - { - connectionString = configuration.GetConnectionString("DefaultConnection"); + public static WorkPeriodic MakeWork() + { + var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod); + workPeriodic.Timeout = TimeSpan.FromMinutes(30); + return workPeriodic; } - protected override async Task ExecuteAsync(CancellationToken token = default) + // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. + private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) { - var timeToStartAnalysis = DateTime.Now; - var options = new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options; + using var db = serviceProvider.GetRequiredService(); - while (!token.IsCancellationRequested) - { - if (DateTime.Now > timeToStartAnalysis) - { - timeToStartAnalysis = DateTime.Now + period; - try - { - using var context = new AsbCloudDbContext(options); - var added = await DetectedAllTelemetriesAsync(context, token); - Trace.TraceInformation($"Total detection complete. Added {added} operations."); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - } - GC.Collect(); - } - - var ms = (int)(timeToStartAnalysis - DateTime.Now).TotalMilliseconds; - ms = ms > 100 ? ms : 100; - await Task.Delay(ms, token).ConfigureAwait(false); - } - } - - public override async Task StopAsync(CancellationToken token) - { - await base.StopAsync(token).ConfigureAwait(false); - } - - private static async Task DetectedAllTelemetriesAsync(IAsbCloudDbContext db, CancellationToken token) - { var lastDetectedDates = await db.DetectedOperations .GroupBy(o => o.IdTelemetry) .Select(g => new @@ -88,7 +57,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations .Select(t => t.Id) .ToListAsync(token); - var JounedlastDetectedDates = telemetryIds + var joinedlastDetectedDates = telemetryIds .GroupJoin(lastDetectedDates, t => t, o => o.IdTelemetry, @@ -97,8 +66,9 @@ namespace AsbCloudInfrastructure.Services.DetectOperations IdTelemetry = outer, inner.SingleOrDefault()?.LastDate, }); + var affected = 0; - foreach (var item in JounedlastDetectedDates) + foreach (var item in joinedlastDetectedDates) { var stopwatch = Stopwatch.StartNew(); var newOperations = await DetectOperationsAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); @@ -109,7 +79,6 @@ namespace AsbCloudInfrastructure.Services.DetectOperations affected += await db.SaveChangesAsync(token); } } - return affected; } private static async Task> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs index 261ceb9c..1420ea89 100644 --- a/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs +++ b/AsbCloudInfrastructure/Services/LimitingParameterBackgroundService.cs @@ -1,61 +1,37 @@ using AsbCloudDb.Model; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using System; using System.Data.Common; using System.Data; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; namespace AsbCloudInfrastructure.Services { #nullable enable - internal class LimitingParameterBackgroundService : BackgroundService + internal static class LimitingParameterCalcWorkFactory { - private readonly string connectionString; - private readonly TimeSpan period = TimeSpan.FromHours(1); + private const string workId = "Limiting parameter calc"; + private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30); - public LimitingParameterBackgroundService(IConfiguration configuration) + public static WorkPeriodic MakeWork() { - connectionString = configuration.GetConnectionString("DefaultConnection"); - } - - protected override async Task ExecuteAsync(CancellationToken token) - { - var timeToStart = DateTime.Now; - var options = new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options; - while (!token.IsCancellationRequested) + var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod) { - if (DateTime.Now > timeToStart) - { - timeToStart = DateTime.Now + period; - try - { - using var context = new AsbCloudDbContext(options); - var added = await LimitingParameterAsync(context, token); - Trace.TraceInformation($"Total limiting parameter complete. Added {added} limiting parameters."); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - } - GC.Collect(); - } - var ms = (int)(timeToStart - DateTime.Now).TotalMilliseconds; - ms = ms > 100 ? ms : 100; - await Task.Delay(ms, token).ConfigureAwait(false); - } + Timeout = TimeSpan.FromMinutes(30) + }; + return workPeriodic; } - private static async Task LimitingParameterAsync(IAsbCloudDbContext context, CancellationToken token) + // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. + private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) { - var lastDetectedDates = await context.LimitingParameter + using var db = serviceProvider.GetRequiredService(); + var lastDetectedDates = await db.LimitingParameter .GroupBy(o => o.IdTelemetry) .Select(g => new { @@ -64,7 +40,7 @@ namespace AsbCloudInfrastructure.Services }) .ToListAsync(token); - var telemetryIds = await context.Telemetries + var telemetryIds = await db.Telemetries .Where(t => t.Info != null && t.TimeZone != null) .Select(t => t.Id) .ToListAsync(token); @@ -79,17 +55,15 @@ namespace AsbCloudInfrastructure.Services inner.SingleOrDefault()?.LastDate, }); - var affected = 0; foreach (var item in telemetryLastDetectedDates) { - var newLimitingParameters = await GetLimitingParameterAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, context, token); + var newLimitingParameters = await GetLimitingParameterAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); if (newLimitingParameters?.Any() == true) { - context.LimitingParameter.AddRange(newLimitingParameters); - affected += await context.SaveChangesAsync(token); + db.LimitingParameter.AddRange(newLimitingParameters); + await db.SaveChangesAsync(token); } } - return affected; } private static async Task> GetLimitingParameterAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryTracker.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryTracker.cs index 190094a1..d3f80a9b 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryTracker.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryTracker.cs @@ -41,6 +41,7 @@ namespace AsbCloudInfrastructure.Services.SAUB public TelemetryTracker(IConfiguration configuration, IMemoryCache memoryCache) { + // TODO: make this background work var contextOptions = new DbContextOptionsBuilder() .UseNpgsql(configuration.GetConnectionString("DefaultConnection")) .Options; diff --git a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeBackgroundService.cs b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs similarity index 86% rename from AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeBackgroundService.cs rename to AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs index 7f241ca3..9efc29a8 100644 --- a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeBackgroundService.cs +++ b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeCalcWorkFactory.cs @@ -1,9 +1,9 @@ using AsbCloudDb.Model; using AsbCloudDb.Model.Subsystems; +using AsbCloudInfrastructure.Background; using AsbCloudInfrastructure.Services.Subsystems.Utils; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Data; @@ -16,56 +16,30 @@ using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.Subsystems { #nullable enable - internal class SubsystemOperationTimeBackgroundService : BackgroundService + internal static class SubsystemOperationTimeCalcWorkFactory { - private readonly string connectionString; - private readonly TimeSpan period = TimeSpan.FromHours(1); + 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; private const int idSubsytemAkb = 1; private const int idSubsytemMse = 2; - public SubsystemOperationTimeBackgroundService(IConfiguration configuration) + public static WorkPeriodic MakeWork() { - connectionString = configuration.GetConnectionString("DefaultConnection"); - } - - protected override async Task ExecuteAsync(CancellationToken token) - { - var timeToStart = DateTime.Now; - var options = new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options; - while (!token.IsCancellationRequested) + var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod) { - if (DateTime.Now > timeToStart) - { - timeToStart = DateTime.Now + period; - try - { - using var context = new AsbCloudDbContext(options); - var added = await OperationTimeAllTelemetriesAsync(context, token); - Trace.TraceInformation($"Total subsystem operation time complete. Added {added} operations time."); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - } - GC.Collect(); - } - var ms = (int)(timeToStart - DateTime.Now).TotalMilliseconds; - ms = ms > 100 ? ms : 100; - await Task.Delay(ms, token).ConfigureAwait(false); - } + Timeout = TimeSpan.FromMinutes(30) + }; + return workPeriodic; } - public override async Task StopAsync(CancellationToken token) + // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД. + private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token) { - await base.StopAsync(token).ConfigureAwait(false); - } + using var db = serviceProvider.GetRequiredService(); - private static async Task OperationTimeAllTelemetriesAsync(IAsbCloudDbContext db, CancellationToken token) - { var lastDetectedDates = await db.SubsystemOperationTimes .GroupBy(o => o.IdTelemetry) .Select(g => new @@ -90,23 +64,21 @@ namespace AsbCloudInfrastructure.Services.Subsystems inner.SingleOrDefault()?.LastDate, }); - var affected = 0; foreach (var item in telemetryLastDetectedDates) { var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); if (newOperationsSaub?.Any() == true) { db.SubsystemOperationTimes.AddRange(newOperationsSaub); - affected += await db.SaveChangesAsync(token); + await db.SaveChangesAsync(token); } var newOperationsSpin = await OperationTimeSpinAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); if (newOperationsSpin?.Any() == true) { db.SubsystemOperationTimes.AddRange(newOperationsSpin); - affected += await db.SaveChangesAsync(token); + await db.SaveChangesAsync(token); } } - return affected; } private static async Task ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index c8dc24a9..01768371 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -1,9 +1,14 @@ using AsbCloudApp.Services; using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.DetectOperations; +using AsbCloudInfrastructure.Services.Subsystems; +using AsbCloudInfrastructure.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; +using System.Threading.Tasks; +using System.Threading; namespace AsbCloudInfrastructure { @@ -12,13 +17,22 @@ namespace AsbCloudInfrastructure public static void BeforeRunHandler(IHost host) { using var scope = host.Services.CreateScope(); - var context = scope.ServiceProvider.GetService(); - context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60)); + var provider = scope.ServiceProvider; + var context = provider.GetService(); + context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60)); context.Database.Migrate(); - var wellService = scope.ServiceProvider.GetService(); - wellService.EnshureTimezonesIsSetAsync(System.Threading.CancellationToken.None).Wait(); + var wellService = provider.GetRequiredService(); + wellService.EnshureTimezonesIsSetAsync(CancellationToken.None).Wait();// TODO: make this background work + + var backgroundWorker = provider.GetRequiredService(); + backgroundWorker.Push(OperationDetectionWorkFactory.MakeWork()); + backgroundWorker.Push(SubsystemOperationTimeCalcWorkFactory.MakeWork()); + backgroundWorker.Push(LimitingParameterCalcWorkFactory.MakeWork()); + + Task.Delay(1_000) + .ContinueWith(async (_) => await backgroundWorker.StartAsync(CancellationToken.None)); } } } From b05aadccb4b8b1a9ff6a9657ab23203a3fb1e1c6 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 2 Dec 2022 17:44:47 +0500 Subject: [PATCH 8/9] Add memory monitor --- AsbCloudInfrastructure/Startup.cs | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index 01768371..09e92644 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Hosting; using System; using System.Threading.Tasks; using System.Threading; +using AsbCloudInfrastructure.Background; namespace AsbCloudInfrastructure { @@ -30,9 +31,42 @@ namespace AsbCloudInfrastructure backgroundWorker.Push(OperationDetectionWorkFactory.MakeWork()); backgroundWorker.Push(SubsystemOperationTimeCalcWorkFactory.MakeWork()); backgroundWorker.Push(LimitingParameterCalcWorkFactory.MakeWork()); + backgroundWorker.Push(MakeMemoryMonitoringWork()); Task.Delay(1_000) .ContinueWith(async (_) => await backgroundWorker.StartAsync(CancellationToken.None)); } + + static WorkPeriodic MakeMemoryMonitoringWork() + { + var workId = "Memory monitoring"; + var workAction = (string _, IServiceProvider _, CancellationToken _) => { + var bytes = GC.GetTotalMemory(false); + var bytesString = FromatBytes(bytes); + System.Diagnostics.Trace.TraceInformation($"Total memory allocated is {bytesString} bytes"); + return Task.CompletedTask; + }; + var workPeriod = TimeSpan.FromMinutes(1); + var work = new WorkPeriodic(workId, workAction, workPeriod); + return work; + } + + static string FromatBytes(long bytes) + { + const double gigaByte = 1024 * 1024 * 1024; + const double megaByte = 1024 * 1024; + const double kiloByte = 1024; + + if (bytes > 10 * gigaByte) + return (bytes / gigaByte).ToString("### ### ###.## Gb"); + + if (bytes > 10 * megaByte) + return (bytes / megaByte).ToString("### ### ###.## Mb"); + + if (bytes > 10 * kiloByte) + return (bytes / megaByte).ToString("### ### ###.## Kb"); + + return bytes.ToString("### ### ###"); + } } } From 85b99135e6f34bc4128ec9464b353518c68e9546 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Mon, 5 Dec 2022 08:47:00 +0500 Subject: [PATCH 9/9] nit tests refactoring --- .../BackgroundWorkerServiceTest.cs | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs index 24b8e999..133f6b36 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/BackgroundWorkerServiceTest.cs @@ -1,20 +1,20 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using System; -using AsbCloudInfrastructure.Services.Background; +using AsbCloudInfrastructure.Background; using System.Threading; using System.Threading.Tasks; using Xunit; namespace AsbCloudWebApi.Tests.ServicesTests { - public class BackgroundWorkerServiceTest + public class BackgroundWorkerTest { private readonly Mock mockServiceProvider; private readonly Mock mockServiceScopeFactory; private readonly Func someAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask; - public BackgroundWorkerServiceTest() + public BackgroundWorkerTest() { var mockServiceScope = new Mock(); mockServiceScopeFactory = new Mock(); @@ -32,20 +32,20 @@ namespace AsbCloudWebApi.Tests.ServicesTests { mockServiceScopeFactory.Invocations.Clear(); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); const string work1Id = "long name 1"; const string work2Id = "long name 2"; var work1 = new WorkBase(work1Id, someAction); var work2 = new WorkPeriodic(work2Id, someAction, TimeSpan.Zero); - backgroundService.Push(work1); - backgroundService.Push(work2); + BackgroundWorker.Push(work1); + BackgroundWorker.Push(work2); - Assert.True(backgroundService.Contains(work1Id)); - Assert.True(backgroundService.Contains(work2Id)); - Assert.False(backgroundService.Contains(work2Id + work1Id)); - Assert.False(backgroundService.Contains(string.Empty)); + Assert.True(BackgroundWorker.Contains(work1Id)); + Assert.True(BackgroundWorker.Contains(work2Id)); + Assert.False(BackgroundWorker.Contains(work2Id + work1Id)); + Assert.False(BackgroundWorker.Contains(string.Empty)); } [Fact] @@ -53,10 +53,10 @@ namespace AsbCloudWebApi.Tests.ServicesTests { mockServiceScopeFactory.Invocations.Clear(); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var work = new WorkBase("", someAction); - backgroundService.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + BackgroundWorker.Push(work); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(10); mockServiceScopeFactory.Verify(f => f.CreateScope()); @@ -65,15 +65,15 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Makes_primary_work_done() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var workDone = false; var work = new WorkBase("", (_, _, _) => { workDone = true; return Task.CompletedTask; }); - backgroundService.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + BackgroundWorker.Push(work); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(10); Assert.True(workDone); @@ -82,10 +82,10 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Sets_ExecutionTime_after_work_done() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var work = new WorkBase("", someAction); - backgroundService.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + BackgroundWorker.Push(work); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(10); Assert.True(work.ExecutionTime > TimeSpan.Zero); @@ -94,7 +94,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Makes_periodic_work_done() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var workDone = false; var work = new WorkPeriodic("", (_, _, _) => { @@ -102,8 +102,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests return Task.CompletedTask; }, TimeSpan.FromMilliseconds(10)); - backgroundService.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + BackgroundWorker.Push(work); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(20); Assert.True(workDone); @@ -112,7 +112,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Does_not_start_periodic_work() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var workDone = false; var work = new WorkPeriodic("", (_, _, _) => { @@ -123,9 +123,9 @@ namespace AsbCloudWebApi.Tests.ServicesTests { LastStart = DateTime.Now }; - backgroundService.Push(work); + BackgroundWorker.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(20); Assert.False(workDone); @@ -152,11 +152,11 @@ namespace AsbCloudWebApi.Tests.ServicesTests return Task.CompletedTask; }); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); - backgroundService.Push(work2); - backgroundService.Push(work1); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); + BackgroundWorker.Push(work2); + BackgroundWorker.Push(work1); - await backgroundService.StartAsync(CancellationToken.None); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(2_100); Assert.True(work2Order < work1Order); @@ -174,12 +174,12 @@ namespace AsbCloudWebApi.Tests.ServicesTests return Task.CompletedTask; }, TimeSpan.FromMilliseconds(1)); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); - backgroundService.Push(work1); - backgroundService.Push(work2); - backgroundService.Delete("1"); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); + BackgroundWorker.Push(work1); + BackgroundWorker.Push(work2); + BackgroundWorker.Delete("1"); - await backgroundService.StartAsync(CancellationToken.None); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(10); Assert.True(workDone); @@ -188,7 +188,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Aborts_long_work() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var workCanceled = false; var work = new WorkBase("", async (_, _, token) => await Task.Delay(1000000, token)) { @@ -200,8 +200,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests } }; - backgroundService.Push(work); - await backgroundService.StartAsync(CancellationToken.None); + BackgroundWorker.Push(work); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(20 * 4); Assert.True(workCanceled); @@ -210,7 +210,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests [Fact] public async Task Execution_continues_after_work_exception() { - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); var work2done = false; var work1 = new WorkBase("1", (_, _, _) => throw new Exception()); var work2 = new WorkBase("2", (_, _, _) => @@ -219,10 +219,10 @@ namespace AsbCloudWebApi.Tests.ServicesTests return Task.CompletedTask; }); - backgroundService.Push(work1); - backgroundService.Push(work2); + BackgroundWorker.Push(work1); + BackgroundWorker.Push(work2); - await backgroundService.StartAsync(CancellationToken.None); + await BackgroundWorker.StartAsync(CancellationToken.None); await Task.Delay(2_100); Assert.True(work2done); @@ -234,11 +234,11 @@ namespace AsbCloudWebApi.Tests.ServicesTests var work1 = new WorkPeriodic("1", someAction, TimeSpan.FromSeconds(30)); var work2 = new WorkBase("1", someAction); - var backgroundService = new BackgroundWorkerService(mockServiceProvider.Object); - backgroundService.Push(work1); + var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object); + BackgroundWorker.Push(work1); Assert.Throws( - () => backgroundService.Push(work2)); + () => BackgroundWorker.Push(work2)); } } }