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="token"></param>
/// <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)
{
//save info to db
@ -93,7 +93,7 @@ namespace AsbCloudApp.Services
string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, fileFullName, fileId);
await fileStorageRepository.SaveFileAsync(filePath, fileStream, token);
return await GetOrDefaultAsync(fileId, token);
return (await GetOrDefaultAsync(fileId, token))!;
}
/// <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
{
#nullable enable
/// <summary>
/// Сервис рапортов
/// </summary>
@ -16,7 +17,6 @@ namespace AsbCloudApp.Services
/// </summary>
int ReportCategoryId { get; }
// TODO: rename this method
/// <summary>
/// Поставить рапорт в очередь на формирование
/// </summary>
@ -28,7 +28,7 @@ namespace AsbCloudApp.Services
/// <param name="end"></param>
/// <param name="handleReportProgress"></param>
/// <returns></returns>
string CreateReport(int idWell, int idUser, int stepSeconds,
string EnqueueCreateReportWork(int idWell, int idUser, int stepSeconds,
int format, DateTime begin, DateTime end,
Action<object, string> handleReportProgress);
@ -49,7 +49,7 @@ namespace AsbCloudApp.Services
/// </summary>
/// <param name="idWell"></param>
/// <returns></returns>
DatesRangeDto GetDatesRangeOrDefault(int idWell);
DatesRangeDto? GetDatesRangeOrDefault(int idWell);
/// <summary>
/// Список готовых рапортов
@ -58,5 +58,7 @@ namespace AsbCloudApp.Services
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ReportPropertiesDto>> GetAllReportsByWellAsync(int idWell, CancellationToken token);
#nullable disable
}
}

View File

