Merge branch 'dev' into feature/8101318

This commit is contained in:
ngfrolov 2022-12-05 09:28:38 +05:00
commit a859c096d1
29 changed files with 807 additions and 610 deletions

View File

@ -73,7 +73,7 @@ namespace AsbCloudApp.Services
/// <param name="fileStream"></param> /// <param name="fileStream"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public async Task<FileInfoDto?> SaveAsync(int idWell, int? idUser, int idCategory, public async Task<FileInfoDto> SaveAsync(int idWell, int? idUser, int idCategory,
string fileFullName, Stream fileStream, CancellationToken token) string fileFullName, Stream fileStream, CancellationToken token)
{ {
//save info to db //save info to db
@ -93,7 +93,7 @@ namespace AsbCloudApp.Services
string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, fileFullName, fileId); string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, fileFullName, fileId);
await fileStorageRepository.SaveFileAsync(filePath, fileStream, token); await fileStorageRepository.SaveFileAsync(filePath, fileStream, token);
return await GetOrDefaultAsync(fileId, token); return (await GetOrDefaultAsync(fileId, token))!;
} }
/// <summary> /// <summary>

View File

@ -1,50 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudApp.Services
{
/// <summary>
/// Сервис выстраивает очередь из фоновых задач. Ограничивает количество одновременно выполняющихся задач.
/// </summary>
public interface IBackgroundWorkerService
{
/// <summary>
/// Проверка, есть ли задача в очереди
/// </summary>
/// <param name="id">идентификатор задачи</param>
/// <returns></returns>
bool Contains(string id);
/// <summary>
/// Добавляет в очередь задач новую задачу
/// </summary>
/// <param name="id">идентификатор задачи</param>
/// <param name="func">делегат</param>
/// <returns>id задачи в очереди</returns>
string Enqueue(string id, Func<string, CancellationToken, Task> func);
/// <summary>
/// Добавляет в очередь задач новую задачу
/// </summary>
/// <param name="func"></param>
/// <returns></returns>
string Enqueue(Func<string, CancellationToken, Task> func);
/// <summary>
/// Добавляет в очередь задач новую задачу
/// </summary>
/// <param name="id">идентификатор задачи</param>
/// <param name="func"></param>
/// <param name="onError"></param>
/// <returns></returns>
string Enqueue(string id, Func<string, CancellationToken, Task> func, Func<string, Exception, CancellationToken, Task> onError);
/// <summary>
/// Пробуем удалить задачу по идентификатору
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
bool TryRemove(string id);
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
namespace AsbCloudApp.Services namespace AsbCloudApp.Services
{ {
#nullable enable
/// <summary> /// <summary>
/// Сервис рапортов /// Сервис рапортов
/// </summary> /// </summary>
@ -16,7 +17,6 @@ namespace AsbCloudApp.Services
/// </summary> /// </summary>
int ReportCategoryId { get; } int ReportCategoryId { get; }
// TODO: rename this method
/// <summary> /// <summary>
/// Поставить рапорт в очередь на формирование /// Поставить рапорт в очередь на формирование
/// </summary> /// </summary>
@ -28,7 +28,7 @@ namespace AsbCloudApp.Services
/// <param name="end"></param> /// <param name="end"></param>
/// <param name="handleReportProgress"></param> /// <param name="handleReportProgress"></param>
/// <returns></returns> /// <returns></returns>
string CreateReport(int idWell, int idUser, int stepSeconds, string EnqueueCreateReportWork(int idWell, int idUser, int stepSeconds,
int format, DateTime begin, DateTime end, int format, DateTime begin, DateTime end,
Action<object, string> handleReportProgress); Action<object, string> handleReportProgress);
@ -49,7 +49,7 @@ namespace AsbCloudApp.Services
/// </summary> /// </summary>
/// <param name="idWell"></param> /// <param name="idWell"></param>
/// <returns></returns> /// <returns></returns>
DatesRangeDto GetDatesRangeOrDefault(int idWell); DatesRangeDto? GetDatesRangeOrDefault(int idWell);
/// <summary> /// <summary>
/// Список готовых рапортов /// Список готовых рапортов
@ -58,5 +58,7 @@ namespace AsbCloudApp.Services
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<ReportPropertiesDto>> GetAllReportsByWellAsync(int idWell, CancellationToken token); Task<IEnumerable<ReportPropertiesDto>> GetAllReportsByWellAsync(int idWell, CancellationToken token);
#nullable disable
} }
} }

View File

@ -61,19 +61,31 @@ namespace AsbCloudDb.Model
public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>(); public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>();
public DbSet<WITS.Record61> Record61 => Set<WITS.Record61>(); public DbSet<WITS.Record61> Record61 => Set<WITS.Record61>();
public int ReferenceCount { get; private set; }
public AsbCloudDbContext() : base() public AsbCloudDbContext() : base()
{ {
ReferenceCount++;
} }
public AsbCloudDbContext(DbContextOptions<AsbCloudDbContext> options) public AsbCloudDbContext(DbContextOptions<AsbCloudDbContext> options)
: base(options) : base(options)
{ {
ReferenceCount++;
}
~AsbCloudDbContext()
{
ReferenceCount--;
} }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
if (!optionsBuilder.IsConfigured) 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) protected override void OnModelCreating(ModelBuilder modelBuilder)

View File

@ -0,0 +1,101 @@
using AsbCloudInfrastructure.Services.DetectOperations;
using AsbCloudInfrastructure.Services.Subsystems;
using AsbCloudInfrastructure.Services;
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
/// <summary>
/// Сервис для фонового выполнения работы
/// </summary>
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 string? CurrentWorkId;
public BackgroundWorker(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
/// <summary>
/// Добавление задачи в очередь.
/// Не периодические задачи будут выполняться вперед.
/// </summary>
/// <param name="work"></param>
/// <exception cref="ArgumentException">Id mast be unique</exception>
public void Push(WorkBase work)
{
workQueue.Push(work);
}
/// <summary>
/// Проверяет наличие работы с указанным Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public bool Contains(string id)
{
return workQueue.Contains(id);
}
/// <summary>
/// Удаление работы по ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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;
}
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);
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);
}
}
CurrentWorkId = null;
await Task.Delay(minDelay, token);
}
}
}
#nullable disable
}

View File

