Add ReduceSamplingController.

This commit is contained in:
ngfrolov 2022-10-11 17:04:26 +05:00
parent 8cbcd9a115
commit 707f2a638a
6 changed files with 282 additions and 80 deletions

View File

@ -0,0 +1,56 @@
using System;
using System.Collections;
namespace AsbCloudApp.Data
{
#nullable enable
/// <summary>
/// Состояние фоновой задачи
/// </summary>
public enum JobState
{
/// <summary>
/// Ожидает в очереди на выполнение
/// </summary>
Waiting,
/// <summary>
/// выполняется
/// </summary>
Working,
/// <summary>
/// успешно выполнена
/// </summary>
Done,
/// <summary>
/// завершена с ошибкой
/// </summary>
Fail
};
/// <summary>
/// работа фоновой задачи
/// </summary>
public class JobDto
{
/// <summary>
/// идентификатор
/// </summary>
public int Id { get; set; }
/// <summary>
/// Состояние
/// </summary>
public JobState State { get; set; }
/// <summary>
/// результат выполнения
/// </summary>
public Hashtable? Results { get; set; }
/// <summary>
/// Исключение, если возникла ошибка
/// </summary>
public string? Error { get; set; }
}
#nullable disable
}

View File

@ -0,0 +1,43 @@
using AsbCloudApp.Data;
using System.Collections.Generic;
namespace AsbCloudApp.Services
{
#nullable enable
/// <summary>
/// Делегат обновления состояния задачи
/// </summary>
/// <param name="job"></param>
public delegate void OnJobProgressDelagate(JobDto job);
/// <summary>
/// Сервис прореживания архива БД.
/// Удаляет часть телеметрии.
/// Понижает частоту записей в БД с 1 запись за 1 сек до 1 запись за N сек.
/// </summary>
public interface IReduceSamplingService
{
/// <summary>
/// Получить все задания. Задания удаляются минимум через 10 сек после выполнения, возможно позднее.
/// </summary>
/// <returns>Enumerable of JobDto or empty</returns>
IEnumerable<JobDto> GetJobs();
/// <summary>
/// Получить состояние определенной задачи
/// </summary>
/// <param name="idTelemetry"></param>
/// <returns></returns>
JobDto? GetOrDefaultState(int idTelemetry);
/// <summary>
/// Создать задачу прореживанию архива и добавить её в очередь на выполнение
/// </summary>
/// <param name="idTelemetry">телеметрия для прореживания</param>
/// <param name="onProgress">колбек процесса выполнения</param>
/// <param name="jobDto">созданная задача или задача из очереди</param>
/// <returns>задача добавлена == true</returns>
bool TryEnqueueRediceSamplingJob(int idTelemetry, OnJobProgressDelagate onProgress, out JobDto jobDto);
}
#nullable disable
}

View File

@ -102,6 +102,7 @@ namespace AsbCloudInfrastructure
services.AddSingleton<ITelemetryTracker, TelemetryTracker>(); services.AddSingleton<ITelemetryTracker, TelemetryTracker>();
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>(); services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>(); services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>();
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
services.AddTransient<IAuthService, AuthService>(); services.AddTransient<IAuthService, AuthService>();
services.AddTransient<IClusterService, ClusterService>(); services.AddTransient<IClusterService, ClusterService>();

View File

