forked from ddrilling/AsbCloudServer
Merge branch 'dev' into feature/read-x-real-ip-clients
This commit is contained in:
commit
a24a6da9ef
@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsbCloudApp", "AsbCloudApp\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsbCloudInfrastructure", "AsbCloudInfrastructure\AsbCloudInfrastructure.csproj", "{67DBFC52-BAE4-4903-827A-AD0288C292B6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{D04A84E7-5F08-4042-8FB5-476EE49E9D22}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsbCloudDb", "AsbCloudDb\AsbCloudDb.csproj", "{40FBD29B-724B-4496-B5D9-1A5D14102456}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsbCloudWebApi.Tests", "AsbCloudWebApi.Tests\AsbCloudWebApi.Tests.csproj", "{9CF6FBB1-9AF5-45AB-A521-24F11A79B540}"
|
||||
@ -35,10 +33,6 @@ Global
|
||||
{67DBFC52-BAE4-4903-827A-AD0288C292B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67DBFC52-BAE4-4903-827A-AD0288C292B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{67DBFC52-BAE4-4903-827A-AD0288C292B6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D04A84E7-5F08-4042-8FB5-476EE49E9D22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D04A84E7-5F08-4042-8FB5-476EE49E9D22}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D04A84E7-5F08-4042-8FB5-476EE49E9D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D04A84E7-5F08-4042-8FB5-476EE49E9D22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{40FBD29B-724B-4496-B5D9-1A5D14102456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{40FBD29B-724B-4496-B5D9-1A5D14102456}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40FBD29B-724B-4496-B5D9-1A5D14102456}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
8894
AsbCloudDb/Migrations/20231107091439_Add_Data_To_WellSectionType.Designer.cs
generated
Normal file
8894
AsbCloudDb/Migrations/20231107091439_Add_Data_To_WellSectionType.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AsbCloudDb.Migrations
|
||||
{
|
||||
public partial class Add_Data_To_WellSectionType : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.InsertData(
|
||||
table: "t_well_section_type",
|
||||
columns: new[] { "id", "caption", "order" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 34, "Хвостовик 6", 6.5f },
|
||||
{ 35, "Хвостовик 7", 6.6f },
|
||||
{ 36, "Хвостовик 8", 6.7f },
|
||||
{ 37, "Хвостовик 9", 6.8f },
|
||||
{ 38, "Хвостовик 10", 6.9f }
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData(
|
||||
table: "t_well_section_type",
|
||||
keyColumn: "id",
|
||||
keyValue: 34);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "t_well_section_type",
|
||||
keyColumn: "id",
|
||||
keyValue: 35);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "t_well_section_type",
|
||||
keyColumn: "id",
|
||||
keyValue: 36);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "t_well_section_type",
|
||||
keyColumn: "id",
|
||||
keyValue: 37);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "t_well_section_type",
|
||||
keyColumn: "id",
|
||||
keyValue: 38);
|
||||
}
|
||||
}
|
||||
}
|
@ -7102,6 +7102,36 @@ namespace AsbCloudDb.Migrations
|
||||
Id = 33,
|
||||
Caption = "Техническая колонна 3",
|
||||
Order = 2.2f
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 34,
|
||||
Caption = "Хвостовик 6",
|
||||
Order = 6.5f
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 35,
|
||||
Caption = "Хвостовик 7",
|
||||
Order = 6.6f
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 36,
|
||||
Caption = "Хвостовик 8",
|
||||
Order = 6.7f
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 37,
|
||||
Caption = "Хвостовик 9",
|
||||
Order = 6.8f
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 38,
|
||||
Caption = "Хвостовик 10",
|
||||
Order = 6.9f
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -42,6 +42,12 @@
|
||||
new (){ Id = 31, Caption = "Техническая колонна", Order = 2},
|
||||
new (){ Id = 32, Caption = "Техническая колонна 2", Order = 2.1f},
|
||||
new (){ Id = 33, Caption = "Техническая колонна 3", Order = 2.2f},
|
||||
|
||||
new (){ Id = 34, Caption = "Хвостовик 6", Order = 6.5f},
|
||||
new (){ Id = 35, Caption = "Хвостовик 7", Order = 6.6f},
|
||||
new (){ Id = 36, Caption = "Хвостовик 8", Order = 6.7f},
|
||||
new (){ Id = 37, Caption = "Хвостовик 9", Order = 6.8f},
|
||||
new (){ Id = 38, Caption = "Хвостовик 10", Order = 6.9f},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
265
AsbCloudDb/Setup db replication.md
Normal file
265
AsbCloudDb/Setup db replication.md
Normal file
@ -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 = '*, <ip standby-сервера>'
|
||||
|
||||
3. Открыть клиент для работы с postgres
|
||||
|
||||
```
|
||||
sudo -u postgres psql
|
||||
```
|
||||
|
||||
4. Создать пользователя с атрибутом REPLICATION. <br />
|
||||
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..." <br />
|
||||
Данные для вставки записи:<br />
|
||||
- replicator - имя пользователя, созданного на предыдущем шаге<br/>
|
||||
- <ip подсети>, например, 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 <ip primary-сервера> -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 = '*, <ip standby-сервера>'
|
||||
port = 9999
|
||||
___
|
||||
backend_hostname0 = '<ip primary-сервера>'
|
||||
backend_port0 = '<порт primary-сервера>'
|
||||
backend_weight0 = 0
|
||||
backend_data_directory0 = '/var/lib/postgresql/14/main'
|
||||
___
|
||||
backend_hostname1 = '<ip replica-сервера>'
|
||||
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 <ip подсети> 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 стартует, но ноды имеют одинаковый индекс и балансировка идет только на первую ноду (как проверить балансировку указано ниже) <br/>
|
||||
|
||||
Проблема решилась удалением файла 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 <ip сервера, где установлен pgpool> -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 <postgres> 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 <postgres> pid 5069: LOG: DB node id: 1 backend pid: 2745 statement: Execute: SELECT x.* FROM public.t_company x
|
||||
|
||||
```
|
@ -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;
|
||||
/// </summary>
|
||||
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();
|
||||
/// <summary>
|
||||
/// Очередь работ
|
||||
/// </summary>
|
||||
private Queue<Work> works = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// Список периодических работ
|
||||
/// </summary>
|
||||
public IEnumerable<Work> Works => works;
|
||||
|
||||
/// <summary>
|
||||
/// Работа выполняемая в данный момент
|
||||
/// </summary>
|
||||
public Work? CurrentWork;
|
||||
|
||||
/// <summary>
|
||||
/// последние 16 завершившиеся с ошибкой
|
||||
/// </summary>
|
||||
public CyclycArray<Work> Felled { get; } = new(16);
|
||||
|
||||
/// <summary>
|
||||
/// последние 16 успешно завершенных
|
||||
/// </summary>
|
||||
public CyclycArray<Work> Done { get; } = new(16);
|
||||
|
||||
/// <summary>
|
||||
/// Ошибка в главном цикле, никогда не должна появляться
|
||||
/// </summary>
|
||||
public string MainLoopLastException { get; private set; } = string.Empty;
|
||||
|
||||
public BackgroundWorker(IServiceProvider serviceProvider)
|
||||
@ -28,26 +54,19 @@ public class BackgroundWorker : BackgroundService
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
Trace.TraceInformation($"{GetType().Name} started");
|
||||
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 +83,36 @@ public class BackgroundWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить в очередь
|
||||
/// <para>
|
||||
/// work.Id может быть не уникальным,
|
||||
/// при этом метод TryRemoveFromQueue удалит все работы с совпадающими id
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="work"></param>
|
||||
public void Enqueue(Work work)
|
||||
{
|
||||
works.Enqueue(work);
|
||||
if (ExecuteTask is null || ExecuteTask.IsCompleted)
|
||||
StartAsync(CancellationToken.None).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаление работы по ID из одноразовой очереди
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public bool TryRemoveFromQueue(string id)
|
||||
{
|
||||
var work = Works.FirstOrDefault(w => w.Id == id);
|
||||
if (work is not null)
|
||||
{
|
||||
works = new Queue<Work>(Works.Where(w => w.Id != id));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace System.Collections.Generic
|
||||
{
|
||||
public class OrderedList<T>: IEnumerable<T>, ICollection<T>
|
||||
where T : notnull
|
||||
{
|
||||
private readonly List<T> list = new List<T>();
|
||||
|
||||
private readonly Func<T, object> keySelector;
|
||||
private readonly bool isDescending = false;
|
||||
|
||||
private IOrderedEnumerable<T> OrdredList => isDescending
|
||||
? list.OrderByDescending(keySelector)
|
||||
: list.OrderBy(keySelector);
|
||||
|
||||
public int Count => list.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public OrderedList(Func<T, object> keySelector, bool isDescending = false)
|
||||
{
|
||||
this.keySelector = keySelector;
|
||||
this.isDescending = isDescending;
|
||||
}
|
||||
|
||||
public void Add(T item) => list.Add(item);
|
||||
|
||||
public void Clear()=> list.Clear();
|
||||
|
||||
public bool Contains(T item)=> list.Contains(item);
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)=> list.CopyTo(array, arrayIndex);
|
||||
|
||||
public bool Remove(T item)=> list.Remove(item);
|
||||
|
||||
public IEnumerator<T> GetEnumerator() => OrdredList.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
116
AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
Normal file
116
AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
Normal file
@ -0,0 +1,116 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис для фонового выполнения периодической работы
|
||||
/// </summary>
|
||||
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<WorkPeriodic> works = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// Список периодических работ
|
||||
/// </summary>
|
||||
public IEnumerable<WorkPeriodic> Works => works;
|
||||
|
||||
/// <summary>
|
||||
/// Работа выполняемая в данный момент
|
||||
/// </summary>
|
||||
public Work? CurrentWork;
|
||||
|
||||
/// <summary>
|
||||
/// Ошибка в главном цикле, никогда не должна появляться
|
||||
/// </summary>
|
||||
public string MainLoopLastException { get; private set; } = string.Empty;
|
||||
|
||||
public PeriodicBackgroundWorker(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken token)
|
||||
{
|
||||
Trace.TraceInformation($"{GetType().Name} started");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить фоновую работу выполняющуюся с заданным периодом
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="period"></param>
|
||||
public void Add<T>(TimeSpan period)
|
||||
where T : Work, new()
|
||||
{
|
||||
var work = new T();
|
||||
var periodic = new WorkPeriodic(work, period);
|
||||
works.Add(periodic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить фоновую работу выполняющуюся с заданным периодом
|
||||
/// </summary>
|
||||
/// <param name="work"></param>
|
||||
/// <param name="period"></param>
|
||||
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;
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ namespace AsbCloudInfrastructure.Background;
|
||||
/// </summary>
|
||||
public abstract class Work : BackgroundWorkDto
|
||||
{
|
||||
private CancellationTokenSource? stoppingCts;
|
||||
|
||||
private sealed class WorkBase : Work
|
||||
{
|
||||
private Func<string, IServiceProvider, Action<string, double?>, 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);
|
||||
|
@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AsbCloudInfrastructure.Background;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Очередь работ
|
||||
/// </para>
|
||||
/// Не периодические задачи будут возвращаться первыми, как самые приоритетные.
|
||||
/// </summary>
|
||||
public class WorkStore
|
||||
{
|
||||
private readonly List<WorkPeriodic> periodics = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// Список периодических задач
|
||||
/// </summary>
|
||||
public IEnumerable<WorkPeriodic> Periodics => periodics;
|
||||
|
||||
/// <summary>
|
||||
/// Работы выполняемые один раз
|
||||
/// </summary>
|
||||
public Queue<Work> RunOnceQueue { get; private set; } = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// последние 16 завершившиеся с ошибкой
|
||||
/// </summary>
|
||||
public CyclycArray<Work> Felled { get; } = new(16);
|
||||
|
||||
/// <summary>
|
||||
/// последние 16 успешно завершенных
|
||||
/// </summary>
|
||||
public CyclycArray<Work> Done { get; } = new(16);
|
||||
|
||||
/// <summary>
|
||||
/// Добавить фоновую работу выполняющуюся с заданным периодом
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="period"></param>
|
||||
public void AddPeriodic<T>(TimeSpan period)
|
||||
where T : Work, new()
|
||||
{
|
||||
var work = new T();
|
||||
var periodic = new WorkPeriodic(work, period);
|
||||
periodics.Add(periodic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавить фоновую работу выполняющуюся с заданным периодом
|
||||
/// </summary>
|
||||
/// <param name="work"></param>
|
||||
/// <param name="period"></param>
|
||||
public void AddPeriodic(Work work, TimeSpan period)
|
||||
{
|
||||
var periodic = new WorkPeriodic(work, period);
|
||||
periodics.Add(periodic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаление работы по ID из одноразовой очереди
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public bool TryRemoveFromRunOnceQueue(string id)
|
||||
{
|
||||
var work = RunOnceQueue.FirstOrDefault(w => w.Id == id);
|
||||
if (work is not null)
|
||||
{
|
||||
RunOnceQueue = new Queue<Work>(RunOnceQueue.Where(w => w.Id != id));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Возвращает приоритетную задачу.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Если приоритетные закончились, то ищет ближайшую периодическую.
|
||||
/// Если до старта ближайшей периодической работы меньше 20 сек,
|
||||
/// то этой задаче устанавливается время последнего запуска в now и она возвращается.
|
||||
/// Если больше 20 сек, то возвращается null.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="maxTimeToNextWork"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
11
AsbCloudInfrastructure/Background/readme.md
Normal file
11
AsbCloudInfrastructure/Background/readme.md
Normal file
@ -0,0 +1,11 @@
|
||||
# BackgroundWorker
|
||||
Класс выполнения разовой фоновой работы.
|
||||
Для каждой работы создается свой scope.
|
||||
|
||||
# NotificationBackgroundWorker
|
||||
Предназначен для различных оповещений пользователей разными способами.
|
||||
Фактически это дополнительный экземпляр BackgroundWorker, чтобы оповещения не ждали завершения долгих операций из стандартного BackgroundWorker.
|
||||
Не должен давать большой нагрузки БД.
|
||||
|
||||
# PeriodicBackgroundWorker
|
||||
Класс выполнения периодической фоновой работы.
|
@ -1,12 +0,0 @@
|
||||
# Проблемы фонового сервиса
|
||||
- Нужно состояние по загрузки сервиса и очереди работ.
|
||||
- Все ли задачи укладываются в таймаут,
|
||||
- Сколько свободного времени остается,
|
||||
- Что делает текущая задача,
|
||||
- нет управления сервисом. Для исключения его влияния на другие процессы сервера.
|
||||
- отключать/включать целиком
|
||||
- отключать/включать отдельную периодическую задачу
|
||||
|
||||
# Сделать
|
||||
- Разработать dto статуса задачи
|
||||
- Отказаться от периодической задачи, при добавлении в хранилище задач период будет параметром метода добавления.
|
@ -165,6 +165,7 @@ namespace AsbCloudInfrastructure
|
||||
services.AddSingleton<ITelemetryDataCache<TelemetryDataSaubDto>>(provider => TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(provider));
|
||||
services.AddSingleton<ITelemetryDataCache<TelemetryDataSpinDto>>(provider => TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(provider));
|
||||
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
|
||||
services.AddSingleton<PeriodicBackgroundWorker>();
|
||||
services.AddSingleton<BackgroundWorker>();
|
||||
services.AddSingleton<NotificationBackgroundWorker>();
|
||||
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
|
||||
|
@ -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<int> 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 &&
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
||||
await instance.InitializeCacheFromDBAsync<TEntity>(db, onProgress, token);
|
||||
});
|
||||
work.Timeout = TimeSpan.FromMinutes(15);
|
||||
worker.WorkStore.RunOnceQueue.Enqueue(work);
|
||||
worker.Enqueue(work);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
@ -55,10 +55,12 @@ public class WorkSubsystemOperationTimeCalc : Work
|
||||
{
|
||||
IdTelemetry = outer,
|
||||
inner.SingleOrDefault()?.LastDate,
|
||||
});
|
||||
})
|
||||
.OrderByDescending(i => i.IdTelemetry);
|
||||
|
||||
var count = telemetryLastDetectedDates.Count();
|
||||
var i = 0d;
|
||||
|
||||
foreach (var item in telemetryLastDetectedDates)
|
||||
{
|
||||
onProgressCallback($"Start handling telemetry: {item.IdTelemetry} from {item.LastDate}", i++ / count);
|
||||
|
@ -82,7 +82,7 @@ public class WellInfoService
|
||||
}
|
||||
|
||||
var wellOperationsStat = operationsStat.FirstOrDefault(s => s.Id == well.Id);
|
||||
var wellLastFactSection = wellOperationsStat?.Sections.LastOrDefault(s => s.Fact is not null);
|
||||
var wellLastFactSection = wellOperationsStat?.Sections.OrderBy(s => s.Fact?.WellDepthStart).LastOrDefault(s => s.Fact is not null);
|
||||
currentDepth ??= wellLastFactSection?.Fact?.WellDepthEnd;
|
||||
|
||||
var wellProcessMaps = processMapPlanWellDrillings
|
||||
|
@ -1,8 +1,6 @@
|
||||
using AsbCloudApp.Services;
|
||||
using AsbCloudDb.Model;
|
||||
using AsbCloudDb.Model;
|
||||
using AsbCloudInfrastructure.Services.DetectOperations;
|
||||
using AsbCloudInfrastructure.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
@ -10,10 +8,7 @@ using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using AsbCloudInfrastructure.Background;
|
||||
using AsbCloudApp.Data.SAUB;
|
||||
using AsbCloudInfrastructure.Services.SAUB;
|
||||
using AsbCloudInfrastructure.Services.Subsystems;
|
||||
using System.Linq;
|
||||
using DocumentFormat.OpenXml.InkML;
|
||||
using AsbCloudDb;
|
||||
using AsbCloudApp.Repositories;
|
||||
|
||||
@ -33,12 +28,12 @@ namespace AsbCloudInfrastructure
|
||||
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSaubDto>>();
|
||||
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSpinDto>>();
|
||||
|
||||
var backgroundWorker = provider.GetRequiredService<BackgroundWorker>();
|
||||
backgroundWorker.WorkStore.AddPeriodic<WellInfoService.WorkWellInfoUpdate>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.WorkStore.AddPeriodic<WorkOperationDetection>(TimeSpan.FromMinutes(15));
|
||||
backgroundWorker.WorkStore.AddPeriodic<WorkSubsystemOperationTimeCalc>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.WorkStore.AddPeriodic<WorkLimitingParameterCalc>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
|
||||
var backgroundWorker = provider.GetRequiredService<PeriodicBackgroundWorker>();
|
||||
backgroundWorker.Add<WellInfoService.WorkWellInfoUpdate>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.Add<WorkOperationDetection>(TimeSpan.FromMinutes(15));
|
||||
backgroundWorker.Add<WorkSubsystemOperationTimeCalc>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.Add<WorkLimitingParameterCalc>(TimeSpan.FromMinutes(30));
|
||||
backgroundWorker.Add(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
|
||||
|
||||
var notificationBackgroundWorker = provider.GetRequiredService<NotificationBackgroundWorker>();
|
||||
|
||||
|
@ -53,11 +53,21 @@ namespace AsbCloudWebApi.Tests.Middlware
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public DatesRangeDto? GetRange(int idWell)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<TelemetryDataSaubStatDto>> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException();
|
||||
|
||||
public Task<Stream> GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token)
|
||||
|
113
AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs
Normal file
113
AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs
Normal file
@ -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<IServiceProvider, ISupportRequiredService>();
|
||||
var serviceScope = Substitute.For<IServiceScope>();
|
||||
var serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
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<string, double?> 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<string, double?> callback, CancellationToken token)
|
||||
{
|
||||
result = expectadResult;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
var goodWork = Work.CreateByDelegate("", workAction);
|
||||
|
||||
Task failAction(string id, IServiceProvider services, Action<string, double?> 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<string, double?> 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);
|
||||
}
|
||||
}
|
@ -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<Work>()));
|
||||
backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny<Work>()));
|
||||
}
|
||||
|
||||
[Fact]
|
@ -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<IServiceProvider, ISupportRequiredService>();
|
||||
var serviceScope = Substitute.For<IServiceScope>();
|
||||
var serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
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<string, double?> 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<string, double?> callback, CancellationToken token)
|
||||
{
|
||||
result = expectadResult;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
var goodWork = Work.CreateByDelegate("", workAction);
|
||||
|
||||
Task failAction(string id, IServiceProvider services, Action<string, double?> 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);
|
||||
}
|
||||
}
|
150
AsbCloudWebApi.Tests/Services/WorkTest.cs
Normal file
150
AsbCloudWebApi.Tests/Services/WorkTest.cs
Normal file
@ -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<IServiceProvider, ISupportRequiredService>();
|
||||
var serviceScope = Substitute.For<IServiceScope>();
|
||||
var serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
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<string, double?> 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<string, double?> 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<string, double?> 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<string, double?> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<IActionResult> RestartAsync(CancellationToken token)
|
||||
{
|
||||
await backgroundWorker.StopAsync(token);
|
||||
await backgroundWorker.StartAsync(token);
|
||||
await worker.StopAsync(token);
|
||||
await worker.StartAsync(token);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
@ -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<IActionResult> RestartAsync(CancellationToken token)
|
||||
{
|
||||
await worker.StopAsync(token);
|
||||
await worker.StartAsync(token);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user