@ -0,0 +1,69 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Background
{
#nullable enable
/// <summary>
/// Класс разовой работы.
/// Разовая работа приоритетнее периодической.
/// </summary>
public class WorkBase
{
/// <summary>
/// Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки.
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Делегат работы.
/// <para>
/// Параметры:
/// <list type="number">
/// <item>
/// <term>string</term>
/// <description>Id Идентификатор работы</description>
/// </item>
/// <item>
/// <term>IServiceProvider</term>
/// <description>Поставщик сервисов</description>
/// </item>
/// <item>
/// <term>CancellationToken</term>
/// <description>Токен отмены задачи</description>
/// </item>
/// </list>
/// </para>
/// </summary>
internal Func<string, IServiceProvider, CancellationToken, Task> ActionAsync { get; set; }
/// <summary>
/// Делегат обработки ошибки.
/// Не должен выполняться долго.
/// </summary>
public Func<string, Exception, CancellationToken, Task>? OnErrorAsync { get; set; }
/// <summary>
/// максимально допустимое время выполнения работы
/// </summary>
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1);
/// <summary>
/// Фактическое время успешного выполнения работы
/// </summary>
public TimeSpan? ExecutionTime { get; internal set; }
/// <summary>
/// Время последнего запуска
/// </summary>
public DateTime LastStart { get; set; }
public WorkBase(string id, Func<string, IServiceProvider, CancellationToken, Task> actionAsync)
{
Id = id;
ActionAsync = actionAsync;
}
}
#nullable disable
}

View File

@ -0,0 +1,36 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Background
{
#nullable enable
/// <summary>
/// Класс периодической работы.
/// </summary>
public class WorkPeriodic : WorkBase
{
/// <summary>
/// Период выполнения задачи
/// </summary>
public TimeSpan Period { get; set; }
/// <summary>
/// Время следующего запуска
/// </summary>
public DateTime NextStart => LastStart + Period;
/// <summary>
/// Класс периодической работы
/// </summary>
/// <param name="id">Идентификатор работы. Должен быть уникальным. Используется в логах и передается в колбэки</param>
/// <param name="actionAsync">Делегат работы</param>
/// <param name="period">Период выполнения задачи</param>
public WorkPeriodic(string id, Func<string, IServiceProvider, CancellationToken, Task> actionAsync, TimeSpan period)
: base(id, actionAsync)
{
Period = period;
}
}
#nullable disable
}

View File

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace AsbCloudInfrastructure.Background
{
#nullable enable
/// <summary>
/// <para>
/// Очередь работ
/// </para>
/// Не периодические задачи будут возвращаться первыми, как самые приоритетные.
/// </summary>
class WorkQueue
{
private Queue<WorkBase> Primary = new(8);
private readonly List<WorkPeriodic> Periodic = new(8);
/// <summary>
/// Добавление работы.
/// </summary>
/// <param name="work"></param>
/// <exception cref="ArgumentException">Id mast be unique</exception>
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);
}
/// <summary>
/// Удаление работы по ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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<WorkBase>(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;
}
/// <summary>
/// <para>
/// Возвращает приоритетную задачу.
/// </para>
/// <para>
/// Если приоритетные закончились, то ищет ближайшую периодическую.
/// Если до старта ближайшей периодической работы меньше 20 сек,
/// то этой задаче устанавливается время последнего запуска в now и она возвращается.
/// Если больше 20 сек, то возвращается null.
/// </para>
/// </summary>
/// <param name="maxTimeToNextWork"></param>
/// <returns></returns>
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
}

View File

@ -6,6 +6,7 @@ using AsbCloudApp.Services;
using AsbCloudApp.Services.Subsystems; using AsbCloudApp.Services.Subsystems;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudDb.Model.Subsystems; using AsbCloudDb.Model.Subsystems;
using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Repository; using AsbCloudInfrastructure.Repository;
using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services;
using AsbCloudInfrastructure.Services.DailyReport; using AsbCloudInfrastructure.Services.DailyReport;
@ -97,16 +98,13 @@ namespace AsbCloudInfrastructure
services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>()); services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>());
services.AddScoped<IEmailService, EmailService>(); services.AddScoped<IEmailService, EmailService>();
services.AddHostedService<OperationDetectionBackgroundService>();
services.AddHostedService<SubsystemOperationTimeBackgroundService>();
services.AddHostedService<LimitingParameterBackgroundService>();
services.AddSingleton(new WitsInfoService()); services.AddSingleton(new WitsInfoService());
services.AddSingleton(new InstantDataRepository()); services.AddSingleton(new InstantDataRepository());
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(configuration)); services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(configuration));
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(configuration)); services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(configuration));
services.AddSingleton<ITelemetryTracker, TelemetryTracker>(); services.AddSingleton<ITelemetryTracker, TelemetryTracker>();
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>(); services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>(); services.AddSingleton<BackgroundWorker>();
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration)); services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
services.AddTransient<IAuthService, AuthService>(); services.AddTransient<IAuthService, AuthService>();

View File

