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);
- }
- }
-}