@ -60,20 +60,32 @@ namespace AsbCloudDb.Model
public DbSet<WITS.Record50> Record50 => Set<WITS.Record50>();
public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>();
public DbSet<WITS.Record61> Record61 => Set<WITS.Record61>();
public int ReferenceCount { get; private set; }
public AsbCloudDbContext() : base()
{
ReferenceCount++;
}
public AsbCloudDbContext(DbContextOptions<AsbCloudDbContext> options)
: base(options)
{
ReferenceCount++;
}
~AsbCloudDbContext()
{
ReferenceCount--;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
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)

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 AsbCloudDb.Model;
using AsbCloudDb.Model.Subsystems;
using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Repository;
using AsbCloudInfrastructure.Services;
using AsbCloudInfrastructure.Services.DailyReport;
@ -97,16 +98,13 @@ namespace AsbCloudInfrastructure
services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>());
services.AddScoped<IEmailService, EmailService>();
services.AddHostedService<OperationDetectionBackgroundService>();
services.AddHostedService<SubsystemOperationTimeBackgroundService>();
services.AddHostedService<LimitingParameterBackgroundService>();
services.AddSingleton(new WitsInfoService());
services.AddSingleton(new InstantDataRepository());
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(configuration));
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(configuration));
services.AddSingleton<ITelemetryTracker, TelemetryTracker>();
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>();
services.AddSingleton<BackgroundWorker>();
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
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
{
private readonly AsbCloudDbContext context;
private readonly IAsbCloudDbContext context;
private readonly int? idTelemetry;
private readonly WellInfoReport info;
@ -25,7 +25,7 @@ namespace AsbCloudInfrastructure
{3, "Информация"},
};
public ReportDataSourcePgCloud(AsbCloudDbContext context, int idWell)
public ReportDataSourcePgCloud(IAsbCloudDbContext context, int idWell)
{
this.context = context;
@ -65,6 +65,7 @@ namespace AsbCloudInfrastructure
public AnalyzeResult Analyze()
{
// TODO: Replace by linq methods.
var messagesStat = (from item in context.TelemetryMessages
where item.IdTelemetry == idTelemetry
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;
}
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>();
dto.IdWell = well.Id;

View File

@ -1,7 +1,5 @@
using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -9,14 +7,16 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudInfrastructure.Services.DetectOperations.Detectors;
using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services.DetectOperations
{
#nullable enable
public class OperationDetectionBackgroundService : BackgroundService
public static class OperationDetectionWorkFactory
{
private readonly string connectionString;
private readonly TimeSpan period = TimeSpan.FromHours(1);
private const string workId = "Operation detection";
private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
private static readonly DetectorAbstract[] detectors = new DetectorAbstract[]
{
@ -31,49 +31,18 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
new DetectorTemplatingWhileDrilling(),
};
public OperationDetectionBackgroundService(IConfiguration configuration)
{
connectionString = configuration.GetConnectionString("DefaultConnection");
public static WorkPeriodic MakeWork()
{
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;
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
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
.GroupBy(o => o.IdTelemetry)
.Select(g => new
@ -88,7 +57,7 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
.Select(t => t.Id)
.ToListAsync(token);
var JounedlastDetectedDates = telemetryIds
var joinedlastDetectedDates = telemetryIds
.GroupJoin(lastDetectedDates,
t => t,
o => o.IdTelemetry,
@ -97,8 +66,9 @@ namespace AsbCloudInfrastructure.Services.DetectOperations
IdTelemetry = outer,
inner.SingleOrDefault()?.LastDate,
});
var affected = 0;
foreach (var item in JounedlastDetectedDates)
foreach (var item in joinedlastDetectedDates)
{
var stopwatch = Stopwatch.StartNew();
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);
}
}
return affected;
}
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.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository;
using AsbCloudInfrastructure.Background;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
@ -16,6 +17,7 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.DrillingProgram
{
# nullable enable
public class DrillingProgramService : IDrillingProgramService
{
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 IWellService wellService;
private readonly IConfiguration configuration;
private readonly IBackgroundWorkerService backgroundWorker;
private readonly BackgroundWorker backgroundWorker;
private readonly IEmailService emailService;
private readonly string connectionString;
private const int idFileCategoryDrillingProgram = 1000;
private const int idFileCategoryDrillingProgramPartsStart = 1001;
@ -55,7 +56,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
IUserRepository userRepository,
IWellService wellService,
IConfiguration configuration,
IBackgroundWorkerService backgroundWorker,
BackgroundWorker backgroundWorker,
IEmailService emailService)
{
this.context = context;
@ -64,7 +65,6 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
this.wellService = wellService;
this.configuration = configuration;
this.backgroundWorker = backgroundWorker;
this.connectionString = configuration.GetConnectionString("DefaultConnection");
this.emailService = emailService;
}
@ -127,7 +127,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
{
Parts = parts,
Program = files.FirstOrDefault(f => f.IdCategory == idFileCategoryDrillingProgram)
.Adapt<FileInfoDto>(),
?.Adapt<FileInfoDto>(),
PermissionToEdit = userRepository.HasPermission(idUser, "DrillingProgram.edit"),
};
@ -157,7 +157,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
else
state.IdState = idStateNotInitialized;
await TryEnqueueMakeProgramAsync(idWell, state, token);
await EnqueueMakeProgramWorkAsync(idWell, state, token);
return state;
}
@ -299,7 +299,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
.AsNoTracking()
.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)
throw new ForbidException($"User {idUser} is not in the approvers list.");
@ -323,11 +323,11 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
else
{
// если все согласованты согласовали - оповещаем публикатора
var approvers = part.RelatedUsers
var approvers = part!.RelatedUsers
.Where(u => u.IdUserRole == idUserRoleApprover);
if (approvers
.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)))
{
await NotifyPublisherOnFullAccepAsync(fileMarkDto, token);
@ -359,7 +359,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken 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 factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован");
@ -371,7 +371,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken 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 factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен");
@ -405,12 +405,12 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
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
{
IdFileCategory = partEntity.IdFileCategory,
Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory).Name,
Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory)!.Name,
Approvers = partEntity.RelatedUsers
.Where(r => r.IdUserRole == idUserRoleApprover)
.Select(r => r.User.Adapt<UserDto>()),
@ -464,31 +464,27 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
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)
{
var workId = MakeWorkId(idWell);
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 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>()
.UseNpgsql(connectionString)
.Options;
using var context = new AsbCloudDbContext(contextOptions);
var fileRepository = new FileRepository(context);
var fileStorageRepository = new FileStorageRepository();
var fileService = new FileService(fileRepository, fileStorageRepository);
var context = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var fileService = serviceProvider.GetRequiredService<FileService>();
var files = state.Parts.Select(p => fileService.GetUrl(p.File));
DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath, state.Parts, well);
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}";
drillingProgramCreateErrors[workId] = new()
@ -497,9 +493,15 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
Exception = exception.Message,
};
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)
{
var workId = MakeWorkId(idWell);
backgroundWorker.TryRemove(workId);
backgroundWorker.Delete(workId);
var filesIds = await context.Files
.Where(f => f.IdWell == idWell &&
@ -529,4 +531,5 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
private static string MakeWorkId(int idWell)
=> $"Make drilling program for wellId {idWell}";
}
#nullable disable
}