@ -1,55 +0,0 @@
using System;
namespace AsbCloudInfrastructure
{
public static class Helper
{
public static T Max<T>(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<T>(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<T>(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);
}
}
}

View File

@ -1,12 +0,0 @@
namespace Mapster
{
public static class MapsterExtension
{
//public static IEnumerable<TDestination> Adapt<TDestination>(this IEnumerable<object> sourceList)
//{
// return sourceList.Select(item => item.Adapt<TDestination>());
//}
}
}

View File

@ -10,7 +10,7 @@ namespace AsbCloudInfrastructure
{ {
public class ReportDataSourcePgCloud : IReportDataSource public class ReportDataSourcePgCloud : IReportDataSource
{ {
private readonly AsbCloudDbContext context; private readonly IAsbCloudDbContext context;
private readonly int? idTelemetry; private readonly int? idTelemetry;
private readonly WellInfoReport info; private readonly WellInfoReport info;
@ -25,7 +25,7 @@ namespace AsbCloudInfrastructure
{3, "Информация"}, {3, "Информация"},
}; };
public ReportDataSourcePgCloud(AsbCloudDbContext context, int idWell) public ReportDataSourcePgCloud(IAsbCloudDbContext context, int idWell)
{ {
this.context = context; this.context = context;
@ -65,6 +65,7 @@ namespace AsbCloudInfrastructure
public AnalyzeResult Analyze() public AnalyzeResult Analyze()
{ {
// TODO: Replace by linq methods.
var messagesStat = (from item in context.TelemetryMessages var messagesStat = (from item in context.TelemetryMessages
where item.IdTelemetry == idTelemetry where item.IdTelemetry == idTelemetry
group item.DateTime by item.IdTelemetry into g group item.DateTime by item.IdTelemetry into g

View File

@ -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
{
/// <summary>
/// Сервис выстраивает очередь из фоновых задач. Ограничивает количество одновременно выполняющихся задач.
/// </summary>
public class BackgroundWorkerService : IDisposable, IBackgroundWorkerService
{
private readonly Worker[] workers;
private readonly Dictionary<string, Work> works = new Dictionary<string, Work>();
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<string, CancellationToken, Task> func)
{
var work = new Work
{
ActionAsync = func
};
return Enqueue(work);
}
public string Enqueue(string id, Func<string, CancellationToken, Task> func)
{
var work = new Work(id, func);
return Enqueue(work);
}
public string Enqueue(string id, Func<string, CancellationToken, Task> func, Func<string, Exception, CancellationToken, Task> 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<string, CancellationToken, Task> ActionAsync { get; set; }
public Func<string, Exception, CancellationToken, Task> OnErrorAsync { get; set; }
public Work()
{
Id = Guid.NewGuid().ToString();
}
public Work(string id, Func<string, CancellationToken, Task> actionAsync)
{
Id = id;
ActionAsync = actionAsync;
}
}
}

View File

@ -268,7 +268,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
return query; return query;
} }
private DetectedOperationDto Convert(DetectedOperation operation, WellDto well, IEnumerable<OperationValueDto> operationValues, IEnumerable<ScheduleDto> schedules) private static DetectedOperationDto Convert(DetectedOperation operation, WellDto well, IEnumerable<OperationValueDto> operationValues, IEnumerable<ScheduleDto> schedules)
{ {
var dto = operation.Adapt<DetectedOperationDto>(); var dto = operation.Adapt<DetectedOperationDto>();
dto.IdWell = well.Id; dto.IdWell = well.Id;

View File

@ -1,7 +1,5 @@
using AsbCloudDb.Model; using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -9,14 +7,16 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudInfrastructure.Services.DetectOperations.Detectors; using AsbCloudInfrastructure.Services.DetectOperations.Detectors;
using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services.DetectOperations namespace AsbCloudInfrastructure.Services.DetectOperations
{ {
#nullable enable #nullable enable
public class OperationDetectionBackgroundService : BackgroundService public static class OperationDetectionWorkFactory
{ {
private readonly string connectionString; private const string workId = "Operation detection";
private readonly TimeSpan period = TimeSpan.FromHours(1); private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
private static readonly DetectorAbstract[] detectors = new DetectorAbstract[] private static readonly DetectorAbstract[] detectors = new DetectorAbstract[]
{ {
@ -31,49 +31,18 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
new DetectorTemplatingWhileDrilling(), new DetectorTemplatingWhileDrilling(),
}; };
public OperationDetectionBackgroundService(IConfiguration configuration) public static WorkPeriodic MakeWork()
{ {
connectionString = configuration.GetConnectionString("DefaultConnection"); 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; using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
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<int> DetectedAllTelemetriesAsync(IAsbCloudDbContext db, CancellationToken token)
{
var lastDetectedDates = await db.DetectedOperations var lastDetectedDates = await db.DetectedOperations
.GroupBy(o => o.IdTelemetry) .GroupBy(o => o.IdTelemetry)
.Select(g => new .Select(g => new
@ -88,7 +57,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
.Select(t => t.Id) .Select(t => t.Id)
.ToListAsync(token); .ToListAsync(token);
var JounedlastDetectedDates = telemetryIds var joinedlastDetectedDates = telemetryIds
.GroupJoin(lastDetectedDates, .GroupJoin(lastDetectedDates,
t => t, t => t,
o => o.IdTelemetry, o => o.IdTelemetry,
@ -97,8 +66,9 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
IdTelemetry = outer, IdTelemetry = outer,
inner.SingleOrDefault()?.LastDate, inner.SingleOrDefault()?.LastDate,
}); });
var affected = 0; var affected = 0;
foreach (var item in JounedlastDetectedDates) foreach (var item in joinedlastDetectedDates)
{ {
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var newOperations = await DetectOperationsAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); 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); affected += await db.SaveChangesAsync(token);
} }
} }
return affected;
} }
private static async Task<IEnumerable<DetectedOperation>> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) private static async Task<IEnumerable<DetectedOperation>> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token)

View File

@ -3,10 +3,11 @@ using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository; using AsbCloudInfrastructure.Background;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -16,6 +17,7 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.DrillingProgram namespace AsbCloudInfrastructure.Services.DrillingProgram
{ {
# nullable enable
public class DrillingProgramService : IDrillingProgramService public class DrillingProgramService : IDrillingProgramService
{ {
private static readonly Dictionary<string, DrillingProgramCreateError> drillingProgramCreateErrors = new Dictionary<string, DrillingProgramCreateError>(); private static readonly Dictionary<string, DrillingProgramCreateError> drillingProgramCreateErrors = new Dictionary<string, DrillingProgramCreateError>();
@ -25,9 +27,8 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private readonly IUserRepository userRepository; private readonly IUserRepository userRepository;
private readonly IWellService wellService; private readonly IWellService wellService;
private readonly IConfiguration configuration; private readonly IConfiguration configuration;
private readonly IBackgroundWorkerService backgroundWorker; private readonly BackgroundWorker backgroundWorker;
private readonly IEmailService emailService; private readonly IEmailService emailService;
private readonly string connectionString;
private const int idFileCategoryDrillingProgram = 1000; private const int idFileCategoryDrillingProgram = 1000;
private const int idFileCategoryDrillingProgramPartsStart = 1001; private const int idFileCategoryDrillingProgramPartsStart = 1001;
@ -55,7 +56,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
IUserRepository userRepository, IUserRepository userRepository,
IWellService wellService, IWellService wellService,
IConfiguration configuration, IConfiguration configuration,
IBackgroundWorkerService backgroundWorker, BackgroundWorker backgroundWorker,
IEmailService emailService) IEmailService emailService)
{ {
this.context = context; this.context = context;
@ -64,7 +65,6 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
this.wellService = wellService; this.wellService = wellService;
this.configuration = configuration; this.configuration = configuration;
this.backgroundWorker = backgroundWorker; this.backgroundWorker = backgroundWorker;
this.connectionString = configuration.GetConnectionString("DefaultConnection");
this.emailService = emailService; this.emailService = emailService;
} }
@ -127,7 +127,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
{ {
Parts = parts, Parts = parts,
Program = files.FirstOrDefault(f => f.IdCategory == idFileCategoryDrillingProgram) Program = files.FirstOrDefault(f => f.IdCategory == idFileCategoryDrillingProgram)
.Adapt<FileInfoDto>(), ?.Adapt<FileInfoDto>(),
PermissionToEdit = userRepository.HasPermission(idUser, "DrillingProgram.edit"), PermissionToEdit = userRepository.HasPermission(idUser, "DrillingProgram.edit"),
}; };
@ -157,7 +157,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
else else
state.IdState = idStateNotInitialized; state.IdState = idStateNotInitialized;
await TryEnqueueMakeProgramAsync(idWell, state, token); await EnqueueMakeProgramWorkAsync(idWell, state, token);
return state; return state;
} }
@ -299,7 +299,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(p => p.IdWell == fileInfo.IdWell && p.IdFileCategory == fileInfo.IdCategory, token); .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) if (user is null)
throw new ForbidException($"User {idUser} is not in the approvers list."); throw new ForbidException($"User {idUser} is not in the approvers list.");
@ -323,11 +323,11 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
else else
{ {
// если все согласованты согласовали - оповещаем публикатора // если все согласованты согласовали - оповещаем публикатора
var approvers = part.RelatedUsers var approvers = part!.RelatedUsers
.Where(u => u.IdUserRole == idUserRoleApprover); .Where(u => u.IdUserRole == idUserRoleApprover);
if (approvers if (approvers
.All(user => fileInfo.FileMarks .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))) (fileMarkDto.IdMarkType == idMarkTypeApprove && user.IdUser == idUser)))
{ {
await NotifyPublisherOnFullAccepAsync(fileMarkDto, token); await NotifyPublisherOnFullAccepAsync(fileMarkDto, token);
@ -359,7 +359,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken token) private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken token)
{ {
var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, 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 user = file.Author;
var factory = new DrillingMailBodyFactory(configuration); var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован"); var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован");
@ -371,7 +371,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token) private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token)
{ {
var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, 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 user = file.Author;
var factory = new DrillingMailBodyFactory(configuration); var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен"); var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен");
@ -405,12 +405,12 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
emailService.EnqueueSend(user.Email, subject, body); emailService.EnqueueSend(user.Email, subject, body);
} }
private DrillingProgramPartDto ConvertPart(int idUser, List<FileCategory> fileCategories, List<AsbCloudDb.Model.FileInfo> files, DrillingProgramPart partEntity, double timezoneOffset) private static DrillingProgramPartDto ConvertPart(int idUser, List<FileCategory> fileCategories, List<AsbCloudDb.Model.FileInfo> files, DrillingProgramPart partEntity, double timezoneOffset)
{ {
var part = new DrillingProgramPartDto var part = new DrillingProgramPartDto
{ {
IdFileCategory = partEntity.IdFileCategory, IdFileCategory = partEntity.IdFileCategory,
Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory).Name, Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory)!.Name,
Approvers = partEntity.RelatedUsers Approvers = partEntity.RelatedUsers
.Where(r => r.IdUserRole == idUserRoleApprover) .Where(r => r.IdUserRole == idUserRoleApprover)
.Select(r => r.User.Adapt<UserDto>()), .Select(r => r.User.Adapt<UserDto>()),
@ -464,31 +464,27 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
return part; 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) if (state.IdState == idStateCreating)
{ {
var workId = MakeWorkId(idWell); var workId = MakeWorkId(idWell);
if (!backgroundWorker.Contains(workId)) 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 resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx";
var tempResultFilePath = Path.Combine(Path.GetTempPath(), "drillingProgram", resultFileName); 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<AsbCloudDbContext>() var context = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
.UseNpgsql(connectionString) var fileService = serviceProvider.GetRequiredService<FileService>();
.Options;
using var context = new AsbCloudDbContext(contextOptions);
var fileRepository = new FileRepository(context);
var fileStorageRepository = new FileStorageRepository();
var fileService = new FileService(fileRepository, fileStorageRepository);
var files = state.Parts.Select(p => fileService.GetUrl(p.File)); var files = state.Parts.Select(p => fileService.GetUrl(p.File));
DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath, state.Parts, well); DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath, state.Parts, well);
await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token); 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}"; var message = $"Не удалось сформировать программу бурения по скважине {well?.Caption}";
drillingProgramCreateErrors[workId] = new() drillingProgramCreateErrors[workId] = new()
@ -497,9 +493,15 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
Exception = exception.Message, Exception = exception.Message,
}; };
return Task.CompletedTask; 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<int> RemoveDrillingProgramAsync(int idWell, CancellationToken token) private async Task<int> RemoveDrillingProgramAsync(int idWell, CancellationToken token)
{ {
var workId = MakeWorkId(idWell); var workId = MakeWorkId(idWell);
backgroundWorker.TryRemove(workId); backgroundWorker.Delete(workId);
var filesIds = await context.Files var filesIds = await context.Files
.Where(f => f.IdWell == idWell && .Where(f => f.IdWell == idWell &&
@ -529,4 +531,5 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private static string MakeWorkId(int idWell) private static string MakeWorkId(int idWell)
=> $"Make drilling program for wellId {idWell}"; => $"Make drilling program for wellId {idWell}";
} }
#nullable disable
} }

View File

@ -8,26 +8,28 @@ using System.Linq;
using System.Net.Mail; using System.Net.Mail;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudInfrastructure.Background;
namespace AsbCloudInfrastructure.Services namespace AsbCloudInfrastructure.Services
{ {
#nullable enable
public class EmailService : IEmailService public class EmailService : IEmailService
{ {
private readonly IBackgroundWorkerService backgroundWorker; private readonly BackgroundWorker backgroundWorker;
private readonly bool IsConfigured; private readonly bool IsConfigured;
private readonly string sender; private readonly string sender;
private readonly string smtpServer; private readonly string smtpServer;
private readonly string smtpPassword; private readonly string smtpPassword;
public EmailService(IBackgroundWorkerService backgroundWorker, IConfiguration configuration) public EmailService(BackgroundWorker backgroundWorker, IConfiguration configuration)
{ {
sender = configuration.GetValue<string>("email:sender", null); sender = configuration.GetValue("email:sender", string.Empty);
smtpPassword = configuration.GetValue<string>("email:password", null); smtpPassword = configuration.GetValue("email:password", string.Empty);
smtpServer = configuration.GetValue<string>("email:smtpServer", null); smtpServer = configuration.GetValue("email:smtpServer", string.Empty);
var configError = (string.IsNullOrEmpty(sender) || var configError = string.IsNullOrEmpty(sender) ||
string.IsNullOrEmpty(smtpPassword) || string.IsNullOrEmpty(smtpPassword) ||
string.IsNullOrEmpty(smtpServer)); string.IsNullOrEmpty(smtpServer);
IsConfigured = !configError; IsConfigured = !configError;
@ -44,20 +46,21 @@ namespace AsbCloudInfrastructure.Services
Trace.TraceWarning("smtp is not configured"); Trace.TraceWarning("smtp is not configured");
return; return;
} }
var jobId = CalcJobId(addresses, subject, htmlBody); var workId = MakeWorkId(addresses, subject, htmlBody);
if (!backgroundWorker.Contains(jobId)) if (!backgroundWorker.Contains(workId))
{ {
var action = MakeEmailSendJobAsync(addresses, subject, htmlBody); var workAction = MakeEmailSendWorkAction(addresses, subject, htmlBody);
backgroundWorker.Enqueue(jobId, action); var work = new WorkBase(workId, workAction);
backgroundWorker.Push(work);
} }
} }
private Func<string, CancellationToken, Task> MakeEmailSendJobAsync(IEnumerable<string> addresses, string subject, string htmlBody) private Func<string, IServiceProvider, CancellationToken, Task> MakeEmailSendWorkAction(IEnumerable<string> addresses, string subject, string htmlBody)
{ {
var mailAddresses = new List<MailAddress>(); var mailAddresses = new List<MailAddress>();
foreach (var address in addresses) foreach (var address in addresses)
{ {
if (MailAddress.TryCreate(address, out MailAddress mailAddress)) if (MailAddress.TryCreate(address, out MailAddress? mailAddress))
mailAddresses.Add(mailAddress); mailAddresses.Add(mailAddress);
else else
Trace.TraceWarning($"Mail {address} is not correct."); Trace.TraceWarning($"Mail {address} is not correct.");
@ -69,16 +72,16 @@ namespace AsbCloudInfrastructure.Services
if (string.IsNullOrEmpty(subject)) if (string.IsNullOrEmpty(subject))
throw new ArgumentInvalidException($"{nameof(subject)} should be set", nameof(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 from = new MailAddress(sender);
var message = new MailMessage
var message = new MailMessage(); {
message.From = from; From = from
};
foreach (var mailAddress in mailAddresses) foreach (var mailAddress in mailAddresses)
message.To.Add(mailAddress); message.To.Add(mailAddress);
//message.To.Add("support@digitaldrilling.ru");
message.BodyEncoding = System.Text.Encoding.UTF8; message.BodyEncoding = System.Text.Encoding.UTF8;
message.Body = htmlBody; message.Body = htmlBody;
@ -91,12 +94,12 @@ namespace AsbCloudInfrastructure.Services
client.Credentials = new System.Net.NetworkCredential(sender, smtpPassword); client.Credentials = new System.Net.NetworkCredential(sender, smtpPassword);
await client.SendMailAsync(message, token); 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<string> addresses, string subject, string content) private static string MakeWorkId(IEnumerable<string> addresses, string subject, string content)
{ {
var hash = GetHashCode(addresses); var hash = GetHashCode(addresses);
hash ^= subject.GetHashCode(); hash ^= subject.GetHashCode();
@ -114,4 +117,5 @@ namespace AsbCloudInfrastructure.Services
return hash; return hash;
} }
} }
#nullable disable
} }

View File

@ -1,61 +1,37 @@
using AsbCloudDb.Model; using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System; using System;
using System.Data.Common; using System.Data.Common;
using System.Data; using System.Data;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services namespace AsbCloudInfrastructure.Services
{ {
#nullable enable #nullable enable
internal class LimitingParameterBackgroundService : BackgroundService internal static class LimitingParameterCalcWorkFactory
{ {
private readonly string connectionString; private const string workId = "Limiting parameter calc";
private readonly TimeSpan period = TimeSpan.FromHours(1); private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
public LimitingParameterBackgroundService(IConfiguration configuration) public static WorkPeriodic MakeWork()
{ {
connectionString = configuration.GetConnectionString("DefaultConnection"); var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod)
{
Timeout = TimeSpan.FromMinutes(30)
};
return workPeriodic;
} }
protected override async Task ExecuteAsync(CancellationToken token) // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД.
private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token)
{ {
var timeToStart = DateTime.Now; using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var options = new DbContextOptionsBuilder<AsbCloudDbContext>() var lastDetectedDates = await db.LimitingParameter
.UseNpgsql(connectionString)
.Options;
while (!token.IsCancellationRequested)
{
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);
}
}
private static async Task<int> LimitingParameterAsync(IAsbCloudDbContext context, CancellationToken token)
{
var lastDetectedDates = await context.LimitingParameter
.GroupBy(o => o.IdTelemetry) .GroupBy(o => o.IdTelemetry)
.Select(g => new .Select(g => new
{ {
@ -64,7 +40,7 @@ namespace AsbCloudInfrastructure.Services
}) })
.ToListAsync(token); .ToListAsync(token);
var telemetryIds = await context.Telemetries var telemetryIds = await db.Telemetries
.Where(t => t.Info != null && t.TimeZone != null) .Where(t => t.Info != null && t.TimeZone != null)
.Select(t => t.Id) .Select(t => t.Id)
.ToListAsync(token); .ToListAsync(token);
@ -79,17 +55,15 @@ namespace AsbCloudInfrastructure.Services
inner.SingleOrDefault()?.LastDate, inner.SingleOrDefault()?.LastDate,
}); });
var affected = 0;
foreach (var item in telemetryLastDetectedDates) 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) if (newLimitingParameters?.Any() == true)
{ {
context.LimitingParameter.AddRange(newLimitingParameters); db.LimitingParameter.AddRange(newLimitingParameters);
affected += await context.SaveChangesAsync(token); await db.SaveChangesAsync(token);
} }
} }
return affected;
} }
private static async Task<IEnumerable<LimitingParameter>> GetLimitingParameterAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token) private static async Task<IEnumerable<LimitingParameter>> GetLimitingParameterAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token)

