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 }