diff --git a/AsbCloudApp/Data/BackgroundWorkDto.cs b/AsbCloudApp/Data/BackgroundWorkDto.cs index 9d4f2105..3374148c 100644 --- a/AsbCloudApp/Data/BackgroundWorkDto.cs +++ b/AsbCloudApp/Data/BackgroundWorkDto.cs @@ -172,7 +172,7 @@ namespace AsbCloudApp.Data if (progress.HasValue) CurrentState.Progress = progress.Value; - Trace.TraceInformation($"{WorkNameForTrace} state: {newState} [{100*progress:#}%]"); + Trace.TraceInformation($"{WorkNameForTrace} state[{100*progress:#}%]: {newState}"); } /// diff --git a/AsbCloudDb/Setup db replication.md b/AsbCloudDb/Setup db replication.md new file mode 100644 index 00000000..f4a529af --- /dev/null +++ b/AsbCloudDb/Setup db replication.md @@ -0,0 +1,265 @@ +# Репликация данных PostgreSQL +## 1. Требования +1. Primary и Replica сервера должны принадлежать одной версии postgreSQL +2. Сервера должны иметь удаленный доступ + + +## 2. Настройка Primary-сервера +1. Открыть postgres.conf на редактирование + +``` +cd /etc/postgresql/15/main/ + +sudo nano postgresql.conf + ``` + + 2. В postgres.conf найти запись listen_addresses и добавить туда ip standby-сервера + + > listen_addresses = '*, ' + + 3. Открыть клиент для работы с postgres + + ``` + sudo -u postgres psql + ``` + + 4. Создать пользователя с атрибутом REPLICATION.
+ P.S: В данном примере создается пользователь с логином replicator и паролем q + + ``` + CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'q'; + ``` + + 5. Открыть на редактирование файл pg_hba.conf + ``` +cd /etc/postgresql/15/main/ + +sudo nano pg_hba.conf + ``` + + 6. Вставить в pg_hba.conf запись. + Запись вставлять после комментария "Allow replication connections from localhost..."
+ Данные для вставки записи:
+ - replicator - имя пользователя, созданного на предыдущем шаге
+- , например, 192.168.0.0/24 + ``` + host replication replicator 192.168.0.0/24 md5 + ``` + + 7. Рестарт сервера + ``` + sudo systemctl restart postgresql + ``` + + ## 3. Настройка replica-сервера + 1. Остановить сервер + ``` + sudo systemctl stop postgresql + ``` + + 2. Важно! Зайти под пользователем postgres + ``` + sudo su - postgres + ``` + + 3. Сделать резервную копию содержимого /var/lib/postgresql/15/main/ в папку main_old + ``` + cp -R /var/lib/postgresql/15/main/ /var/lib/postgresql/15/main_old/ + ``` + + 4. Удалить папку main + ``` + rm -rf /var/lib/postgresql/15/main/ + ``` + + 5. Используя утилиту basebackup создать базовую резервную копию с правами владения postgres (либо любого пользователя с соответствующими разрешениями). + + ``` + pg_basebackup -h -D /var/lib/postgresql/14/main/ -U replicator -P -v -R -X stream -C -S slaveslot1 + + где: /var/lib/postgresql/15/main/ - каталог replica-сервера + ``` + + 6. Убедиться, что в папке main созданы файлы standby.signal и postgresql.auto.conf. +``` +ls -ltrh /var/lib/postgresql/15/main/ +``` + +7. Запустить сервер +``` +systemctl start postgresql +``` + + ## 4. Проверка настроек + 1. Подсоединиться к primary-серверу + ``` +sudo -u postgres psql + ``` + + 2. На primary-сервере выполнить команду + ``` + SELECT * FROM pg_replication_slots; + ``` + + 3. Убедиться, что в представлении отображается слот репликации с именем slotslave1 + 4. На standby-сервере выпонить команду + ``` + SELECT * FROM pg_stat_wal_receiver; + ``` + 5. Убедиться, что появилась запись с ip primary-сервера + + 6. На primary - сервере проверить режим репликации. Он может быть синхронным или асинхронным. Для проверки необходимо выполнить команду + ``` + SELECT * FROM pg_stat_replication; + ``` + + 7. Для включения синхронного режима необходимо выполнить следующую команду + ``` + ALTER SYSTEM SET synchronous_standby_names TO '*'; + ``` + + 8. Сделать рестарт primary-сервера. + + 9. Внести запись в любую таблицу базы данных primary-сервера + 10. Убедиться, что соответствующая запись появилась в таблице базы данных standby-сервера + 11. Попытаться внести запись в таблицу базы данных standby-сервера. + 12. Убедиться, что операция завершилась с ошибкой + > cannot execute OPERATION in a read-only transaction + + + +## 5. Установка PgPool-II + + + 1. Установить на primary-сервер pgpool2 и postgresql-14-pgpool2 +``` +apt-get -y install pgpool2 postgresql-15-pgpool2 + +``` + 2. Установить на standby-сервер только postgresql-14-pgpool2 +``` +apt-get -y install postgresql-15-pgpool2 +``` +### Далее все настройки выполнить на primary-сервере + 3. Зайти на редактирование в конфигурационный файл pgpool2 + ``` + sudo nano /etc/pgpool2/pgpool.conf + ``` + 4. Задать параметры следующим образом: + ``` + backend_clustering_mode = 'streaming_replication' + listen_addresses = '*, ' + port = 9999 + ___ + backend_hostname0 = '' + backend_port0 = '<порт primary-сервера>' + backend_weight0 = 0 + backend_data_directory0 = '/var/lib/postgresql/14/main' + ___ + backend_hostname1 = '' + backend_port1 = '<порт primary-сервера>' + backend_weight1 = 1 + ___ + enable_pool_hba = on + log_statement = on + log_per_node_statement = on + pid_file_name = "pgpool.pid" + load_balance_mode = on + statement_level_load_balance = on + sr_check_period = 1 + sr_check_user = '<имя пользователя>' + sr_check_password = '<пароль пользователя>' + health_check_period = 10 + health_check_user = '<имя пользователя>' + health_check_password = '<пароль пользователя>' + ``` +5. Поскольку enable_pool_hba указан в режиме on, это значит, что Pgpool-II будет использовать pool_hba.conf для аутентификации клиента. Поэтому открываем на редактирование pool_hba.conf +``` +sudo nano /etc/pgpool2/pool_hba.conf +``` +6. Добавить строку +``` +host all all md5 +``` +7. Pgpool-II извлекает пароль пользователя из файла pool_passwd +``` +sudo nano /etc/pgpool2/pool_passwd +``` +Файл паролей представляет собой текстовый файл следующего формата: +``` +пользователь1:пароль1 +пользователь2:пароль2 +``` +Файл может содержать 3 типа паролей. Pgpool-II идентифицирует тип формата пароля по его префиксу, поэтому каждая запись пароля в pool_passwd должна иметь префикс формата пароля. + +- Обычный текст : пароль в текстовом формате с использованием префикса TEXT (например, TEXTmypassword ) . +- Зашифрованный пароль AES256 : зашифрованный пароль AES256, используя префикс AES (например, AESmzVzywsN1Z5GABhSAhwLSA== ) . +- Хешированный пароль MD5 : хешированный пароль MD5, используя префикс md5 (например, md5270e98c3db83dbc0e40f98d9bfe20972 ) . + +8. В примере в качестве пароля используется обычный текст (пароль q) +``` +postgres:TEXTq +``` +9. Запустить pgpool +``` +sudo pgpool -n +``` +10. Убедиться, что процесс успешно запущен и подключены 2 ноды с разными индексами. В данном примере для primary node установлен индекс 0, а для standBy ноды установден индекс 1 +``` + +2023-09-14 06:08:08.339: main pid 3941: LOG: find_primary_node: primary node is 0 +2023-09-14 06:08:08.339: main pid 3941: LOG: find_primary_node: standby node is 1 +2023-09-14 06:08:08.343: pcp_main pid 3977: LOG: PCP process: 3977 started +2023-09-14 06:08:08.343: sr_check_worker pid 3978: LOG: process started +2023-09-14 06:08:08.345: health_check pid 3979: LOG: process started +2023-09-14 06:08:08.349: health_check pid 3980: LOG: process started +2023-09-14 06:08:08.559: main pid 3941: LOG: pgpool-II successfully started. version 4.3.5 (tamahomeboshi) +2023-09-14 06:08:08.662: main pid 3941: LOG: node status[0]: 1 +2023-09-14 06:08:08.662: main pid 3941: LOG: node status[1]: 2 + +``` +11. При старте pgpool возможны следующие ошибки: +- файл pgpool_status не найден / нет прав +- pgpool стартует, но ноды имеют одинаковый индекс и балансировка идет только на первую ноду (как проверить балансировку указано ниже)
+ +Проблема решилась удалением файла pgpool_status, откуда pgpool пытался считывать статусы для нод. + +``` +cd /var/log/postgresql +rm -rf pgpool_status +sudo systemctl restart postgresql +sudo pgpool -n +``` + +## 6. Тестирование балансировки PgPool-II +1. При запущенном pgpool (он должен выводить логи), открыть еще один терминал. Зайти в базу, используя Pgpool-II на 9999-порте, выполнив команду +``` +psql -h -p 9999 -d postgres -U postgres +``` + +2. Выполнить команду +``` +show pool_nodes; +``` +3. Убедиться, что обе ноды находятся в статусе up, а балансировка установлена на standBy-сервере (load_balance_node = true) +``` + node_id | hostname | port | status | pg_status | lb_weight | role | pg_role | select_cnt | load_balance_node | replication_delay | replication_state | replication_sync_state | last_status_change +---------+--------------+------+--------+-----------+-----------+---------+---------+------------+-------------------+-------------------+-------------------+------------------------+--------------------- + 0 | 192.168.0.71 | 5432 | up | up | 0.000000 | primary | primary | 0 | false | 0 | | | 2023-09-14 06:36:16 + 1 | 192.168.0.72 | 5432 | up | up | 1.000000 | standby | standby | 0 | true | 0 | | | 2023-09-14 06:36:16 +(2 rows) + +``` +4. Выполнить команды Insert / Update / Delete (в качесве примера была внесена запись в таблицу public.t_company). Убедиться, что запрос приходит на primary-сервер (нода с индексом 0). + +``` +2023-09-14 07:04:31.800: DBeaver 23.1.2 - Main pid 4805: LOG: DB node id: 0 backend pid: 4814 statement: Execute: INSERT INTO public.t_company (id,caption,id_company_type) + VALUES ($1,$2,$3) + +``` +5. Выполинть команду Select. Убедиться, что запрос приходит на standBy-сервер (нода с индексом 1). +``` + +2023-09-14 07:53:19.275: DBeaver 23.1.2 - Main pid 5069: LOG: DB node id: 1 backend pid: 2745 statement: Execute: SELECT x.* FROM public.t_company x + +``` \ No newline at end of file diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index 101a8273..f56b0ae4 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,13 +14,37 @@ namespace AsbCloudInfrastructure.Background; ///
public class BackgroundWorker : BackgroundService { - private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); - private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); + private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); private readonly IServiceProvider serviceProvider; - public WorkStore WorkStore { get; } = new WorkStore(); + /// + /// Очередь работ + /// + private Queue works = new(8); + + /// + /// Список периодических работ + /// + public IEnumerable Works => works; + + /// + /// Работа выполняемая в данный момент + /// public Work? CurrentWork; + /// + /// последние 16 завершившиеся с ошибкой + /// + public CyclycArray Felled { get; } = new(16); + + /// + /// последние 16 успешно завершенных + /// + public CyclycArray Done { get; } = new(16); + + /// + /// Ошибка в главном цикле, никогда не должна появляться + /// public string MainLoopLastException { get; private set; } = string.Empty; public BackgroundWorker(IServiceProvider serviceProvider) @@ -28,26 +54,18 @@ public class BackgroundWorker : BackgroundService protected override async Task ExecuteAsync(CancellationToken token) { - while (!token.IsCancellationRequested) + while (!token.IsCancellationRequested && works.TryDequeue(out CurrentWork)) { try { - var work = WorkStore.GetNext(); - if (work is null) - { - await Task.Delay(executePeriod, token); - continue; - } - - CurrentWork = work; using var scope = serviceProvider.CreateScope(); - var result = await work.Start(scope.ServiceProvider, token); + var result = await CurrentWork.Start(scope.ServiceProvider, token); if (!result) - WorkStore.Felled.Add(work); + Felled.Add(CurrentWork); else - WorkStore.Done.Add(work); + Done.Add(CurrentWork); CurrentWork = null; await Task.Delay(minDelay, token); @@ -64,4 +82,36 @@ public class BackgroundWorker : BackgroundService } } } + + /// + /// Добавить в очередь + /// + /// work.Id может быть не уникальным, + /// при этом метод TryRemoveFromQueue удалит все работы с совпадающими id + /// + /// + /// + public void Enqueue(Work work) + { + works.Enqueue(work); + if (ExecuteTask is null || ExecuteTask.IsCompleted) + StartAsync(CancellationToken.None).Wait(); + } + + /// + /// Удаление работы по ID из одноразовой очереди + /// + /// + /// + public bool TryRemoveFromQueue(string id) + { + var work = Works.FirstOrDefault(w => w.Id == id); + if (work is not null) + { + works = new Queue(Works.Where(w => w.Id != id)); + return true; + } + + return false; + } } diff --git a/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs new file mode 100644 index 00000000..a7490ed7 --- /dev/null +++ b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs @@ -0,0 +1,115 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Background; + +/// +/// Сервис для фонового выполнения периодической работы +/// +public class PeriodicBackgroundWorker : BackgroundService +{ + private readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); + private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); + private readonly IServiceProvider serviceProvider; + + private readonly List works = new(8); + + /// + /// Список периодических работ + /// + public IEnumerable Works => works; + + /// + /// Работа выполняемая в данный момент + /// + public Work? CurrentWork; + + /// + /// Ошибка в главном цикле, никогда не должна появляться + /// + public string MainLoopLastException { get; private set; } = string.Empty; + + public PeriodicBackgroundWorker(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + var periodicWork = GetNext(); + if (periodicWork is null) + { + await Task.Delay(executePeriod, token); + continue; + } + + CurrentWork = periodicWork.Work; + + using var scope = serviceProvider.CreateScope(); + + var result = await periodicWork.Work.Start(scope.ServiceProvider, token); + + CurrentWork = null; + await Task.Delay(minDelay, token); + } + catch (Exception ex) + { + MainLoopLastException = $"BackgroundWorker " + + $"MainLoopLastException: \r\n" + + $"date: {DateTime.Now:O}\r\n" + + $"message: {ex.Message}\r\n" + + $"inner: {ex.InnerException?.Message}\r\n" + + $"stackTrace: {ex.StackTrace}"; + Trace.TraceError(MainLoopLastException); + } + } + } + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// + public void Add(TimeSpan period) + where T : Work, new() + { + var work = new T(); + var periodic = new WorkPeriodic(work, period); + works.Add(periodic); + } + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// + public void Add(Work work, TimeSpan period) + { + var periodic = new WorkPeriodic(work, period); + works.Add(periodic); + if (ExecuteTask is null || ExecuteTask.IsCompleted) + StartAsync(CancellationToken.None).Wait(); + } + + private WorkPeriodic? GetNext() + { + var work = works + .OrderBy(w => w.NextStart) + .FirstOrDefault(); + + if (work is null || work.NextStart > DateTime.Now) + return null; + + return work; + } +} diff --git a/AsbCloudInfrastructure/Background/Work.cs b/AsbCloudInfrastructure/Background/Work.cs index 4820aa26..1177892c 100644 --- a/AsbCloudInfrastructure/Background/Work.cs +++ b/AsbCloudInfrastructure/Background/Work.cs @@ -13,6 +13,8 @@ namespace AsbCloudInfrastructure.Background; /// public abstract class Work : BackgroundWorkDto { + private CancellationTokenSource? stoppingCts; + private sealed class WorkBase : Work { private Func, CancellationToken, Task> ActionAsync { get; } @@ -69,8 +71,9 @@ public abstract class Work : BackgroundWorkDto SetStatusStart(); try { - var task = Action(Id, services, UpdateStatus, token); - await task.WaitAsync(Timeout, token); + stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(token); + var task = Action(Id, services, UpdateStatus, stoppingCts.Token); + await task.WaitAsync(Timeout, stoppingCts.Token); SetStatusComplete(); return true; } @@ -97,6 +100,11 @@ public abstract class Work : BackgroundWorkDto return false; } + public void Stop() + { + stoppingCts?.Cancel(); + } + private static string FormatExceptionMessage(Exception exception) { var firstException = FirstException(exception); diff --git a/AsbCloudInfrastructure/Background/WorkStore.cs b/AsbCloudInfrastructure/Background/WorkStore.cs deleted file mode 100644 index be7386e6..00000000 --- a/AsbCloudInfrastructure/Background/WorkStore.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace AsbCloudInfrastructure.Background; - -/// -/// -/// Очередь работ -/// -/// Не периодические задачи будут возвращаться первыми, как самые приоритетные. -/// -public class WorkStore -{ - private readonly List periodics = new(8); - - /// - /// Список периодических задач - /// - public IEnumerable Periodics => periodics; - - /// - /// Работы выполняемые один раз - /// - public Queue RunOnceQueue { get; private set; } = new(8); - - /// - /// последние 16 завершившиеся с ошибкой - /// - public CyclycArray Felled { get; } = new(16); - - /// - /// последние 16 успешно завершенных - /// - public CyclycArray Done { get; } = new(16); - - /// - /// Добавить фоновую работу выполняющуюся с заданным периодом - /// - /// - /// - public void AddPeriodic(TimeSpan period) - where T : Work, new() - { - var work = new T(); - var periodic = new WorkPeriodic(work, period); - periodics.Add(periodic); - } - - /// - /// Добавить фоновую работу выполняющуюся с заданным периодом - /// - /// - /// - public void AddPeriodic(Work work, TimeSpan period) - { - var periodic = new WorkPeriodic(work, period); - periodics.Add(periodic); - } - - /// - /// Удаление работы по ID из одноразовой очереди - /// - /// - /// - public bool TryRemoveFromRunOnceQueue(string id) - { - var work = RunOnceQueue.FirstOrDefault(w => w.Id == id); - if (work is not null) - { - RunOnceQueue = new Queue(RunOnceQueue.Where(w => w.Id != id)); - return true; - } - - return false; - } - - /// - /// - /// Возвращает приоритетную задачу. - /// - /// - /// Если приоритетные закончились, то ищет ближайшую периодическую. - /// Если до старта ближайшей периодической работы меньше 20 сек, - /// то этой задаче устанавливается время последнего запуска в now и она возвращается. - /// Если больше 20 сек, то возвращается null. - /// - /// - /// - /// - public Work? GetNext() - { - if (RunOnceQueue.Any()) - return RunOnceQueue.Dequeue(); - - var work = GetNextPeriodic(); - if (work is null || work.NextStart > DateTime.Now) - return null; - - return work.Work; - } - - private WorkPeriodic? GetNextPeriodic() - { - var work = Periodics - .OrderBy(w => w.NextStart) - .FirstOrDefault(); - return work; - } -} diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 93c6575e..28bde671 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -165,6 +165,7 @@ namespace AsbCloudInfrastructure services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider)); services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider)); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => ReduceSamplingService.GetInstance(configuration)); diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index 228a3564..c91689a3 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -513,7 +513,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram if (state.IdState == idStateCreating) { var workId = MakeWorkId(idWell); - if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) + if (!backgroundWorker.Works.Any(w => w.Id == workId)) { var well = (await wellService.GetOrDefaultAsync(idWell, token))!; var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf"; @@ -542,7 +542,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram var work = Work.CreateByDelegate(workId, workAction); work.OnErrorAsync = onErrorAction; - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); } } } @@ -556,7 +556,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token) { var workId = MakeWorkId(idWell); - backgroundWorker.WorkStore.TryRemoveFromRunOnceQueue(workId); + backgroundWorker.TryRemoveFromQueue(workId); var filesIds = await context.Files .Where(f => f.IdWell == idWell && diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index bbbc8196..e56e423f 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -52,12 +52,12 @@ namespace AsbCloudInfrastructure.Services.Email } var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message); - if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w=>w.Id==workId)) + if (!backgroundWorker.Works.Any(w=>w.Id==workId)) { var workAction = MakeEmailSendWorkAction(notification); var work = Work.CreateByDelegate(workId, workAction); - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); } return Task.CompletedTask; diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index 6a66c30e..ed24bc3e 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -95,7 +95,7 @@ namespace AsbCloudInfrastructure.Services }; var work = Work.CreateByDelegate(workId, workAction); - backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorkerService.Enqueue(work); progressHandler.Invoke(new ReportProgressDto { diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs index eabfcf77..3a6eac6f 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -52,7 +52,7 @@ namespace AsbCloudInfrastructure.Services.SAUB await instance.InitializeCacheFromDBAsync(db, onProgress, token); }); work.Timeout = TimeSpan.FromMinutes(15); - worker.WorkStore.RunOnceQueue.Enqueue(work); + worker.Enqueue(work); } return instance; } diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index cef6745b..64191e5e 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -33,12 +33,12 @@ namespace AsbCloudInfrastructure _ = provider.GetRequiredService>(); _ = provider.GetRequiredService>(); - var backgroundWorker = provider.GetRequiredService(); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(15)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); + var backgroundWorker = provider.GetRequiredService(); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(TimeSpan.FromMinutes(15)); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); var notificationBackgroundWorker = provider.GetRequiredService(); diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs index 17a14ab1..7ced750b 100644 --- a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs +++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs @@ -53,11 +53,21 @@ namespace AsbCloudWebApi.Tests.Middlware throw new NotImplementedException(); } + public DatesRangeDto? GetRange(int idWell) + { + throw new NotImplementedException(); + } + public Task GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token) { throw new NotImplementedException(); } + public Task GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token) + { + throw new NotImplementedException(); + } + public Task> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException(); public Task GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token) diff --git a/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs new file mode 100644 index 00000000..0f54dddf --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs @@ -0,0 +1,113 @@ +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services; + +public class BackgroundWorkerTest +{ + private IServiceProvider provider; + private BackgroundWorker service; + + public BackgroundWorkerTest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + + service = new BackgroundWorker(provider); + typeof(BackgroundWorker) + .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(service, TimeSpan.FromMilliseconds(1)); + } + + [Fact] + public async Task Enqueue_n_works() + { + var workCount = 10; + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.Delay(1); + } + + //act + for (int i = 0; i < workCount; i++) + { + var work = Work.CreateByDelegate(i.ToString(), workAction); + service.Enqueue(work); + } + + await service.ExecuteTask; + + //assert + Assert.Equal(workCount, result); + } + + [Fact] + public async Task Enqueue_continues_after_exceptions() + { + var expectadResult = 42; + var result = 0; + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result = expectadResult; + return Task.CompletedTask; + } + var goodWork = Work.CreateByDelegate("", workAction); + + Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => throw new Exception(); + + var badWork = Work.CreateByDelegate("", failAction); + badWork.OnErrorAsync = (id, exception, token) => throw new Exception(); + + //act + service.Enqueue(badWork); + service.Enqueue(goodWork); + + await service.ExecuteTask; + + //assert + Assert.Equal(expectadResult, result); + Assert.Equal(1, service.Felled.Count); + Assert.Equal(1, service.Done.Count); + } + + [Fact] + public async Task TryRemove() + { + var workCount = 5; + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.Delay(10); + } + + //act + for (int i = 0; i < workCount; i++) + { + var work = Work.CreateByDelegate(i.ToString(), workAction); + service.Enqueue(work); + } + + var removed = service.TryRemoveFromQueue((workCount - 1).ToString()); + + await service.ExecuteTask; + + //assert + Assert.True(removed); + Assert.Equal(workCount - 1, result); + Assert.Equal(workCount - 1, service.Done.Count); + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs b/AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs rename to AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs b/AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs b/AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs similarity index 99% rename from AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs index f0ccd6c5..2a9635c9 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs @@ -366,7 +366,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); Assert.Equal(2, state.IdState); - backgroundWorkerMock.Verify(s => s.WorkStore.RunOnceQueue.Enqueue(It.IsAny())); + backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny())); } [Fact] diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs rename to AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs rename to AsbCloudWebApi.Tests/Services/FileServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs b/AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs rename to AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs b/AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs rename to AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs diff --git a/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs new file mode 100644 index 00000000..d0c16285 --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs @@ -0,0 +1,97 @@ +using AsbCloudInfrastructure.Background; +using DocumentFormat.OpenXml.Drawing.Charts; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services; + +public class PeriodicBackgroundWorkerTest +{ + private IServiceProvider provider; + private PeriodicBackgroundWorker service; + + public PeriodicBackgroundWorkerTest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + + service = new PeriodicBackgroundWorker(provider); + typeof(PeriodicBackgroundWorker) + .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(service, TimeSpan.FromMilliseconds(1)); + + typeof(PeriodicBackgroundWorker) + .GetField("executePeriod", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(service, TimeSpan.FromMilliseconds(1)); + } + + [Fact] + public async Task WorkRunsTwice() + { + var workCount = 2; + var periodMs = 100d; + + var period = TimeSpan.FromMilliseconds(periodMs); + + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.CompletedTask; + } + + //act + var work = Work.CreateByDelegate("", workAction); + + var stopwatch = Stopwatch.StartNew(); + service.Add(work, period); + + var delay = (periodMs / 20) + (periodMs * workCount) - stopwatch.ElapsedMilliseconds; + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + + //assert + Assert.Equal(workCount, result); + } + + + [Fact] + public async Task Enqueue_continues_after_exceptions() + { + var expectadResult = 42; + var result = 0; + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result = expectadResult; + return Task.CompletedTask; + } + var goodWork = Work.CreateByDelegate("", workAction); + + Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => throw new Exception(); + + var badWork = Work.CreateByDelegate("", failAction); + badWork.OnErrorAsync = (id, exception, token) => throw new Exception(); + + //act + service.Add(badWork, TimeSpan.FromSeconds(2)); + service.Add(goodWork, TimeSpan.FromSeconds(2)); + + await Task.Delay(TimeSpan.FromMilliseconds(20)); + + //assert + Assert.Equal(expectadResult, result); + Assert.Equal(1, badWork.CountErrors); + Assert.Equal(1, goodWork.CountComplete); + Assert.Equal(1, goodWork.CountStart); + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs b/AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs rename to AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs b/AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs rename to AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs b/AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs rename to AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs rename to AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs rename to AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs diff --git a/AsbCloudWebApi.Tests/Services/WorkTest.cs b/AsbCloudWebApi.Tests/Services/WorkTest.cs new file mode 100644 index 00000000..4f144d98 --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/WorkTest.cs @@ -0,0 +1,150 @@ +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services +{ + public class WorkTest + { + private IServiceProvider provider; + + public WorkTest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + } + + [Fact] + public async Task Work_done_with_success() + { + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => Task.CompletedTask; + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + await work.Start(provider, CancellationToken.None); + var done = DateTime.Now; + var executionTime = done - begin; + + //assert + Assert.Equal(1, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(0, work.CountErrors); + Assert.Null(work.CurrentState); + Assert.Null(work.LastError); + + var lastState = work.LastComplete; + Assert.NotNull(lastState); + Assert.InRange(lastState.Start, begin, done - 0.5 * executionTime); + Assert.InRange(lastState.End, done - 0.5 * executionTime, done); + Assert.InRange(lastState.ExecutionTime, TimeSpan.Zero, executionTime); + } + + [Fact] + public async Task Work_calls_callback() + { + var expectedState = "42"; + var expectedProgress = 42d; + + var timeout = TimeSpan.FromMilliseconds(40); + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + callback.Invoke(expectedState, expectedProgress); + return Task.Delay(timeout); + } + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + _ = work.Start(provider, CancellationToken.None); + await Task.Delay(timeout/3); + + //assert + Assert.Equal(0, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(0, work.CountErrors); + Assert.NotNull(work.CurrentState); + Assert.Null(work.LastComplete); + Assert.Null(work.LastError); + + var currentState = work.CurrentState; + Assert.NotNull(currentState); + Assert.InRange(currentState.Start, begin, begin + timeout); + Assert.InRange(currentState.StateUpdate, begin, begin + timeout); + Assert.Equal(expectedState, currentState.State); + Assert.Equal(expectedProgress, currentState.Progress); + } + + [Fact] + public async Task Work_fails_with_info() + { + var expectedState = "41"; + var expectedErrorText = "42"; + var minWorkTime = TimeSpan.FromMilliseconds(10); + + async Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + await Task.Delay(minWorkTime); + callback(expectedState, 0); + throw new Exception(expectedErrorText); + } + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + await work.Start(provider, CancellationToken.None); + + //assert + Assert.Equal(0, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(1, work.CountErrors); + Assert.Null(work.CurrentState); + Assert.Null(work.LastComplete); + + var error = work.LastError; + Assert.NotNull(error); + Assert.InRange(error.Start, begin, DateTime.Now); + Assert.InRange(error.End, begin, DateTime.Now); + Assert.InRange(error.ExecutionTime, minWorkTime, DateTime.Now - begin); + Assert.Contains(expectedErrorText, error.ErrorText, StringComparison.InvariantCultureIgnoreCase); + Assert.Equal(expectedState, error.State); + } + + [Fact] + public async Task Stop() + { + var workTime = TimeSpan.FromMilliseconds(1_000); + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => Task.Delay(workTime, token); + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + _ = work.Start(provider, CancellationToken.None); + await Task.Delay(10); + work.Stop(); + await Task.Delay(10); + + //assert + Assert.Equal(0, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(0, work.CountErrors); + Assert.Null(work.LastComplete); + Assert.Null(work.LastError); + } + } +} diff --git a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs index 0a8c02c0..bb0cdc43 100644 --- a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs +++ b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs @@ -14,23 +14,22 @@ namespace AsbCloudWebApi.Controllers [ApiController] public class BackgroundWorkController : ControllerBase { - private readonly BackgroundWorker backgroundWorker; + private readonly BackgroundWorker worker; - public BackgroundWorkController(BackgroundWorker backgroundWorker) + public BackgroundWorkController(BackgroundWorker worker) { - this.backgroundWorker = backgroundWorker; + this.worker = worker; } [HttpGet] public IActionResult GetAll() { var result = new { - CurrentWork = (BackgroundWorkDto?)backgroundWorker.CurrentWork, - backgroundWorker.MainLoopLastException, - RunOnceQueue = backgroundWorker.WorkStore.RunOnceQueue.Select(work => (BackgroundWorkDto)work), - Periodics = backgroundWorker.WorkStore.Periodics.Select(work => (BackgroundWorkDto)work.Work), - Done = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work), - Felled = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work), + CurrentWork = (BackgroundWorkDto?)worker.CurrentWork, + worker.MainLoopLastException, + RunOnceQueue = worker.Works.Select(work => (BackgroundWorkDto)work), + Done = worker.Done.Select(work => (BackgroundWorkDto)work), + Felled = worker.Felled.Select(work => (BackgroundWorkDto)work), }; return Ok(result); } @@ -38,7 +37,7 @@ namespace AsbCloudWebApi.Controllers [HttpGet("current")] public IActionResult GetCurrent() { - var work = backgroundWorker.CurrentWork; + var work = worker.CurrentWork; if (work == null) return NoContent(); @@ -48,22 +47,22 @@ namespace AsbCloudWebApi.Controllers [HttpGet("failed")] public IActionResult GetFelled() { - var result = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work); + var result = worker.Felled.Select(work => (BackgroundWorkDto)work); return Ok(result); } [HttpGet("done")] public IActionResult GetDone() { - var result = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work); + var result = worker.Done.Select(work => (BackgroundWorkDto)work); return Ok(result); } [HttpPost("restart"), Obsolete("temporary method")] public async Task RestartAsync(CancellationToken token) { - await backgroundWorker.StopAsync(token); - await backgroundWorker.StartAsync(token); + await worker.StopAsync(token); + await worker.StartAsync(token); return Ok(); } } diff --git a/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs new file mode 100644 index 00000000..d7f666a0 --- /dev/null +++ b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs @@ -0,0 +1,44 @@ +using AsbCloudApp.Data; +using AsbCloudInfrastructure.Background; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudWebApi.Controllers +{ + [Route("api/[controller]")] + [Authorize] + [ApiController] + public class PeriodicBackgroundWorkerController : ControllerBase + { + private readonly PeriodicBackgroundWorker worker; + + public PeriodicBackgroundWorkerController(PeriodicBackgroundWorker worker) + { + this.worker = worker; + } + + [HttpGet] + public IActionResult GetAll() + { + var result = new + { + currentWork = (BackgroundWorkDto?)worker.CurrentWork, + worker.MainLoopLastException, + works = worker.Works.Select(work => (BackgroundWorkDto)work.Work), + }; + return Ok(result); + } + + [HttpPost("restart"), Obsolete("temporary method")] + public async Task RestartAsync(CancellationToken token) + { + await worker.StopAsync(token); + await worker.StartAsync(token); + return Ok(); + } + } +} diff --git a/AsbCloudWebApi/SignalR/ReportsHub.cs b/AsbCloudWebApi/SignalR/ReportsHub.cs index 1097c5ef..66292d05 100644 --- a/AsbCloudWebApi/SignalR/ReportsHub.cs +++ b/AsbCloudWebApi/SignalR/ReportsHub.cs @@ -26,7 +26,7 @@ namespace AsbCloudWebApi.SignalR await base.AddToGroup(groupName); var workId = groupName.Replace("Report_", ""); - var work = backgroundWorker.WorkStore.RunOnceQueue.FirstOrDefault(work => work.Id == workId); + var work = backgroundWorker.Works.FirstOrDefault(work => work.Id == workId); var progress = new ReportProgressDto() { diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index 962e5e0f..899d4ad7 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -29,12 +29,12 @@ public class SignalRNotificationTransportService : INotificationTransportService { var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x"); - if (backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) + if (backgroundWorker.Works.Any(w => w.Id == workId)) return Task.CompletedTask; var workAction = MakeSignalRSendWorkAction(notifications); var work = Work.CreateByDelegate(workId, workAction); - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); return Task.CompletedTask; }