View File

@ -1,11 +1,11 @@
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository; using AsbCloudInfrastructure.Background;
using AsbSaubReport; using AsbSaubReport;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -15,30 +15,32 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services namespace AsbCloudInfrastructure.Services
{ {
#nullable enable
public class ReportService : IReportService public class ReportService : IReportService
{ {
private readonly IAsbCloudDbContext db; private readonly IAsbCloudDbContext db;
private readonly string connectionString;
private readonly ITelemetryService telemetryService; private readonly ITelemetryService telemetryService;
private readonly IWellService wellService; private readonly IWellService wellService;
private readonly IBackgroundWorkerService backgroundWorkerService; private readonly BackgroundWorker 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;
}
public int ReportCategoryId { get; private set; } 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<object, string> progressHandler) DateTime end, Action<object, string> progressHandler)
{ {
var timezoneOffset = wellService.GetTimezone(idWell).Hours; var timezoneOffset = wellService.GetTimezone(idWell).Hours;
@ -47,12 +49,12 @@ namespace AsbCloudInfrastructure.Services
var beginRemote = begin.ToTimeZoneOffsetHours(timezoneOffset); var beginRemote = begin.ToTimeZoneOffsetHours(timezoneOffset);
var endRemote = end.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<AsbCloudDbContext>() using var context = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
.UseNpgsql(connectionString) var fileService = serviceProvider.GetRequiredService<FileService>();
.Options;
using var context = new AsbCloudDbContext(contextOptions);
var tempDir = Path.Combine(Path.GetTempPath(), "report"); var tempDir = Path.Combine(Path.GetTempPath(), "report");
@ -66,10 +68,7 @@ namespace AsbCloudInfrastructure.Services
}; };
generator.Make(reportFileName); generator.Make(reportFileName);
var fileRepository = new FileRepository(context); var fileInfo = (await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token))!;
var fileStorageRepository = new FileStorageRepository();
var fileService = new FileService(fileRepository, fileStorageRepository);
var fileInfo = await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token);
progressHandler.Invoke(new progressHandler.Invoke(new
{ {
@ -91,13 +90,17 @@ namespace AsbCloudInfrastructure.Services
}; };
context.ReportProperties.Add(newReportProperties); context.ReportProperties.Add(newReportProperties);
context.SaveChanges(); context.SaveChanges();
}); };
var work = new WorkBase(workId, workAction);
backgroundWorkerService.Push(work);
progressHandler.Invoke(new ReportProgressDto progressHandler.Invoke(new ReportProgressDto
{ {
Operation = "Ожидает начала в очереди.", Operation = "Ожидает начала в очереди.",
Progress = 0f, Progress = 0f,
}, newReportId); }, workId);
return newReportId; return workId;
} }
public int GetReportPagesCount(int idWell, DateTime begin, DateTime end, int stepSeconds, int format) 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 beginRemote = begin.ToTimeZoneOffsetHours(timezoneOffset);
var endRemote = end.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(); var pagesCount = generator.GetPagesCount();
return pagesCount; return pagesCount;
} }
public DatesRangeDto GetDatesRangeOrDefault(int idWell) public DatesRangeDto? GetDatesRangeOrDefault(int idWell)
{ {
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell); var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null) if (idTelemetry is null)
@ -128,8 +131,8 @@ namespace AsbCloudInfrastructure.Services
.OrderBy(o => o.File.UploadDate) .OrderBy(o => o.File.UploadDate)
.AsNoTracking() .AsNoTracking()
.Take(1024); .Take(1024);
var properties = await propertiesQuery.ToListAsync(token); var entities = await propertiesQuery.ToListAsync(token);
return properties.Select(p => new ReportPropertiesDto var dtos = entities.Select(p => new ReportPropertiesDto
{ {
Id = p.Id, Id = p.Id,
Name = p.File.Name, Name = p.File.Name,
@ -151,10 +154,11 @@ namespace AsbCloudInfrastructure.Services
Step = p.Step, Step = p.Step,
Format = p.Format == 0 ? ".pdf" : ".las" Format = p.Format == 0 ? ".pdf" : ".las"
}); });
return dtos;
} }
private static IReportGenerator GetReportGenerator(int idWell, DateTime begin, 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); var dataSource = new ReportDataSourcePgCloud(context, idWell);
IReportGenerator generator = format switch IReportGenerator generator = format switch
@ -173,4 +177,5 @@ namespace AsbCloudInfrastructure.Services
return generator; return generator;
} }
} }
#nullable disable
} }