View File

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

View File

@ -1,61 +1,37 @@
using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Data.Common;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services
{
#nullable enable
internal class LimitingParameterBackgroundService : BackgroundService
internal static class LimitingParameterCalcWorkFactory
{
private readonly string connectionString;
private readonly TimeSpan period = TimeSpan.FromHours(1);
private const string workId = "Limiting parameter calc";
private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
public LimitingParameterBackgroundService(IConfiguration configuration)
public static WorkPeriodic MakeWork()
{
connectionString = configuration.GetConnectionString("DefaultConnection");
}
protected override async Task ExecuteAsync(CancellationToken token)
{
var timeToStart = DateTime.Now;
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
while (!token.IsCancellationRequested)
var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod)
{
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);
}
Timeout = TimeSpan.FromMinutes(30)
};
return workPeriodic;
}
private static async Task<int> LimitingParameterAsync(IAsbCloudDbContext context, CancellationToken token)
// TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД.
private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token)
{
var lastDetectedDates = await context.LimitingParameter
using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var lastDetectedDates = await db.LimitingParameter
.GroupBy(o => o.IdTelemetry)
.Select(g => new
{
@ -64,7 +40,7 @@ namespace AsbCloudInfrastructure.Services
})
.ToListAsync(token);
var telemetryIds = await context.Telemetries
var telemetryIds = await db.Telemetries
.Where(t => t.Info != null && t.TimeZone != null)
.Select(t => t.Id)
.ToListAsync(token);
@ -79,17 +55,15 @@ namespace AsbCloudInfrastructure.Services
inner.SingleOrDefault()?.LastDate,
});
var affected = 0;
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)
{
context.LimitingParameter.AddRange(newLimitingParameters);
affected += await context.SaveChangesAsync(token);
db.LimitingParameter.AddRange(newLimitingParameters);
await db.SaveChangesAsync(token);
}
}
return affected;
}
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.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository;
using AsbCloudInfrastructure.Background;
using AsbSaubReport;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
@ -15,30 +15,32 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services
{
#nullable enable
public class ReportService : IReportService
{
private readonly IAsbCloudDbContext db;
private readonly string connectionString;
private readonly ITelemetryService telemetryService;
private readonly IWellService wellService;
private readonly IBackgroundWorkerService 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;
}
private readonly BackgroundWorker backgroundWorkerService;
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)
{
var timezoneOffset = wellService.GetTimezone(idWell).Hours;
@ -47,12 +49,12 @@ namespace AsbCloudInfrastructure.Services
var beginRemote = begin.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>()
.UseNpgsql(connectionString)
.Options;
using var context = new AsbCloudDbContext(contextOptions);
using var context = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var fileService = serviceProvider.GetRequiredService<FileService>();
var tempDir = Path.Combine(Path.GetTempPath(), "report");
@ -65,11 +67,8 @@ namespace AsbCloudInfrastructure.Services
progressHandler.Invoke(e.Adapt<ReportProgressDto>(), id);
};
generator.Make(reportFileName);
var fileRepository = new FileRepository(context);
var fileStorageRepository = new FileStorageRepository();
var fileService = new FileService(fileRepository, fileStorageRepository);
var fileInfo = await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token);
var fileInfo = (await fileService.MoveAsync(idWell, idUser, ReportCategoryId, reportFileName, reportFileName, token))!;
progressHandler.Invoke(new
{
@ -91,13 +90,17 @@ namespace AsbCloudInfrastructure.Services
};
context.ReportProperties.Add(newReportProperties);
context.SaveChanges();
});
};
var work = new WorkBase(workId, workAction);
backgroundWorkerService.Push(work);
progressHandler.Invoke(new ReportProgressDto
{
Operation = "Ожидает начала в очереди.",
Progress = 0f,
}, newReportId);
return newReportId;
}, workId);
return workId;
}
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 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();
return pagesCount;
}
public DatesRangeDto GetDatesRangeOrDefault(int idWell)
public DatesRangeDto? GetDatesRangeOrDefault(int idWell)
{
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
@ -128,8 +131,8 @@ namespace AsbCloudInfrastructure.Services
.OrderBy(o => o.File.UploadDate)
.AsNoTracking()
.Take(1024);
var properties = await propertiesQuery.ToListAsync(token);
return properties.Select(p => new ReportPropertiesDto
var entities = await propertiesQuery.ToListAsync(token);
var dtos = entities.Select(p => new ReportPropertiesDto
{
Id = p.Id,
Name = p.File.Name,
@ -151,10 +154,11 @@ namespace AsbCloudInfrastructure.Services
Step = p.Step,
Format = p.Format == 0 ? ".pdf" : ".las"
});
return dtos;
}
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);
IReportGenerator generator = format switch
@ -173,4 +177,5 @@ namespace AsbCloudInfrastructure.Services
return generator;
}
}
#nullable disable
}