@ -1,9 +1,12 @@
using AsbCloudDb; using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,27 +14,22 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services namespace AsbCloudInfrastructure.Services
{ {
#nullable enable #nullable enable
public class JobDto class JobHandle
{ {
public enum JobState { Waiting, Working, Done, Fail }; public int Id => Job.Id;
public int Id { get; internal set; } public JobDto Job { get; set; } = null!;
public JobState State { get; internal set; } public DateTime DateCreate { get; set; } = DateTime.Now;
public object? Result { get; internal set; } public OnJobProgressDelagate? OnProgress { get; set; }
public Exception? Error { get; internal set; }
} }
class JobWithProgress: JobDto public class ReduceSamplingService : IReduceSamplingService
{ {
public ReduceSamplingService.OnProgressDelagate OnProgress { get; set; } = null!; private const string jobResultKeyDeleted = "deleted";
} private const string jobResultKeyTotal = "total";
public class ReduceSamplingService
{
public delegate void OnProgressDelagate(JobDto job);
private static ReduceSamplingService? instance; private static ReduceSamplingService? instance;
private readonly string connectionString; private readonly string connectionString;
private const int ratio = 5; private const int ratio = 5;
private readonly List<JobWithProgress> jobs = new(5); private readonly List<JobHandle> jobHandlers = new(5);
private bool isHandling; private bool isHandling;
private CancellationTokenSource? cancellationTokenSource; private CancellationTokenSource? cancellationTokenSource;
private Task? task; private Task? task;
@ -46,62 +44,73 @@ namespace AsbCloudInfrastructure.Services
Stop(); Stop();
} }
/// <summary>
/// Get singleton Instance
/// </summary>
/// <param name="configuration"></param>
/// <returns></returns>
public static ReduceSamplingService GetInstance(IConfiguration configuration) public static ReduceSamplingService GetInstance(IConfiguration configuration)
{ {
if (instance is null) instance ??= new(configuration);
instance = new(configuration);
return instance; return instance;
} }
public int TryEnqueueRediceSamplingJob(int idTelemetry, OnProgressDelagate onProgress) /// <inheritdoc/>
public bool TryEnqueueRediceSamplingJob(int idTelemetry, OnJobProgressDelagate? onProgress, out JobDto jobDto)
{ {
var result = 0; lock (jobHandlers)
lock (jobs)
{ {
if (jobs.Any(j => j.Id == idTelemetry)) var oldJob = jobHandlers.FirstOrDefault(j => j.Id == idTelemetry);
result = - 1; if (oldJob is not null)
{
jobDto = oldJob.Job;
return false;
}
var job = new JobWithProgress jobDto = new JobDto
{ {
Id = idTelemetry, Id = idTelemetry,
State = JobDto.JobState.Waiting, State = JobState.Waiting,
Results = new(),
};
var jobHandler = new JobHandle
{
Job = jobDto,
OnProgress = onProgress, OnProgress = onProgress,
}; };
jobs.Add(job); jobHandlers.Add(jobHandler);
result = jobs.Count;
} }
EnsureHandleQueueStarted(); EnsureHandleQueueStarted();
return result; return true;
} }
public JobDto? GetState(int idTelemetry) /// <inheritdoc/>
public JobDto? GetOrDefaultState(int idTelemetry)
{ {
JobWithProgress? job; JobHandle? jobHandler;
lock (jobs) lock (jobHandlers)
{ {
job = jobs.FirstOrDefault(j=>j.Id == idTelemetry); jobHandler = jobHandlers.FirstOrDefault(j => j.Id == idTelemetry);
} }
return job; return jobHandler?.Job;
} }
public IEnumerable<JobDto> GetJobs() /// <inheritdoc/>
{ public IEnumerable<JobDto> GetJobs() => jobHandlers.Select(j=>j.Job);
return jobs;
}
private bool TryTakeWaitingJob(out JobWithProgress? job) private bool TryTakeWaitingJob(out JobHandle? job)
{ {
lock (jobs) lock (jobHandlers)
{ {
job = jobs.FirstOrDefault(j => j.State == JobDto.JobState.Waiting); job = jobHandlers.FirstOrDefault(j => j.Job.State == JobState.Waiting);
} }
return job is not null; return job is not null;
} }
private void EnsureHandleQueueStarted() private void EnsureHandleQueueStarted()
{ {
if(isHandling) if (isHandling)
return; return;
isHandling = true; isHandling = true;
cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource = new CancellationTokenSource();
@ -112,33 +121,43 @@ namespace AsbCloudInfrastructure.Services
private async Task HandleJobs(CancellationToken token) private async Task HandleJobs(CancellationToken token)
{ {
while (TryTakeWaitingJob(out JobWithProgress? job)) while (TryTakeWaitingJob(out JobHandle? jobHandler))
{ {
job!.State = JobDto.JobState.Working; jobHandler!.Job.State = JobState.Working;
try try
{ {
await RediceSamplingSaubAsync(job, token); await RediceSamplingSaubAsync(jobHandler, token);
await RediceSamplingSpinAsync(job, token); await RediceSamplingSpinAsync(jobHandler, token);
job.State = JobDto.JobState.Done; jobHandler.Job.State = JobState.Done;
} }
catch (Exception exception) catch (Exception exception)
{ {
job.State = JobDto.JobState.Fail; jobHandler.Job.State = JobState.Fail;
job.Error = exception; jobHandler.Job.Results = null;
job.OnProgress.Invoke(job); jobHandler.Job.Error = exception.Message;
jobHandler.OnProgress?.Invoke(jobHandler.Job);
}
if (!jobHandlers.Any(j => j.Job.State == JobState.Waiting))
{
var sw = Stopwatch.StartNew();
await VacuumAsync(token);
sw.Stop();
if (sw.ElapsedMilliseconds < 10_000)
{
var delayMs = 10_000 - (int)sw.ElapsedMilliseconds;
await Task.Delay(delayMs, token);
}
CleanJobs();
} }
await Task.Delay(100, token);
} }
await VacuumAsync(token);
await Task.Delay(1_000, token);
CleanJobs();
} }
private void CleanJobs() private void CleanJobs()
{ {
lock (jobs) lock (jobHandlers)
{ {
jobs.RemoveAll(j => j.State == JobDto.JobState.Done || j.State == JobDto.JobState.Fail); jobHandlers.RemoveAll(j => j.Job.State == JobState.Done || j.Job.State == JobState.Fail);
} }
} }
@ -154,17 +173,11 @@ namespace AsbCloudInfrastructure.Services
cancellationTokenSource?.Cancel(); cancellationTokenSource?.Cancel();
task?.Wait(1_000); task?.Wait(1_000);
task = null; task = null;
cancellationTokenSource?.Dispose();
cancellationTokenSource = null; cancellationTokenSource = null;
} }
/// <summary> private Task RediceSamplingSaubAsync(JobHandle job, CancellationToken token)
/// Прореживание данных телеметрии САУБ.
/// Каждая ratio запись будет сохранена, остальные удаляются. Остаются (Row_number % ratio) = 0.
/// </summary>
/// <param name="idTelemetry">Id телеметрии</param>
/// <param name="ratio">желаемое отношение оставшихся записей к исходным</param>
/// <returns></returns>
private Task RediceSamplingSaubAsync(JobWithProgress job, CancellationToken token)
{ {
const int ramLimit = 10 * 1024 * 1024; const int ramLimit = 10 * 1024 * 1024;
const int dataItemSize = 345; // by profiler const int dataItemSize = 345; // by profiler
@ -204,13 +217,13 @@ namespace AsbCloudInfrastructure.Services
return RediceSamplingAsync<TelemetryDataSaub>( return RediceSamplingAsync<TelemetryDataSaub>(
job, job,
chankSize, chankSize,
sqlSelectTemplate, sqlSelectTemplate,
sqlDeleteTemplate, sqlDeleteTemplate,
token); token);
} }
private Task RediceSamplingSpinAsync(JobWithProgress job, CancellationToken token) private Task RediceSamplingSpinAsync(JobHandle job, CancellationToken token)
{ {
const int ramLimit = 10 * 1024 * 1024; const int ramLimit = 10 * 1024 * 1024;
const int dataItemSize = 345; // by profiler const int dataItemSize = 345; // by profiler
@ -251,26 +264,30 @@ namespace AsbCloudInfrastructure.Services
} }
private async Task RediceSamplingAsync<TEntity>( private async Task RediceSamplingAsync<TEntity>(
JobWithProgress job, JobHandle jobHandle,
int chankSize, int chankSize,
string sqlSelectTemplate, string sqlSelectTemplate,
string sqlDeleteTemplate, string sqlDeleteTemplate,
CancellationToken token) CancellationToken token)
where TEntity: class, ITelemetryData where TEntity : class, AsbCloudDb.Model.ITelemetryData
{ {
using var db = MakeContext(); using var db = MakeContext();
var dbset = db.Set<TEntity>(); var dbset = db.Set<TEntity>();
var deleted = 0; var deleted = 0;
var oldCount = await dbset.Where(t => t.IdTelemetry == job.Id).CountAsync(token); var totalCount = await dbset.Where(t => t.IdTelemetry == jobHandle.Id).CountAsync(token);
if (job.Result is Tuple<int, int> tupleResult) var result = jobHandle.Job.Results!;
{ if (result[jobResultKeyDeleted] is int previousDeleted)
deleted += tupleResult.Item1; deleted += previousDeleted;
oldCount += tupleResult.Item2;
} if (result[jobResultKeyTotal] is int previousCount)
job.Result = (deleted, oldCount); totalCount += previousCount;
job.OnProgress?.Invoke(job);
result[jobResultKeyDeleted] = deleted;
result[jobResultKeyTotal] = totalCount;
jobHandle.OnProgress?.Invoke(jobHandle.Job);
var startDate = DateTimeOffset.MinValue; var startDate = DateTimeOffset.MinValue;
do do
@ -297,8 +314,9 @@ namespace AsbCloudInfrastructure.Services
startDate = lastDate; startDate = lastDate;
deleted += currentDeleted; deleted += currentDeleted;
job.Result = (deleted, oldCount); result[jobResultKeyDeleted] = deleted;
job.OnProgress?.Invoke(job); result[jobResultKeyTotal] = totalCount;
jobHandle.OnProgress?.Invoke(jobHandle.Job);
} while (true); } while (true);
return; return;

View File

@ -0,0 +1,78 @@
using AsbCloudApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using System.Linq;
using AsbCloudWebApi.SignalR;
using Microsoft.AspNetCore.SignalR;
namespace AsbCloudWebApi.Controllers
{
/// <summary>
/// Редактор кустов для админки
/// </summary>
[Route("api/admin/[controller]")]
[ApiController]
[Authorize]
public class ReduceSamplingController: ControllerBase
{
private readonly IReduceSamplingService service;
private readonly IHubContext<TelemetryHub> telemetryHubContext;
private const string sirnalRGroupName = "ReduceSampling";
private const string sirnalRMethodOnProgress = "OnProgress";
public ReduceSamplingController(
IReduceSamplingService service,
IHubContext<TelemetryHub> telemetryHubContext )
{
this.service = service;
this.telemetryHubContext = telemetryHubContext;
}
/// <summary>
/// Получить все задания. Задания удаляются минимум через 10 сек после выполнения, возможно позднее.
/// </summary>
/// <param name="idTelemetry"></param>
/// <returns></returns>
[HttpGet]
public virtual ActionResult<IEnumerable<JobDto>> GetAll(int idTelemetry)
{
var result = service.GetJobs();
if (result.Any())
return Ok(result);
else
return NoContent();
}
/// <summary>
/// Получить состояние определенной задачи
/// </summary>
/// <param name="idTelemetry"></param>
/// <returns></returns>
[HttpGet("{idTelemetry}")]
public virtual ActionResult<JobDto> GetOrDefault(int idTelemetry)
{
var result = service.GetOrDefaultState(idTelemetry);
return Ok(result);
}
/// <summary>
/// Создать задачу прореживанию архива и добавить её в очередь на выполнение.
/// Если задача есть в очереди, она же и возвращается, но подписка не происходит.
/// </summary>
[HttpPost]
[Permission]
public virtual ActionResult<JobDto> Enqueue(int idTelemetry)
{
void onProgress(JobDto job) =>
Task.Run(async () =>
await telemetryHubContext.Clients.Group(sirnalRGroupName)
.SendAsync(sirnalRMethodOnProgress, job));
service.TryEnqueueRediceSamplingJob(idTelemetry, onProgress, out JobDto job);
return Ok(job);
}
}
}

View File

@ -8,6 +8,7 @@ using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Org.BouncyCastle.Utilities.Collections; using Org.BouncyCastle.Utilities.Collections;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -24,8 +25,13 @@ namespace ConsoleApp1
// use ServiceFactory to make services // use ServiceFactory to make services
static void Main(/*string[] args*/) static void Main(/*string[] args*/)
{ {
Task.Run(() => { throw new Exception(); }) var h = new Hashtable();
.ContinueWith(t => { Console.WriteLine("Dooom"); }); h.Add("name", 1);
h.Add("name2", "66");
var v = h["v"];
var s = System.Text.Json.JsonSerializer.Serialize(h);
Console.WriteLine($"total time: ms"); Console.WriteLine($"total time: ms");
Console.ReadLine(); Console.ReadLine();
} }