View File

@ -41,6 +41,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
public TelemetryTracker(IConfiguration configuration, IMemoryCache memoryCache) public TelemetryTracker(IConfiguration configuration, IMemoryCache memoryCache)
{ {
// TODO: make this background work
var contextOptions = new DbContextOptionsBuilder<AsbCloudDbContext>() var contextOptions = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(configuration.GetConnectionString("DefaultConnection")) .UseNpgsql(configuration.GetConnectionString("DefaultConnection"))
.Options; .Options;

View File

@ -1,9 +1,9 @@
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudDb.Model.Subsystems; using AsbCloudDb.Model.Subsystems;
using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Services.Subsystems.Utils; using AsbCloudInfrastructure.Services.Subsystems.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
@ -16,56 +16,30 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.Subsystems namespace AsbCloudInfrastructure.Services.Subsystems
{ {
#nullable enable #nullable enable
internal class SubsystemOperationTimeBackgroundService : BackgroundService internal static class SubsystemOperationTimeCalcWorkFactory
{ {
private readonly string connectionString; private const string workId = "Subsystem operation time calc";
private readonly TimeSpan period = TimeSpan.FromHours(1); private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
private const int idSubsytemTorqueMaster = 65537; private const int idSubsytemTorqueMaster = 65537;
private const int idSubsytemSpinMaster = 65536; private const int idSubsytemSpinMaster = 65536;
private const int idSubsytemAkb = 1; private const int idSubsytemAkb = 1;
private const int idSubsytemMse = 2; private const int idSubsytemMse = 2;
public SubsystemOperationTimeBackgroundService(IConfiguration configuration) public static WorkPeriodic MakeWork()
{ {
connectionString = configuration.GetConnectionString("DefaultConnection"); var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod)
{
Timeout = TimeSpan.FromMinutes(30)
};
return workPeriodic;
} }
protected override async Task ExecuteAsync(CancellationToken token) // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД.
private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token)
{ {
var timeToStart = DateTime.Now; using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
while (!token.IsCancellationRequested)
{
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);
}
}
public override async Task StopAsync(CancellationToken token)
{
await base.StopAsync(token).ConfigureAwait(false);
}
private static async Task<int> OperationTimeAllTelemetriesAsync(IAsbCloudDbContext db, CancellationToken token)
{
var lastDetectedDates = await db.SubsystemOperationTimes var lastDetectedDates = await db.SubsystemOperationTimes
.GroupBy(o => o.IdTelemetry) .GroupBy(o => o.IdTelemetry)
.Select(g => new .Select(g => new
@ -90,23 +64,21 @@ namespace AsbCloudInfrastructure.Services.Subsystems
inner.SingleOrDefault()?.LastDate, inner.SingleOrDefault()?.LastDate,
}); });
var affected = 0;
foreach (var item in telemetryLastDetectedDates) foreach (var item in telemetryLastDetectedDates)
{ {
var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token); var newOperationsSaub = await OperationTimeSaubAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token);
if (newOperationsSaub?.Any() == true) if (newOperationsSaub?.Any() == true)
{ {
db.SubsystemOperationTimes.AddRange(newOperationsSaub); 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); var newOperationsSpin = await OperationTimeSpinAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token);
if (newOperationsSpin?.Any() == true) if (newOperationsSpin?.Any() == true)
{ {
db.SubsystemOperationTimes.AddRange(newOperationsSpin); db.SubsystemOperationTimes.AddRange(newOperationsSpin);
affected += await db.SaveChangesAsync(token); await db.SaveChangesAsync(token);
} }
} }
return affected;
} }
private static async Task<DbDataReader> ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token) private static async Task<DbDataReader> ExecuteReaderAsync(IAsbCloudDbContext db, string query, CancellationToken token)

