diff --git a/AsbCloudApp/Data/BackgroundWorkDto.cs b/AsbCloudApp/Data/BackgroundWorkDto.cs
index 9d4f2105..3374148c 100644
--- a/AsbCloudApp/Data/BackgroundWorkDto.cs
+++ b/AsbCloudApp/Data/BackgroundWorkDto.cs
@@ -172,7 +172,7 @@ namespace AsbCloudApp.Data
if (progress.HasValue)
CurrentState.Progress = progress.Value;
- Trace.TraceInformation($"{WorkNameForTrace} state: {newState} [{100*progress:#}%]");
+ Trace.TraceInformation($"{WorkNameForTrace} state[{100*progress:#}%]: {newState}");
}
///
diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs
index 101a8273..f56b0ae4 100644
--- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs
+++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs
@@ -1,7 +1,9 @@
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;
@@ -12,13 +14,37 @@ namespace AsbCloudInfrastructure.Background;
///
public class BackgroundWorker : BackgroundService
{
- private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10);
- private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2);
+ private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1);
private readonly IServiceProvider serviceProvider;
- public WorkStore WorkStore { get; } = new WorkStore();
+ ///
+ /// Очередь работ
+ ///
+ private Queue works = new(8);
+
+ ///
+ /// Список периодических работ
+ ///
+ public IEnumerable Works => works;
+
+ ///
+ /// Работа выполняемая в данный момент
+ ///
public Work? CurrentWork;
+ ///
+ /// последние 16 завершившиеся с ошибкой
+ ///
+ public CyclycArray Felled { get; } = new(16);
+
+ ///
+ /// последние 16 успешно завершенных
+ ///
+ public CyclycArray Done { get; } = new(16);
+
+ ///
+ /// Ошибка в главном цикле, никогда не должна появляться
+ ///
public string MainLoopLastException { get; private set; } = string.Empty;
public BackgroundWorker(IServiceProvider serviceProvider)
@@ -28,26 +54,18 @@ public class BackgroundWorker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken token)
{
- while (!token.IsCancellationRequested)
+ while (!token.IsCancellationRequested && works.TryDequeue(out CurrentWork))
{
try
{
- var work = WorkStore.GetNext();
- if (work is null)
- {
- await Task.Delay(executePeriod, token);
- continue;
- }
-
- CurrentWork = work;
using var scope = serviceProvider.CreateScope();
- var result = await work.Start(scope.ServiceProvider, token);
+ var result = await CurrentWork.Start(scope.ServiceProvider, token);
if (!result)
- WorkStore.Felled.Add(work);
+ Felled.Add(CurrentWork);
else
- WorkStore.Done.Add(work);
+ Done.Add(CurrentWork);
CurrentWork = null;
await Task.Delay(minDelay, token);
@@ -64,4 +82,36 @@ public class BackgroundWorker : BackgroundService
}
}
}
+
+ ///
+ /// Добавить в очередь
+ ///
+ /// work.Id может быть не уникальным,
+ /// при этом метод TryRemoveFromQueue удалит все работы с совпадающими id
+ ///
+ ///
+ ///
+ public void Enqueue(Work work)
+ {
+ works.Enqueue(work);
+ if (ExecuteTask is null || ExecuteTask.IsCompleted)
+ StartAsync(CancellationToken.None).Wait();
+ }
+
+ ///
+ /// Удаление работы по ID из одноразовой очереди
+ ///
+ ///
+ ///
+ public bool TryRemoveFromQueue(string id)
+ {
+ var work = Works.FirstOrDefault(w => w.Id == id);
+ if (work is not null)
+ {
+ works = new Queue(Works.Where(w => w.Id != id));
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
new file mode 100644
index 00000000..a7490ed7
--- /dev/null
+++ b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
@@ -0,0 +1,115 @@
+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.Background;
+
+///
+/// Сервис для фонового выполнения периодической работы
+///
+public class PeriodicBackgroundWorker : BackgroundService
+{
+ private readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10);
+ private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1);
+ private readonly IServiceProvider serviceProvider;
+
+ private readonly List works = new(8);
+
+ ///
+ /// Список периодических работ
+ ///
+ public IEnumerable Works => works;
+
+ ///
+ /// Работа выполняемая в данный момент
+ ///
+ public Work? CurrentWork;
+
+ ///
+ /// Ошибка в главном цикле, никогда не должна появляться
+ ///
+ public string MainLoopLastException { get; private set; } = string.Empty;
+
+ public PeriodicBackgroundWorker(IServiceProvider serviceProvider)
+ {
+ this.serviceProvider = serviceProvider;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ try
+ {
+ var periodicWork = GetNext();
+ if (periodicWork is null)
+ {
+ await Task.Delay(executePeriod, token);
+ continue;
+ }
+
+ CurrentWork = periodicWork.Work;
+
+ using var scope = serviceProvider.CreateScope();
+
+ var result = await periodicWork.Work.Start(scope.ServiceProvider, token);
+
+ CurrentWork = null;
+ await Task.Delay(minDelay, token);
+ }
+ catch (Exception ex)
+ {
+ MainLoopLastException = $"BackgroundWorker " +
+ $"MainLoopLastException: \r\n" +
+ $"date: {DateTime.Now:O}\r\n" +
+ $"message: {ex.Message}\r\n" +
+ $"inner: {ex.InnerException?.Message}\r\n" +
+ $"stackTrace: {ex.StackTrace}";
+ Trace.TraceError(MainLoopLastException);
+ }
+ }
+ }
+
+ ///
+ /// Добавить фоновую работу выполняющуюся с заданным периодом
+ ///
+ ///
+ ///
+ public void Add(TimeSpan period)
+ where T : Work, new()
+ {
+ var work = new T();
+ var periodic = new WorkPeriodic(work, period);
+ works.Add(periodic);
+ }
+
+ ///
+ /// Добавить фоновую работу выполняющуюся с заданным периодом
+ ///
+ ///
+ ///
+ public void Add(Work work, TimeSpan period)
+ {
+ var periodic = new WorkPeriodic(work, period);
+ works.Add(periodic);
+ if (ExecuteTask is null || ExecuteTask.IsCompleted)
+ StartAsync(CancellationToken.None).Wait();
+ }
+
+ private WorkPeriodic? GetNext()
+ {
+ var work = works
+ .OrderBy(w => w.NextStart)
+ .FirstOrDefault();
+
+ if (work is null || work.NextStart > DateTime.Now)
+ return null;
+
+ return work;
+ }
+}
diff --git a/AsbCloudInfrastructure/Background/Work.cs b/AsbCloudInfrastructure/Background/Work.cs
index 4820aa26..1177892c 100644
--- a/AsbCloudInfrastructure/Background/Work.cs
+++ b/AsbCloudInfrastructure/Background/Work.cs
@@ -13,6 +13,8 @@ namespace AsbCloudInfrastructure.Background;
///
public abstract class Work : BackgroundWorkDto
{
+ private CancellationTokenSource? stoppingCts;
+
private sealed class WorkBase : Work
{
private Func, CancellationToken, Task> ActionAsync { get; }
@@ -69,8 +71,9 @@ public abstract class Work : BackgroundWorkDto
SetStatusStart();
try
{
- var task = Action(Id, services, UpdateStatus, token);
- await task.WaitAsync(Timeout, token);
+ stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(token);
+ var task = Action(Id, services, UpdateStatus, stoppingCts.Token);
+ await task.WaitAsync(Timeout, stoppingCts.Token);
SetStatusComplete();
return true;
}
@@ -97,6 +100,11 @@ public abstract class Work : BackgroundWorkDto
return false;
}
+ public void Stop()
+ {
+ stoppingCts?.Cancel();
+ }
+
private static string FormatExceptionMessage(Exception exception)
{
var firstException = FirstException(exception);
diff --git a/AsbCloudInfrastructure/Background/WorkStore.cs b/AsbCloudInfrastructure/Background/WorkStore.cs
deleted file mode 100644
index be7386e6..00000000
--- a/AsbCloudInfrastructure/Background/WorkStore.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace AsbCloudInfrastructure.Background;
-
-///
-///
-/// Очередь работ
-///
-/// Не периодические задачи будут возвращаться первыми, как самые приоритетные.
-///
-public class WorkStore
-{
- private readonly List periodics = new(8);
-
- ///
- /// Список периодических задач
- ///
- public IEnumerable Periodics => periodics;
-
- ///
- /// Работы выполняемые один раз
- ///
- public Queue RunOnceQueue { get; private set; } = new(8);
-
- ///
- /// последние 16 завершившиеся с ошибкой
- ///
- public CyclycArray Felled { get; } = new(16);
-
- ///
- /// последние 16 успешно завершенных
- ///
- public CyclycArray Done { get; } = new(16);
-
- ///
- /// Добавить фоновую работу выполняющуюся с заданным периодом
- ///
- ///
- ///
- public void AddPeriodic(TimeSpan period)
- where T : Work, new()
- {
- var work = new T();
- var periodic = new WorkPeriodic(work, period);
- periodics.Add(periodic);
- }
-
- ///
- /// Добавить фоновую работу выполняющуюся с заданным периодом
- ///
- ///
- ///
- public void AddPeriodic(Work work, TimeSpan period)
- {
- var periodic = new WorkPeriodic(work, period);
- periodics.Add(periodic);
- }
-
- ///
- /// Удаление работы по ID из одноразовой очереди
- ///
- ///
- ///
- public bool TryRemoveFromRunOnceQueue(string id)
- {
- var work = RunOnceQueue.FirstOrDefault(w => w.Id == id);
- if (work is not null)
- {
- RunOnceQueue = new Queue(RunOnceQueue.Where(w => w.Id != id));
- return true;
- }
-
- return false;
- }
-
- ///
- ///
- /// Возвращает приоритетную задачу.
- ///
- ///
- /// Если приоритетные закончились, то ищет ближайшую периодическую.
- /// Если до старта ближайшей периодической работы меньше 20 сек,
- /// то этой задаче устанавливается время последнего запуска в now и она возвращается.
- /// Если больше 20 сек, то возвращается null.
- ///
- ///
- ///
- ///
- public Work? GetNext()
- {
- if (RunOnceQueue.Any())
- return RunOnceQueue.Dequeue();
-
- var work = GetNextPeriodic();
- if (work is null || work.NextStart > DateTime.Now)
- return null;
-
- return work.Work;
- }
-
- private WorkPeriodic? GetNextPeriodic()
- {
- var work = Periodics
- .OrderBy(w => w.NextStart)
- .FirstOrDefault();
- return work;
- }
-}
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index 93c6575e..28bde671 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -165,6 +165,7 @@ namespace AsbCloudInfrastructure
services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider));
services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider));
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(provider => ReduceSamplingService.GetInstance(configuration));
diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs
index 228a3564..c91689a3 100644
--- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs
+++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs
@@ -513,7 +513,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
if (state.IdState == idStateCreating)
{
var workId = MakeWorkId(idWell);
- if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId))
+ if (!backgroundWorker.Works.Any(w => w.Id == workId))
{
var well = (await wellService.GetOrDefaultAsync(idWell, token))!;
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf";
@@ -542,7 +542,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
var work = Work.CreateByDelegate(workId, workAction);
work.OnErrorAsync = onErrorAction;
- backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work);
+ backgroundWorker.Enqueue(work);
}
}
}
@@ -556,7 +556,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token)
{
var workId = MakeWorkId(idWell);
- backgroundWorker.WorkStore.TryRemoveFromRunOnceQueue(workId);
+ backgroundWorker.TryRemoveFromQueue(workId);
var filesIds = await context.Files
.Where(f => f.IdWell == idWell &&
diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs
index bbbc8196..e56e423f 100644
--- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs
+++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs
@@ -52,12 +52,12 @@ namespace AsbCloudInfrastructure.Services.Email
}
var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message);
- if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w=>w.Id==workId))
+ if (!backgroundWorker.Works.Any(w=>w.Id==workId))
{
var workAction = MakeEmailSendWorkAction(notification);
var work = Work.CreateByDelegate(workId, workAction);
- backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work);
+ backgroundWorker.Enqueue(work);
}
return Task.CompletedTask;
diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs
index 6a66c30e..ed24bc3e 100644
--- a/AsbCloudInfrastructure/Services/ReportService.cs
+++ b/AsbCloudInfrastructure/Services/ReportService.cs
@@ -95,7 +95,7 @@ namespace AsbCloudInfrastructure.Services
};
var work = Work.CreateByDelegate(workId, workAction);
- backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work);
+ backgroundWorkerService.Enqueue(work);
progressHandler.Invoke(new ReportProgressDto
{
diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs
index eabfcf77..3a6eac6f 100644
--- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs
+++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs
@@ -52,7 +52,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
await instance.InitializeCacheFromDBAsync(db, onProgress, token);
});
work.Timeout = TimeSpan.FromMinutes(15);
- worker.WorkStore.RunOnceQueue.Enqueue(work);
+ worker.Enqueue(work);
}
return instance;
}
diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs
index cef6745b..64191e5e 100644
--- a/AsbCloudInfrastructure/Startup.cs
+++ b/AsbCloudInfrastructure/Startup.cs
@@ -33,12 +33,12 @@ namespace AsbCloudInfrastructure
_ = provider.GetRequiredService>();
_ = provider.GetRequiredService>();
- var backgroundWorker = provider.GetRequiredService();
- backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30));
- backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(15));
- backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30));
- backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30));
- backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
+ var backgroundWorker = provider.GetRequiredService();
+ backgroundWorker.Add(TimeSpan.FromMinutes(30));
+ backgroundWorker.Add(TimeSpan.FromMinutes(15));
+ backgroundWorker.Add(TimeSpan.FromMinutes(30));
+ backgroundWorker.Add(TimeSpan.FromMinutes(30));
+ backgroundWorker.Add(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
var notificationBackgroundWorker = provider.GetRequiredService();
diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs
index 17a14ab1..7ced750b 100644
--- a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs
+++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs
@@ -53,11 +53,21 @@ namespace AsbCloudWebApi.Tests.Middlware
throw new NotImplementedException();
}
+ public DatesRangeDto? GetRange(int idWell)
+ {
+ throw new NotImplementedException();
+ }
+
public Task GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token)
{
throw new NotImplementedException();
}
+ public Task GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token)
+ {
+ throw new NotImplementedException();
+ }
+
public Task> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException();
public Task GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token)
diff --git a/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs
new file mode 100644
index 00000000..0f54dddf
--- /dev/null
+++ b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs
@@ -0,0 +1,113 @@
+using AsbCloudInfrastructure.Background;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using System;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace AsbCloudWebApi.Tests.Services;
+
+public class BackgroundWorkerTest
+{
+ private IServiceProvider provider;
+ private BackgroundWorker service;
+
+ public BackgroundWorkerTest()
+ {
+ provider = Substitute.For();
+ var serviceScope = Substitute.For();
+ var serviceScopeFactory = Substitute.For();
+ serviceScopeFactory.CreateScope().Returns(serviceScope);
+ ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory);
+
+ service = new BackgroundWorker(provider);
+ typeof(BackgroundWorker)
+ .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(service, TimeSpan.FromMilliseconds(1));
+ }
+
+ [Fact]
+ public async Task Enqueue_n_works()
+ {
+ var workCount = 10;
+ var result = 0;
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ result++;
+ return Task.Delay(1);
+ }
+
+ //act
+ for (int i = 0; i < workCount; i++)
+ {
+ var work = Work.CreateByDelegate(i.ToString(), workAction);
+ service.Enqueue(work);
+ }
+
+ await service.ExecuteTask;
+
+ //assert
+ Assert.Equal(workCount, result);
+ }
+
+ [Fact]
+ public async Task Enqueue_continues_after_exceptions()
+ {
+ var expectadResult = 42;
+ var result = 0;
+
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ result = expectadResult;
+ return Task.CompletedTask;
+ }
+ var goodWork = Work.CreateByDelegate("", workAction);
+
+ Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ => throw new Exception();
+
+ var badWork = Work.CreateByDelegate("", failAction);
+ badWork.OnErrorAsync = (id, exception, token) => throw new Exception();
+
+ //act
+ service.Enqueue(badWork);
+ service.Enqueue(goodWork);
+
+ await service.ExecuteTask;
+
+ //assert
+ Assert.Equal(expectadResult, result);
+ Assert.Equal(1, service.Felled.Count);
+ Assert.Equal(1, service.Done.Count);
+ }
+
+ [Fact]
+ public async Task TryRemove()
+ {
+ var workCount = 5;
+ var result = 0;
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ result++;
+ return Task.Delay(10);
+ }
+
+ //act
+ for (int i = 0; i < workCount; i++)
+ {
+ var work = Work.CreateByDelegate(i.ToString(), workAction);
+ service.Enqueue(work);
+ }
+
+ var removed = service.TryRemoveFromQueue((workCount - 1).ToString());
+
+ await service.ExecuteTask;
+
+ //assert
+ Assert.True(removed);
+ Assert.Equal(workCount - 1, result);
+ Assert.Equal(workCount - 1, service.Done.Count);
+ }
+}
diff --git a/AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs b/AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs
rename to AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs b/AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs b/AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs
similarity index 99%
rename from AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs
index f0ccd6c5..2a9635c9 100644
--- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs
+++ b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs
@@ -366,7 +366,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
Assert.Equal(2, state.IdState);
- backgroundWorkerMock.Verify(s => s.WorkStore.RunOnceQueue.Enqueue(It.IsAny()));
+ backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny()));
}
[Fact]
diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/FileServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs b/AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs b/AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs
new file mode 100644
index 00000000..d0c16285
--- /dev/null
+++ b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs
@@ -0,0 +1,97 @@
+using AsbCloudInfrastructure.Background;
+using DocumentFormat.OpenXml.Drawing.Charts;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace AsbCloudWebApi.Tests.Services;
+
+public class PeriodicBackgroundWorkerTest
+{
+ private IServiceProvider provider;
+ private PeriodicBackgroundWorker service;
+
+ public PeriodicBackgroundWorkerTest()
+ {
+ provider = Substitute.For();
+ var serviceScope = Substitute.For();
+ var serviceScopeFactory = Substitute.For();
+ serviceScopeFactory.CreateScope().Returns(serviceScope);
+ ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory);
+
+ service = new PeriodicBackgroundWorker(provider);
+ typeof(PeriodicBackgroundWorker)
+ .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)?
+ .SetValue(service, TimeSpan.FromMilliseconds(1));
+
+ typeof(PeriodicBackgroundWorker)
+ .GetField("executePeriod", BindingFlags.NonPublic | BindingFlags.Instance)?
+ .SetValue(service, TimeSpan.FromMilliseconds(1));
+ }
+
+ [Fact]
+ public async Task WorkRunsTwice()
+ {
+ var workCount = 2;
+ var periodMs = 100d;
+
+ var period = TimeSpan.FromMilliseconds(periodMs);
+
+ var result = 0;
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ result++;
+ return Task.CompletedTask;
+ }
+
+ //act
+ var work = Work.CreateByDelegate("", workAction);
+
+ var stopwatch = Stopwatch.StartNew();
+ service.Add(work, period);
+
+ var delay = (periodMs / 20) + (periodMs * workCount) - stopwatch.ElapsedMilliseconds;
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+
+ //assert
+ Assert.Equal(workCount, result);
+ }
+
+
+ [Fact]
+ public async Task Enqueue_continues_after_exceptions()
+ {
+ var expectadResult = 42;
+ var result = 0;
+
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ result = expectadResult;
+ return Task.CompletedTask;
+ }
+ var goodWork = Work.CreateByDelegate("", workAction);
+
+ Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ => throw new Exception();
+
+ var badWork = Work.CreateByDelegate("", failAction);
+ badWork.OnErrorAsync = (id, exception, token) => throw new Exception();
+
+ //act
+ service.Add(badWork, TimeSpan.FromSeconds(2));
+ service.Add(goodWork, TimeSpan.FromSeconds(2));
+
+ await Task.Delay(TimeSpan.FromMilliseconds(20));
+
+ //assert
+ Assert.Equal(expectadResult, result);
+ Assert.Equal(1, badWork.CountErrors);
+ Assert.Equal(1, goodWork.CountComplete);
+ Assert.Equal(1, goodWork.CountStart);
+ }
+}
diff --git a/AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs b/AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs
rename to AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs b/AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs b/AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs
rename to AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs
similarity index 100%
rename from AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs
rename to AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs
diff --git a/AsbCloudWebApi.Tests/Services/WorkTest.cs b/AsbCloudWebApi.Tests/Services/WorkTest.cs
new file mode 100644
index 00000000..ea826565
--- /dev/null
+++ b/AsbCloudWebApi.Tests/Services/WorkTest.cs
@@ -0,0 +1,126 @@
+using AsbCloudInfrastructure.Background;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace AsbCloudWebApi.Tests.Services
+{
+ public class WorkTest
+ {
+ private IServiceProvider provider;
+
+ public WorkTest()
+ {
+ provider = Substitute.For();
+ var serviceScope = Substitute.For();
+ var serviceScopeFactory = Substitute.For();
+ serviceScopeFactory.CreateScope().Returns(serviceScope);
+ ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory);
+ }
+
+ [Fact]
+ public async Task Work_done_with_success()
+ {
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ => Task.CompletedTask;
+
+ var work = Work.CreateByDelegate("", workAction);
+
+ //act
+ var begin = DateTime.Now;
+ await work.Start(provider, CancellationToken.None);
+ var done = DateTime.Now;
+ var executionTime = done - begin;
+
+ //assert
+ Assert.Equal(1, work.CountComplete);
+ Assert.Equal(1, work.CountStart);
+ Assert.Equal(0, work.CountErrors);
+ Assert.Null(work.CurrentState);
+ Assert.Null(work.LastError);
+
+ var lastState = work.LastComplete;
+ Assert.NotNull(lastState);
+ Assert.InRange(lastState.Start, begin, done - 0.5 * executionTime);
+ Assert.InRange(lastState.End, done - 0.5 * executionTime, done);
+ Assert.InRange(lastState.ExecutionTime, TimeSpan.Zero, executionTime);
+ }
+
+ [Fact]
+ public async Task Work_calls_callback()
+ {
+ var expectedState = "42";
+ var expectedProgress = 42d;
+
+ var timeout = TimeSpan.FromMilliseconds(40);
+
+ Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ callback.Invoke(expectedState, expectedProgress);
+ return Task.Delay(timeout);
+ }
+
+ var work = Work.CreateByDelegate("", workAction);
+
+ //act
+ var begin = DateTime.Now;
+ _ = work.Start(provider, CancellationToken.None);
+ await Task.Delay(timeout/3);
+
+ //assert
+ Assert.Equal(0, work.CountComplete);
+ Assert.Equal(1, work.CountStart);
+ Assert.Equal(0, work.CountErrors);
+ Assert.NotNull(work.CurrentState);
+ Assert.Null(work.LastComplete);
+ Assert.Null(work.LastError);
+
+ var currentState = work.CurrentState;
+ Assert.NotNull(currentState);
+ Assert.InRange(currentState.Start, begin, begin + timeout);
+ Assert.InRange(currentState.StateUpdate, begin, begin + timeout);
+ Assert.Equal(expectedState, currentState.State);
+ Assert.Equal(expectedProgress, currentState.Progress);
+ }
+
+
+ [Fact]
+ public async Task Work_fails_with_info()
+ {
+ var expectedState = "41";
+ var expectedErrorText = "42";
+ var minWorkTime = TimeSpan.FromMilliseconds(10);
+
+ async Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token)
+ {
+ await Task.Delay(minWorkTime);
+ callback(expectedState, 0);
+ throw new Exception(expectedErrorText);
+ }
+
+ var work = Work.CreateByDelegate("", workAction);
+
+ //act
+ var begin = DateTime.Now;
+ await work.Start(provider, CancellationToken.None);
+
+ //assert
+ Assert.Equal(0, work.CountComplete);
+ Assert.Equal(1, work.CountStart);
+ Assert.Equal(1, work.CountErrors);
+ Assert.Null(work.CurrentState);
+ Assert.Null(work.LastComplete);
+
+ var error = work.LastError;
+ Assert.NotNull(error);
+ Assert.InRange(error.Start, begin, DateTime.Now);
+ Assert.InRange(error.End, begin, DateTime.Now);
+ Assert.InRange(error.ExecutionTime, minWorkTime, DateTime.Now - begin);
+ Assert.Contains(expectedErrorText, error.ErrorText, StringComparison.InvariantCultureIgnoreCase);
+ Assert.Equal(expectedState, error.State);
+ }
+ }
+}
diff --git a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs
index 0a8c02c0..bb0cdc43 100644
--- a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs
+++ b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs
@@ -14,23 +14,22 @@ namespace AsbCloudWebApi.Controllers
[ApiController]
public class BackgroundWorkController : ControllerBase
{
- private readonly BackgroundWorker backgroundWorker;
+ private readonly BackgroundWorker worker;
- public BackgroundWorkController(BackgroundWorker backgroundWorker)
+ public BackgroundWorkController(BackgroundWorker worker)
{
- this.backgroundWorker = backgroundWorker;
+ this.worker = worker;
}
[HttpGet]
public IActionResult GetAll()
{
var result = new {
- CurrentWork = (BackgroundWorkDto?)backgroundWorker.CurrentWork,
- backgroundWorker.MainLoopLastException,
- RunOnceQueue = backgroundWorker.WorkStore.RunOnceQueue.Select(work => (BackgroundWorkDto)work),
- Periodics = backgroundWorker.WorkStore.Periodics.Select(work => (BackgroundWorkDto)work.Work),
- Done = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work),
- Felled = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work),
+ CurrentWork = (BackgroundWorkDto?)worker.CurrentWork,
+ worker.MainLoopLastException,
+ RunOnceQueue = worker.Works.Select(work => (BackgroundWorkDto)work),
+ Done = worker.Done.Select(work => (BackgroundWorkDto)work),
+ Felled = worker.Felled.Select(work => (BackgroundWorkDto)work),
};
return Ok(result);
}
@@ -38,7 +37,7 @@ namespace AsbCloudWebApi.Controllers
[HttpGet("current")]
public IActionResult GetCurrent()
{
- var work = backgroundWorker.CurrentWork;
+ var work = worker.CurrentWork;
if (work == null)
return NoContent();
@@ -48,22 +47,22 @@ namespace AsbCloudWebApi.Controllers
[HttpGet("failed")]
public IActionResult GetFelled()
{
- var result = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work);
+ var result = worker.Felled.Select(work => (BackgroundWorkDto)work);
return Ok(result);
}
[HttpGet("done")]
public IActionResult GetDone()
{
- var result = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work);
+ var result = worker.Done.Select(work => (BackgroundWorkDto)work);
return Ok(result);
}
[HttpPost("restart"), Obsolete("temporary method")]
public async Task RestartAsync(CancellationToken token)
{
- await backgroundWorker.StopAsync(token);
- await backgroundWorker.StartAsync(token);
+ await worker.StopAsync(token);
+ await worker.StartAsync(token);
return Ok();
}
}
diff --git a/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs
new file mode 100644
index 00000000..d7f666a0
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs
@@ -0,0 +1,44 @@
+using AsbCloudApp.Data;
+using AsbCloudInfrastructure.Background;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudWebApi.Controllers
+{
+ [Route("api/[controller]")]
+ [Authorize]
+ [ApiController]
+ public class PeriodicBackgroundWorkerController : ControllerBase
+ {
+ private readonly PeriodicBackgroundWorker worker;
+
+ public PeriodicBackgroundWorkerController(PeriodicBackgroundWorker worker)
+ {
+ this.worker = worker;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ var result = new
+ {
+ currentWork = (BackgroundWorkDto?)worker.CurrentWork,
+ worker.MainLoopLastException,
+ works = worker.Works.Select(work => (BackgroundWorkDto)work.Work),
+ };
+ return Ok(result);
+ }
+
+ [HttpPost("restart"), Obsolete("temporary method")]
+ public async Task RestartAsync(CancellationToken token)
+ {
+ await worker.StopAsync(token);
+ await worker.StartAsync(token);
+ return Ok();
+ }
+ }
+}
diff --git a/AsbCloudWebApi/SignalR/ReportsHub.cs b/AsbCloudWebApi/SignalR/ReportsHub.cs
index 1097c5ef..66292d05 100644
--- a/AsbCloudWebApi/SignalR/ReportsHub.cs
+++ b/AsbCloudWebApi/SignalR/ReportsHub.cs
@@ -26,7 +26,7 @@ namespace AsbCloudWebApi.SignalR
await base.AddToGroup(groupName);
var workId = groupName.Replace("Report_", "");
- var work = backgroundWorker.WorkStore.RunOnceQueue.FirstOrDefault(work => work.Id == workId);
+ var work = backgroundWorker.Works.FirstOrDefault(work => work.Id == workId);
var progress = new ReportProgressDto()
{
diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs
index 962e5e0f..899d4ad7 100644
--- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs
+++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs
@@ -29,12 +29,12 @@ public class SignalRNotificationTransportService : INotificationTransportService
{
var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x");
- if (backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId))
+ if (backgroundWorker.Works.Any(w => w.Id == workId))
return Task.CompletedTask;
var workAction = MakeSignalRSendWorkAction(notifications);
var work = Work.CreateByDelegate(workId, workAction);
- backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work);
+ backgroundWorker.Enqueue(work);
return Task.CompletedTask;
}