View File

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

View File

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

View File

@ -1,26 +1,72 @@
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.DetectOperations;
using AsbCloudInfrastructure.Services.Subsystems;
using AsbCloudInfrastructure.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using AsbCloudInfrastructure.Background;
namespace AsbCloudInfrastructure
{
public class Startup
{
public static void BeforeRunHandler(IHost host, IConfigurationRoot configuration)
public static void BeforeRunHandler(IHost host)
{
using var scope = host.Services.CreateScope();
var context = scope.ServiceProvider.GetService<IAsbCloudDbContext>();
context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60));
var provider = scope.ServiceProvider;
var context = provider.GetService<IAsbCloudDbContext>();
context.Database.SetCommandTimeout(TimeSpan.FromSeconds(2 * 60));
context.Database.Migrate();
var wellService = scope.ServiceProvider.GetService<IWellService>();
wellService.EnshureTimezonesIsSetAsync(System.Threading.CancellationToken.None).Wait();
var wellService = provider.GetRequiredService<IWellService>();
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.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository;
using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Services.DrillingProgram;
using Mapster;
using Microsoft.Extensions.Configuration;
@ -83,8 +83,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests
private readonly Mock<IUserRepository> userRepositoryMock;
private readonly Mock<IWellService> wellServiceMock;
private readonly Mock<IConfiguration> configurationMock;
private readonly Mock<IBackgroundWorkerService> backgroundWorkerMock;
private readonly Mock<IEmailService> emailService;
private readonly Mock<BackgroundWorker> backgroundWorkerMock;
private readonly Mock<IEmailService> emailServiceMock;
public DrillingProgramServiceTest()
{
@ -102,7 +102,8 @@ namespace AsbCloudWebApi.Tests.ServicesTests
userRepositoryMock = new Mock<IUserRepository>();
wellServiceMock = new Mock<IWellService>();
configurationMock = new Mock<IConfiguration>();
backgroundWorkerMock = new Mock<IBackgroundWorkerService>();
backgroundWorkerMock = new Mock<BackgroundWorker>();
emailServiceMock = new Mock<IEmailService>();
}
[Fact]
@ -115,7 +116,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var users = await service.GetAvailableUsers(idWell, CancellationToken.None);
@ -132,7 +133,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var result = await service.AddPartsAsync(idWell, new int[] { 1001, 1002 }, CancellationToken.None);
@ -151,7 +152,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var result = await service.RemovePartsAsync(idWell, new int[] { 1005 }, CancellationToken.None);
@ -174,7 +175,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var result = await service.AddUserAsync(idWell, 1001, publisher1.Id, 1, CancellationToken.None);
@ -209,7 +210,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var result = await service.RemoveUserAsync(idWell, idFileCategory, publisher1.Id, idUserRole, CancellationToken.None);
@ -235,7 +236,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var fileMark = new FileMarkDto
{
@ -266,7 +267,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var fileMark = new FileMarkDto
{
IdFile = file1001.Id,
@ -304,7 +305,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var fileMark = new FileMarkDto
{
@ -331,7 +332,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
@ -358,12 +359,12 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
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]
@ -388,7 +389,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
wellServiceMock.Object,
configurationMock.Object,
backgroundWorkerMock.Object,
emailService.Object);
emailServiceMock.Object);
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);

View File

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

View File

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

View File

@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Middlewares
{
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."))
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.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
namespace AsbCloudWebApi
{
@ -15,30 +10,8 @@ namespace AsbCloudWebApi
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();
AsbCloudInfrastructure.Startup.BeforeRunHandler(host, configuration);
AsbCloudInfrastructure.Startup.BeforeRunHandler(host);
host.Run();
}
@ -48,17 +21,5 @@ namespace AsbCloudWebApi
{
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("");
}
}
}