View File

@ -1,26 +1,72 @@
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.DetectOperations;
using AsbCloudInfrastructure.Services.Subsystems;
using AsbCloudInfrastructure.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using System; using System;
using System.Linq; using System.Threading.Tasks;
using System.Threading;
using AsbCloudInfrastructure.Background;
namespace AsbCloudInfrastructure namespace AsbCloudInfrastructure
{ {
public class Startup public class Startup
{ {
public static void BeforeRunHandler(IHost host, IConfigurationRoot configuration) public static void BeforeRunHandler(IHost host)
{ {
using var scope = host.Services.CreateScope(); using var scope = host.Services.CreateScope();
var context = scope.ServiceProvider.GetService<IAsbCloudDbContext>(); var provider = scope.ServiceProvider;
context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60));
var context = provider.GetService<IAsbCloudDbContext>();
context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60));
context.Database.Migrate(); context.Database.Migrate();
var wellService = scope.ServiceProvider.GetService<IWellService>(); var wellService = provider.GetRequiredService<IWellService>();
wellService.EnshureTimezonesIsSetAsync(System.Threading.CancellationToken.None).Wait(); wellService.EnshureTimezonesIsSetAsync(CancellationToken.None).Wait();// TODO: make this background work
var backgroundWorker = provider.GetRequiredService<Background.BackgroundWorker>();
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("### ### ###");
} }
} }
} }

