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
}