View File

@ -0,0 +1,244 @@
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System;
using AsbCloudInfrastructure.Background;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace AsbCloudWebApi.Tests.ServicesTests
{
public class BackgroundWorkerTest
{
private readonly Mock<IServiceProvider> mockServiceProvider;
private readonly Mock<IServiceScopeFactory> mockServiceScopeFactory;
private readonly Func<string, IServiceProvider, CancellationToken, Task> someAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask;
public BackgroundWorkerTest()
{
var mockServiceScope = new Mock<IServiceScope>();
mockServiceScopeFactory = new Mock<IServiceScopeFactory>();
mockServiceProvider = new Mock<IServiceProvider>();
mockServiceScope.SetReturnsDefault(mockServiceProvider.Object);
mockServiceProvider.SetReturnsDefault(mockServiceScopeFactory.Object);
mockServiceProvider.Setup(s => s.GetService(It.IsAny<Type>()))
.Returns(mockServiceScopeFactory.Object);
mockServiceScopeFactory.SetReturnsDefault(mockServiceScope.Object);
}
[Fact]
public void Contains_returns_true()
{
mockServiceScopeFactory.Invocations.Clear();
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);
BackgroundWorker.Push(work1);
BackgroundWorker.Push(work2);
Assert.True(BackgroundWorker.Contains(work1Id));
Assert.True(BackgroundWorker.Contains(work2Id));
Assert.False(BackgroundWorker.Contains(work2Id + work1Id));
Assert.False(BackgroundWorker.Contains(string.Empty));
}
[Fact]
public async Task Push_makes_new_scope_after_start()
{
mockServiceScopeFactory.Invocations.Clear();
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var work = new WorkBase("", someAction);
BackgroundWorker.Push(work);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(10);
mockServiceScopeFactory.Verify(f => f.CreateScope());
}
[Fact]
public async Task Makes_primary_work_done()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var workDone = false;
var work = new WorkBase("", (_, _, _) =>
{
workDone = true;
return Task.CompletedTask;
});
BackgroundWorker.Push(work);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(10);
Assert.True(workDone);
}
[Fact]
public async Task Sets_ExecutionTime_after_work_done()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var work = new WorkBase("", someAction);
BackgroundWorker.Push(work);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(10);
Assert.True(work.ExecutionTime > TimeSpan.Zero);
}
[Fact]
public async Task Makes_periodic_work_done()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var workDone = false;
var work = new WorkPeriodic("", (_, _, _) =>
{
workDone = true;
return Task.CompletedTask;
},
TimeSpan.FromMilliseconds(10));
BackgroundWorker.Push(work);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(20);
Assert.True(workDone);
}
[Fact]
public async Task Does_not_start_periodic_work()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var workDone = false;
var work = new WorkPeriodic("", (_, _, _) =>
{
workDone = true;
return Task.CompletedTask;
},
TimeSpan.FromSeconds(30))
{
LastStart = DateTime.Now
};
BackgroundWorker.Push(work);
await BackgroundWorker.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 BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
BackgroundWorker.Push(work2);
BackgroundWorker.Push(work1);
await BackgroundWorker.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 BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
BackgroundWorker.Push(work1);
BackgroundWorker.Push(work2);
BackgroundWorker.Delete("1");
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(10);
Assert.True(workDone);
}
[Fact]
public async Task Aborts_long_work()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var workCanceled = false;
var work = new WorkBase("", async (_, _, token) => await Task.Delay(1000000, token))
{
Timeout = TimeSpan.FromMilliseconds(1),
OnErrorAsync = async (id, ex, token) =>
{
workCanceled = ex is System.TimeoutException;
await Task.CompletedTask;
}
};
BackgroundWorker.Push(work);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(20 * 4);
Assert.True(workCanceled);
}
[Fact]
public async Task Execution_continues_after_work_exception()
{
var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
var work2done = false;
var work1 = new WorkBase("1", (_, _, _) => throw new Exception());
var work2 = new WorkBase("2", (_, _, _) =>
{
work2done = true;
return Task.CompletedTask;
});
BackgroundWorker.Push(work1);
BackgroundWorker.Push(work2);
await BackgroundWorker.StartAsync(CancellationToken.None);
await Task.Delay(2_100);
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 BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
BackgroundWorker.Push(work1);
Assert.Throws<ArgumentException>(
() => BackgroundWorker.Push(work2));
}
}
}

View File

@ -2,7 +2,7 @@
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository; using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Services.DrillingProgram; using AsbCloudInfrastructure.Services.DrillingProgram;
using Mapster; using Mapster;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -83,8 +83,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests
private readonly Mock<IUserRepository> userRepositoryMock; private readonly Mock<IUserRepository> userRepositoryMock;
private readonly Mock<IWellService> wellServiceMock; private readonly Mock<IWellService> wellServiceMock;
private readonly Mock<IConfiguration> configurationMock; private readonly Mock<IConfiguration> configurationMock;
private readonly Mock<IBackgroundWorkerService> backgroundWorkerMock; private readonly Mock<BackgroundWorker> backgroundWorkerMock;
private readonly Mock<IEmailService> emailService; private readonly Mock<IEmailService> emailServiceMock;
public DrillingProgramServiceTest() public DrillingProgramServiceTest()
{ {
@ -102,7 +102,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests
userRepositoryMock = new Mock<IUserRepository>(); userRepositoryMock = new Mock<IUserRepository>();
wellServiceMock = new Mock<IWellService>(); wellServiceMock = new Mock<IWellService>();
configurationMock = new Mock<IConfiguration>(); configurationMock = new Mock<IConfiguration>();
backgroundWorkerMock = new Mock<IBackgroundWorkerService>(); backgroundWorkerMock = new Mock<BackgroundWorker>();
emailServiceMock = new Mock<IEmailService>();
} }
[Fact] [Fact]
@ -115,7 +116,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var users = await service.GetAvailableUsers(idWell, CancellationToken.None); var users = await service.GetAvailableUsers(idWell, CancellationToken.None);
@ -132,7 +133,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var result = await service.AddPartsAsync(idWell, new int[] { 1001, 1002 }, CancellationToken.None); var result = await service.AddPartsAsync(idWell, new int[] { 1001, 1002 }, CancellationToken.None);
@ -151,7 +152,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var result = await service.RemovePartsAsync(idWell, new int[] { 1005 }, CancellationToken.None); var result = await service.RemovePartsAsync(idWell, new int[] { 1005 }, CancellationToken.None);
@ -174,7 +175,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var result = await service.AddUserAsync(idWell, 1001, publisher1.Id, 1, CancellationToken.None); var result = await service.AddUserAsync(idWell, 1001, publisher1.Id, 1, CancellationToken.None);
@ -209,7 +210,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var result = await service.RemoveUserAsync(idWell, idFileCategory, publisher1.Id, idUserRole, CancellationToken.None); var result = await service.RemoveUserAsync(idWell, idFileCategory, publisher1.Id, idUserRole, CancellationToken.None);
@ -235,7 +236,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var fileMark = new FileMarkDto var fileMark = new FileMarkDto
{ {
@ -266,7 +267,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var fileMark = new FileMarkDto var fileMark = new FileMarkDto
{ {
IdFile = file1001.Id, IdFile = file1001.Id,
@ -304,7 +305,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var fileMark = new FileMarkDto var fileMark = new FileMarkDto
{ {
@ -331,7 +332,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
@ -358,12 +359,12 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
Assert.Equal(2, state.IdState); Assert.Equal(2, state.IdState);
backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny<Func<string, CancellationToken, Task>>())); backgroundWorkerMock.Verify(s => s.Push(It.IsAny<WorkBase>()));
} }
[Fact] [Fact]
@ -388,7 +389,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object, wellServiceMock.Object,
configurationMock.Object, configurationMock.Object,
backgroundWorkerMock.Object, backgroundWorkerMock.Object,
emailService.Object); emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);

View File

@ -68,7 +68,7 @@ namespace AsbCloudWebApi.Controllers
).ConfigureAwait(false); ).ConfigureAwait(false);
}, token); }, token);
var id = reportService.CreateReport(idWell, (int)idUser, var id = reportService.EnqueueCreateReportWork(idWell, (int)idUser,
stepSeconds, format, begin, end, HandleReportProgressAsync); stepSeconds, format, begin, end, HandleReportProgressAsync);
return Ok(id); return Ok(id);

View File

@ -43,6 +43,7 @@ namespace AsbCloudWebApi.Middlewares
sw.Stop(); sw.Stop();
requestLog.ElapsedMilliseconds = sw.ElapsedMilliseconds; requestLog.ElapsedMilliseconds = sw.ElapsedMilliseconds;
requestLog.Status = context.Response.StatusCode; requestLog.Status = context.Response.StatusCode;
// TODO: Add request params and body size.
service.RegisterRequestError(requestLog, ex); service.RegisterRequestError(requestLog, ex);
throw; throw;
} }

View File

@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Middlewares
{ {
Console.WriteLine(ex.Message); 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.")) 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."); Console.WriteLine("Reading the request body timed out due to data arriving too slowly.");

View File

@ -1,10 +1,5 @@
using DocumentFormat.OpenXml.InkML;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
namespace AsbCloudWebApi namespace AsbCloudWebApi
{ {
@ -15,30 +10,8 @@ namespace AsbCloudWebApi
public static void Main(string[] args) 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(); var host = CreateHostBuilder(args).Build();
AsbCloudInfrastructure.Startup.BeforeRunHandler(host, configuration); AsbCloudInfrastructure.Startup.BeforeRunHandler(host);
host.Run(); host.Run();
} }
@ -48,17 +21,5 @@ namespace AsbCloudWebApi
{ {
webBuilder.UseStartup<Startup>(); webBuilder.UseStartup<Startup>();
}); });
private static void WriteHelp()
{
Console.WriteLine("Ïðè çàïóñêå áåç êëþ÷åé ïðîãðàììà ïðîñòî ñòàðòóåò â îáû÷íîì ðåæèìå.");
Console.WriteLine("Êëþ÷è äëÿ çàïóñêà:");
Console.WriteLine("db_init - ñîçäàòü êîíòåêñò ÁÄ è âûéòè.");
Console.WriteLine("Êîíòåêñò ñîçäàñòñÿ äëÿ ñòðîêè ïîäêëþ÷åíèÿ \"DefaultConnection\"");
Console.WriteLine("Ñîçäàíèå êîíòåêñòà ïðèâåäåò ê ñîçäàíèþ ÁÄ, åñëè åé íåò");
Console.WriteLine("è ïðèìåíåíèþ âñåõ ìèãðàöèé, åñëè ÁÄ óæå åñòü.");
Console.WriteLine("Äëÿ ñîçäàíèÿ êîíòåêñòà â ÁÄ äîëæíà áûòü ñîçäàíà ñõåìà public");
Console.WriteLine("");
}
} }
} }