forked from ddrilling/AsbCloudServer
Merge branch 'dev' into feature/daily_report
# Conflicts: # AsbCloudWebApi.Tests/Services/DailyReportServiceTest.cs
This commit is contained in:
commit
a757754776
@ -172,7 +172,7 @@ namespace AsbCloudApp.Data
|
|||||||
if (progress.HasValue)
|
if (progress.HasValue)
|
||||||
CurrentState.Progress = progress.Value;
|
CurrentState.Progress = progress.Value;
|
||||||
|
|
||||||
Trace.TraceInformation($"{WorkNameForTrace} state: {newState}");
|
Trace.TraceInformation($"{WorkNameForTrace} state[{100*progress:#}%]: {newState}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -21,7 +21,7 @@ namespace AsbCloudApp.Data.SAUB
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// id категории события
|
/// id категории события
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Range(1, int.MaxValue, ErrorMessage = "Id категории события не может быть отрицательным")]
|
[Range(0, int.MaxValue, ErrorMessage = "Id категории события не может быть отрицательным")]
|
||||||
public int IdCategory { get; set; }
|
public int IdCategory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -32,7 +32,7 @@ namespace AsbCloudApp.Data.SAUB
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// тип определения наступления события
|
/// тип определения наступления события
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Range(1, int.MaxValue, ErrorMessage = "Id типа события не может быть отрицательным")]
|
[Range(0, int.MaxValue, ErrorMessage = "Id типа события не может быть отрицательным")]
|
||||||
public int EventType { get; set; }
|
public int EventType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -44,4 +44,5 @@ public class SectionByOperationsDto
|
|||||||
/// Дата после завершения последней операции операции в секции
|
/// Дата после завершения последней операции операции в секции
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset DateEnd { get; set; }
|
public DateTimeOffset DateEnd { get; set; }
|
||||||
|
public string Caption { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,13 @@ namespace AsbCloudApp.Repositories
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IEnumerable<TDto>? GetOrDefault(int idTelemetry, TelemetryDataRequest request);
|
IEnumerable<TDto>? GetOrDefault(int idTelemetry, TelemetryDataRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Диапазон дат находящийся в кеше
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idTelemetry"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
DatesRangeDto? GetOrDefaultCachedaDateRange(int idTelemetry);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить диапазон дат телеметрии.
|
/// Получить диапазон дат телеметрии.
|
||||||
/// Дата первой записи телеметрии храниться отдельно и запоняется при инициализации
|
/// Дата первой записи телеметрии храниться отдельно и запоняется при инициализации
|
||||||
|
@ -84,7 +84,7 @@ namespace AsbCloudApp.Requests
|
|||||||
{
|
{
|
||||||
if (LtDepth < GtDepth)
|
if (LtDepth < GtDepth)
|
||||||
yield return new ValidationResult(
|
yield return new ValidationResult(
|
||||||
$"{nameof(LtDepth)} должно быть больше {nameof(GtDepth)}. ({LtDepth:O} < {GtDepth:O})",
|
$"{nameof(LtDepth)} должно быть больше {nameof(GtDepth)}. ({LtDepth} < {GtDepth})",
|
||||||
new[] { nameof(LtDepth), nameof(GtDepth) });
|
new[] { nameof(LtDepth), nameof(GtDepth) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,13 +37,21 @@ namespace AsbCloudApp.Services
|
|||||||
Task<IEnumerable<TDto>> GetAsync(int idWell, TelemetryDataRequest request, CancellationToken token);
|
Task<IEnumerable<TDto>> GetAsync(int idWell, TelemetryDataRequest request, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получение периода за период
|
/// Период за который есть данные по скважине в рамках временного интервала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell"></param>
|
/// <param name="idWell"></param>
|
||||||
/// <param name="start"></param>
|
/// <param name="geDate"></param>
|
||||||
/// <param name="end"></param>
|
/// <param name="leDate"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
DatesRangeDto? GetRange(int idWell, DateTimeOffset start, DateTimeOffset end);
|
Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Период за который есть данные по скважине
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idWell"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
DatesRangeDto? GetRange(int idWell);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// добавить/изменить данные тех. процесса (используется панелью)
|
/// добавить/изменить данные тех. процесса (используется панелью)
|
||||||
|
8814
AsbCloudDb/Migrations/20231101110412_Update_EntityFillerSubsystem.Designer.cs
generated
Normal file
8814
AsbCloudDb/Migrations/20231101110412_Update_EntityFillerSubsystem.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AsbCloudDb.Migrations
|
||||||
|
{
|
||||||
|
public partial class Update_EntityFillerSubsystem : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_subsystem",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 65536,
|
||||||
|
columns: new[] { "description", "name" },
|
||||||
|
values: new object[] { "Осцилляция", "Осцилляция" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_subsystem",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 65537,
|
||||||
|
columns: new[] { "description", "name" },
|
||||||
|
values: new object[] { "Демпфер", "Демпфер" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_subsystem",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 65536,
|
||||||
|
columns: new[] { "description", "name" },
|
||||||
|
values: new object[] { "Spin master", "Spin master" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_subsystem",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 65537,
|
||||||
|
columns: new[] { "description", "name" },
|
||||||
|
values: new object[] { "Torque master", "Torque master" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8815
AsbCloudDb/Migrations/20231102045101_Rename_Field_IsContact_In_CompanyType.Designer.cs
generated
Normal file
8815
AsbCloudDb/Migrations/20231102045101_Rename_Field_IsContact_In_CompanyType.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AsbCloudDb.Migrations
|
||||||
|
{
|
||||||
|
public partial class Rename_Field_IsContact_In_CompanyType : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "IsContact",
|
||||||
|
table: "t_company_type",
|
||||||
|
newName: "is_contact");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "is_contact",
|
||||||
|
table: "t_company_type",
|
||||||
|
newName: "IsContact");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8857
AsbCloudDb/Migrations/20231102045600_Add_Or_Update_Data_In_CompanyType.Designer.cs
generated
Normal file
8857
AsbCloudDb/Migrations/20231102045600_Add_Or_Update_Data_In_CompanyType.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,101 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AsbCloudDb.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_Or_Update_Data_In_CompanyType : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "caption", "order" },
|
||||||
|
values: new object[] { "Недропользователь", 3 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 2,
|
||||||
|
columns: new[] { "is_contact", "order" },
|
||||||
|
values: new object[] { true, 2 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 3,
|
||||||
|
columns: new[] { "is_contact", "order" },
|
||||||
|
values: new object[] { true, 0 });
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (4, 'Сервис по ГТИ', true, 6) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Сервис по ГТИ', is_contact=true, ""order""=6;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (5, 'Растворный сервис', true, 4) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Растворный сервис', is_contact=true, ""order""=4;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (6, 'Сервис по ННБ', true, 5) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Сервис по ННБ', is_contact=true, ""order""=5;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (7, 'Служба супервайзинга', true, 1) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Служба супервайзинга', is_contact=true, ""order""=1;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (9, 'Сервис по цементированию', true, 7) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Сервис по цементированию', is_contact=true, ""order""=7;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (11, 'Дизельный сервис', false, 9) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Дизельный сервис', is_contact=false, ""order""=9;");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(@"INSERT INTO public.t_company_type (id, caption, is_contact, ""order"") " +
|
||||||
|
@"VALUES (12, 'Сервис по обслуживанию верхних силовых приводов', true, 8) " +
|
||||||
|
@"ON CONFLICT (id) DO UPDATE SET caption='Сервис по обслуживанию верхних силовых приводов', is_contact=false, ""order""=8;");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 4);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 5);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 6);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 7);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 9);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 12);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "t_company_type",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "caption", "order" },
|
||||||
|
values: new object[] { "Недрапользователь", 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -120,7 +120,8 @@ namespace AsbCloudDb.Migrations
|
|||||||
.HasColumnName("caption");
|
.HasColumnName("caption");
|
||||||
|
|
||||||
b.Property<bool>("IsContact")
|
b.Property<bool>("IsContact")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_contact");
|
||||||
|
|
||||||
b.Property<int>("Order")
|
b.Property<int>("Order")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
@ -134,9 +135,9 @@ namespace AsbCloudDb.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
Caption = "Недрапользователь",
|
Caption = "Недропользователь",
|
||||||
IsContact = false,
|
IsContact = false,
|
||||||
Order = 1
|
Order = 3
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
@ -151,6 +152,48 @@ namespace AsbCloudDb.Migrations
|
|||||||
Caption = "Сервис автоматизации бурения",
|
Caption = "Сервис автоматизации бурения",
|
||||||
IsContact = false,
|
IsContact = false,
|
||||||
Order = 0
|
Order = 0
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
Caption = "Сервис по ГТИ",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 6
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 5,
|
||||||
|
Caption = "Растворный сервис",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 4
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 6,
|
||||||
|
Caption = "Сервис по ННБ",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 5
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 7,
|
||||||
|
Caption = "Служба супервайзинга",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 1
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 9,
|
||||||
|
Caption = "Сервис по цементированию",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 7
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 12,
|
||||||
|
Caption = "Сервис по обслуживанию верхних силовых приводов",
|
||||||
|
IsContact = true,
|
||||||
|
Order = 7
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4596,14 +4639,14 @@ namespace AsbCloudDb.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 65536,
|
Id = 65536,
|
||||||
Description = "Spin master",
|
Description = "Осцилляция",
|
||||||
Name = "Spin master"
|
Name = "Осцилляция"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 65537,
|
Id = 65537,
|
||||||
Description = "Torque master",
|
Description = "Демпфер",
|
||||||
Name = "Torque master"
|
Name = "Демпфер"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ namespace AsbCloudDb.Model
|
|||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
if (!optionsBuilder.IsConfigured)
|
if (!optionsBuilder.IsConfigured)
|
||||||
optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True"
|
optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True;Include Error Detail=True;"
|
||||||
//, builder=>builder.EnableRetryOnFailure(2, System.TimeSpan.FromMinutes(1))
|
//, builder=>builder.EnableRetryOnFailure(2, System.TimeSpan.FromMinutes(1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ namespace AsbCloudDb.Model
|
|||||||
[Column("caption")]
|
[Column("caption")]
|
||||||
[StringLength(255)]
|
[StringLength(255)]
|
||||||
public string Caption { get; set; } = null!;
|
public string Caption { get; set; } = null!;
|
||||||
|
|
||||||
|
[Column("is_contact")]
|
||||||
public bool IsContact { get; set; }
|
public bool IsContact { get; set; }
|
||||||
|
|
||||||
[Column("order")]
|
[Column("order")]
|
||||||
|
@ -3,9 +3,16 @@
|
|||||||
internal class EntityFillerCompanyType : EntityFiller<CompanyType>
|
internal class EntityFillerCompanyType : EntityFiller<CompanyType>
|
||||||
{
|
{
|
||||||
public override CompanyType[] GetData() => new CompanyType[] {
|
public override CompanyType[] GetData() => new CompanyType[] {
|
||||||
new (){ Id = 1, Caption = "Недрапользователь", Order = 1 },
|
new (){ Id = 1, Caption = "Недропользователь", IsContact = true, Order = 3 },
|
||||||
new (){ Id = 2, Caption = "Буровой подрядчик", Order = 2 },
|
new (){ Id = 2, Caption = "Буровой подрядчик", IsContact = true, Order = 2 },
|
||||||
new (){ Id = 3, Caption = "Сервис автоматизации бурения", Order = 0 }
|
new (){ Id = 3, Caption = "Сервис автоматизации бурения", IsContact = true, Order = 0 },
|
||||||
|
new (){ Id = 4, Caption = "Сервис по ГТИ", IsContact = true, Order = 6 },
|
||||||
|
new (){ Id = 5, Caption = "Растворный сервис", IsContact = true, Order = 4 },
|
||||||
|
new (){ Id = 6, Caption = "Сервис по ННБ", IsContact = true, Order = 5 },
|
||||||
|
new (){ Id = 7, Caption = "Служба супервайзинга", Order = 1 },
|
||||||
|
new (){ Id = 9, Caption = "Сервис по цементированию", IsContact = true, Order = 7 },
|
||||||
|
new (){ Id = 11, Caption = "Дизельный сервис", IsContact = false, Order = 9 },
|
||||||
|
new (){ Id = 12, Caption = "Сервис по обслуживанию верхних силовых приводов", IsContact = true, Order = 8 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ namespace AsbCloudDb.Model.DefaultData
|
|||||||
new () {Id = 12, Name = "АПД слайд", Description = "Режим работы \"Бурение в слайде\""},
|
new () {Id = 12, Name = "АПД слайд", Description = "Режим работы \"Бурение в слайде\""},
|
||||||
new () {Id = 2, Name = "MSE", Description = "Алгоритм поиска оптимальных параметров бурения САУБ"},
|
new () {Id = 2, Name = "MSE", Description = "Алгоритм поиска оптимальных параметров бурения САУБ"},
|
||||||
//Spin master - id подсистем с 65_536 до 131_071
|
//Spin master - id подсистем с 65_536 до 131_071
|
||||||
new () {Id = 65536, Name = "Spin master", Description = "Spin master"},
|
new () {Id = 65536, Name = "Осцилляция", Description = "Осцилляция"},
|
||||||
new () {Id = 65537, Name = "Torque master", Description = "Torque master"}
|
new () {Id = 65537, Name = "Демпфер", Description = "Демпфер"}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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,6 +1,9 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -11,13 +14,39 @@ namespace AsbCloudInfrastructure.Background;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BackgroundWorker : BackgroundService
|
public class BackgroundWorker : BackgroundService
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10);
|
private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1);
|
||||||
private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2);
|
|
||||||
private readonly IServiceProvider serviceProvider;
|
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;
|
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)
|
public BackgroundWorker(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
this.serviceProvider = serviceProvider;
|
this.serviceProvider = serviceProvider;
|
||||||
@ -25,25 +54,64 @@ public class BackgroundWorker : BackgroundService
|
|||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken token)
|
protected override async Task ExecuteAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested && works.TryDequeue(out CurrentWork))
|
||||||
{
|
{
|
||||||
var work = WorkStore.GetNext();
|
try
|
||||||
if (work is null)
|
|
||||||
{
|
{
|
||||||
await Task.Delay(executePeriod, token);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentWork = work;
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
|
||||||
var result = await work.Start(scope.ServiceProvider, token);
|
var result = await CurrentWork.Start(scope.ServiceProvider, token);
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
WorkStore.Felled.Add(work);
|
Felled.Add(CurrentWork);
|
||||||
|
else
|
||||||
|
Done.Add(CurrentWork);
|
||||||
|
|
||||||
CurrentWork = null;
|
CurrentWork = null;
|
||||||
await Task.Delay(minDelay, token);
|
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>
|
||||||
|
/// Добавить в очередь
|
||||||
|
/// <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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
115
AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
Normal file
115
AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -12,6 +13,8 @@ namespace AsbCloudInfrastructure.Background;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Work : BackgroundWorkDto
|
public abstract class Work : BackgroundWorkDto
|
||||||
{
|
{
|
||||||
|
private CancellationTokenSource? stoppingCts;
|
||||||
|
|
||||||
private sealed class WorkBase : Work
|
private sealed class WorkBase : Work
|
||||||
{
|
{
|
||||||
private Func<string, IServiceProvider, Action<string, double?>, CancellationToken, Task> ActionAsync { get; }
|
private Func<string, IServiceProvider, Action<string, double?>, CancellationToken, Task> ActionAsync { get; }
|
||||||
@ -68,8 +71,9 @@ public abstract class Work : BackgroundWorkDto
|
|||||||
SetStatusStart();
|
SetStatusStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var task = Action(Id, services, UpdateStatus, token);
|
stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
await task.WaitAsync(Timeout, token);
|
var task = Action(Id, services, UpdateStatus, stoppingCts.Token);
|
||||||
|
await task.WaitAsync(Timeout, stoppingCts.Token);
|
||||||
SetStatusComplete();
|
SetStatusComplete();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -78,16 +82,29 @@ public abstract class Work : BackgroundWorkDto
|
|||||||
var message = FormatExceptionMessage(exception);
|
var message = FormatExceptionMessage(exception);
|
||||||
SetLastError(message);
|
SetLastError(message);
|
||||||
if (OnErrorAsync is not null)
|
if (OnErrorAsync is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var task = Task.Run(
|
var task = Task.Run(
|
||||||
async () => await OnErrorAsync(Id, exception, token),
|
async () => await OnErrorAsync(Id, exception, token),
|
||||||
token);
|
token);
|
||||||
await task.WaitAsync(OnErrorHandlerTimeout, token);
|
await task.WaitAsync(OnErrorHandlerTimeout, token);
|
||||||
}
|
}
|
||||||
|
catch (Exception onErrorAsyncException)
|
||||||
|
{
|
||||||
|
var message2 = FormatExceptionMessage(onErrorAsyncException);
|
||||||
|
Trace.TraceError($"Backgroud work:\"{Id}\" OnError handler throws exception: {message2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
stoppingCts?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatExceptionMessage(Exception exception)
|
private static string FormatExceptionMessage(Exception exception)
|
||||||
{
|
{
|
||||||
var firstException = FirstException(exception);
|
var firstException = FirstException(exception);
|
||||||
|
@ -1,105 +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>
|
|
||||||
/// Завершившиеся с ошибкой
|
|
||||||
/// </summary>
|
|
||||||
public CyclycArray<Work> Felled { 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -163,6 +163,7 @@ namespace AsbCloudInfrastructure
|
|||||||
services.AddSingleton<ITelemetryDataCache<TelemetryDataSaubDto>>(provider => TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(provider));
|
services.AddSingleton<ITelemetryDataCache<TelemetryDataSaubDto>>(provider => TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(provider));
|
||||||
services.AddSingleton<ITelemetryDataCache<TelemetryDataSpinDto>>(provider => TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(provider));
|
services.AddSingleton<ITelemetryDataCache<TelemetryDataSpinDto>>(provider => TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(provider));
|
||||||
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
|
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
|
||||||
|
services.AddSingleton<PeriodicBackgroundWorker>();
|
||||||
services.AddSingleton<BackgroundWorker>();
|
services.AddSingleton<BackgroundWorker>();
|
||||||
services.AddSingleton<NotificationBackgroundWorker>();
|
services.AddSingleton<NotificationBackgroundWorker>();
|
||||||
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
|
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
|
||||||
|
@ -128,12 +128,14 @@ public class WellOperationRepository : IWellOperationRepository
|
|||||||
operation.IdWell,
|
operation.IdWell,
|
||||||
operation.IdType,
|
operation.IdType,
|
||||||
operation.IdWellSectionType,
|
operation.IdWellSectionType,
|
||||||
|
operation.WellSectionType.Caption,
|
||||||
})
|
})
|
||||||
.Select(group => new
|
.Select(group => new
|
||||||
{
|
{
|
||||||
group.Key.IdWell,
|
group.Key.IdWell,
|
||||||
group.Key.IdType,
|
group.Key.IdType,
|
||||||
group.Key.IdWellSectionType,
|
group.Key.IdWellSectionType,
|
||||||
|
group.Key.Caption,
|
||||||
|
|
||||||
First = group
|
First = group
|
||||||
.OrderBy(operation => operation.DateStart)
|
.OrderBy(operation => operation.DateStart)
|
||||||
@ -162,6 +164,8 @@ public class WellOperationRepository : IWellOperationRepository
|
|||||||
IdType = item.IdType,
|
IdType = item.IdType,
|
||||||
IdWellSectionType = item.IdWellSectionType,
|
IdWellSectionType = item.IdWellSectionType,
|
||||||
|
|
||||||
|
Caption = item.Caption,
|
||||||
|
|
||||||
DateStart = item.First.DateStart,
|
DateStart = item.First.DateStart,
|
||||||
DepthStart = item.First.DepthStart,
|
DepthStart = item.First.DepthStart,
|
||||||
|
|
||||||
|
@ -513,7 +513,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
|
|||||||
if (state.IdState == idStateCreating)
|
if (state.IdState == idStateCreating)
|
||||||
{
|
{
|
||||||
var workId = MakeWorkId(idWell);
|
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 well = (await wellService.GetOrDefaultAsync(idWell, token))!;
|
||||||
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf";
|
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf";
|
||||||
@ -542,7 +542,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
|
|||||||
|
|
||||||
var work = Work.CreateByDelegate(workId, workAction);
|
var work = Work.CreateByDelegate(workId, workAction);
|
||||||
work.OnErrorAsync = onErrorAction;
|
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)
|
private async Task<int> RemoveDrillingProgramAsync(int idWell, CancellationToken token)
|
||||||
{
|
{
|
||||||
var workId = MakeWorkId(idWell);
|
var workId = MakeWorkId(idWell);
|
||||||
backgroundWorker.WorkStore.TryRemoveFromRunOnceQueue(workId);
|
backgroundWorker.TryRemoveFromQueue(workId);
|
||||||
|
|
||||||
var filesIds = await context.Files
|
var filesIds = await context.Files
|
||||||
.Where(f => f.IdWell == idWell &&
|
.Where(f => f.IdWell == idWell &&
|
||||||
|
@ -52,12 +52,12 @@ namespace AsbCloudInfrastructure.Services.Email
|
|||||||
}
|
}
|
||||||
|
|
||||||
var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message);
|
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 workAction = MakeEmailSendWorkAction(notification);
|
||||||
|
|
||||||
var work = Work.CreateByDelegate(workId, workAction);
|
var work = Work.CreateByDelegate(workId, workAction);
|
||||||
backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work);
|
backgroundWorker.Enqueue(work);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -95,7 +95,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
var work = Work.CreateByDelegate(workId, workAction);
|
var work = Work.CreateByDelegate(workId, workAction);
|
||||||
backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work);
|
backgroundWorkerService.Enqueue(work);
|
||||||
|
|
||||||
progressHandler.Invoke(new ReportProgressDto
|
progressHandler.Invoke(new ReportProgressDto
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Exceptions;
|
||||||
using AsbCloudApp.Repositories;
|
using AsbCloudApp.Repositories;
|
||||||
using AsbCloudApp.Services;
|
using AsbCloudApp.Services;
|
||||||
using AsbCloudDb;
|
using AsbCloudDb;
|
||||||
@ -204,28 +205,62 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public DatesRangeDto? GetRange(int idWell, DateTimeOffset start, DateTimeOffset end)
|
public async Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token)
|
||||||
|
{
|
||||||
|
var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell)
|
||||||
|
?? throw new ArgumentInvalidException(nameof(idWell), $"По скважине id:{idWell} нет телеметрии");
|
||||||
|
|
||||||
|
if ((DateTimeOffset.UtcNow - geDate) < TimeSpan.FromHours(12))
|
||||||
|
{
|
||||||
|
// пробуем обойтись кешем
|
||||||
|
var cechedRange = telemetryDataCache.GetOrDefaultCachedaDateRange(telemetry.Id);
|
||||||
|
if (cechedRange?.From <= geDate)
|
||||||
|
{
|
||||||
|
var datesRange = new DatesRangeDto
|
||||||
|
{
|
||||||
|
From = geDate.DateTime,
|
||||||
|
To = cechedRange.To
|
||||||
|
};
|
||||||
|
if (leDate.HasValue && leDate > geDate)
|
||||||
|
datesRange.To = leDate.Value.Date;
|
||||||
|
return datesRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = db.Set<TEntity>()
|
||||||
|
.Where(entity => entity.IdTelemetry == telemetry.Id)
|
||||||
|
.Where(entity => entity.DateTime >= geDate.ToUniversalTime());
|
||||||
|
|
||||||
|
if(leDate.HasValue)
|
||||||
|
query = query.Where(entity => entity.DateTime <= leDate.Value.ToUniversalTime());
|
||||||
|
|
||||||
|
var gquery = query
|
||||||
|
.GroupBy(entity => entity.IdTelemetry)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
MinDate = group.Min(entity => entity.DateTime),
|
||||||
|
MaxDate = group.Max(entity => entity.DateTime),
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await gquery.FirstOrDefaultAsync(token);
|
||||||
|
if (result is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var range = new DatesRangeDto
|
||||||
|
{
|
||||||
|
From = result.MinDate.ToOffset(TimeSpan.FromHours(telemetry.TimeZone!.Hours)).DateTime,
|
||||||
|
To = result.MaxDate.ToOffset(TimeSpan.FromHours(telemetry.TimeZone!.Hours)).DateTime,
|
||||||
|
};
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatesRangeDto? GetRange(int idWell)
|
||||||
{
|
{
|
||||||
var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
|
var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
|
||||||
if (telemetry is null)
|
if (telemetry is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var datesRange = telemetryDataCache.GetOrDefaultDataDateRange(telemetry.Id);
|
return telemetryDataCache.GetOrDefaultDataDateRange(telemetry.Id);
|
||||||
|
|
||||||
if (datesRange is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var from = datesRange.From > start.DateTime
|
|
||||||
? datesRange.From : start.DateTime;
|
|
||||||
|
|
||||||
var to = datesRange.To < end.DateTime
|
|
||||||
? datesRange.To : end.DateTime;
|
|
||||||
|
|
||||||
return new DatesRangeDto
|
|
||||||
{
|
|
||||||
From = from,
|
|
||||||
To = to,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract TDto Convert(TEntity src, double timezoneOffset);
|
public abstract TDto Convert(TEntity src, double timezoneOffset);
|
||||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Mapster;
|
using Mapster;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using AsbCloudInfrastructure.Background;
|
using AsbCloudInfrastructure.Background;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -25,7 +24,6 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
public double TimezoneHours { get; init; } = 5;
|
public double TimezoneHours { get; init; } = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IServiceProvider provider = null!;
|
|
||||||
private const int activeWellCapacity = 12 * 60 * 60;
|
private const int activeWellCapacity = 12 * 60 * 60;
|
||||||
private const int doneWellCapacity = 65 * 60;
|
private const int doneWellCapacity = 65 * 60;
|
||||||
|
|
||||||
@ -54,9 +52,8 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
await instance.InitializeCacheFromDBAsync<TEntity>(db, onProgress, token);
|
await instance.InitializeCacheFromDBAsync<TEntity>(db, onProgress, token);
|
||||||
});
|
});
|
||||||
work.Timeout = TimeSpan.FromMinutes(15);
|
work.Timeout = TimeSpan.FromMinutes(15);
|
||||||
worker.WorkStore.RunOnceQueue.Enqueue(work);
|
worker.Enqueue(work);
|
||||||
}
|
}
|
||||||
instance.provider = provider;
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,10 +67,9 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
if (!range.Any())
|
if (!range.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var newItems = range
|
range = range.OrderBy(x => x.DateTime);
|
||||||
.OrderBy(i => i.DateTime);
|
|
||||||
|
|
||||||
foreach (var item in newItems)
|
foreach (var item in range)
|
||||||
item.IdTelemetry = idTelemetry;
|
item.IdTelemetry = idTelemetry;
|
||||||
|
|
||||||
TelemetryDataCacheItem cacheItem;
|
TelemetryDataCacheItem cacheItem;
|
||||||
@ -88,12 +84,12 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
{
|
{
|
||||||
cacheItem = caches.GetOrAdd(idTelemetry, _ => new TelemetryDataCacheItem()
|
cacheItem = caches.GetOrAdd(idTelemetry, _ => new TelemetryDataCacheItem()
|
||||||
{
|
{
|
||||||
FirstByDate = newItems.ElementAt(0),
|
FirstByDate = range.ElementAt(0),
|
||||||
LastData = new CyclycArray<TDto>(activeWellCapacity)
|
LastData = new CyclycArray<TDto>(activeWellCapacity)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheItem.LastData.AddRange(newItems);
|
cacheItem.LastData.AddRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -151,6 +147,20 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
return new DatesRangeDto { From = from.Value, To = to };
|
return new DatesRangeDto { From = from.Value, To = to };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DatesRangeDto? GetOrDefaultCachedaDateRange(int idTelemetry)
|
||||||
|
{
|
||||||
|
if (!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (cacheItem.LastData.Count < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var to = cacheItem.LastData[^1].DateTime;
|
||||||
|
var from = cacheItem.LastData[0].DateTime;
|
||||||
|
|
||||||
|
return new DatesRangeDto { From = from, To = to };
|
||||||
|
}
|
||||||
|
|
||||||
public (TDto First, TDto Last)? GetOrDefaultFirstLast(int idTelemetry)
|
public (TDto First, TDto Last)? GetOrDefaultFirstLast(int idTelemetry)
|
||||||
{
|
{
|
||||||
if (!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))
|
if (!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))
|
||||||
@ -167,14 +177,16 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
private async Task InitializeCacheFromDBAsync<TEntity>(IAsbCloudDbContext db, Action<string, double?> onProgress, CancellationToken token)
|
private async Task InitializeCacheFromDBAsync<TEntity>(IAsbCloudDbContext db, Action<string, double?> onProgress, CancellationToken token)
|
||||||
where TEntity : class, AsbCloudDb.Model.ITelemetryData
|
where TEntity : class, AsbCloudDb.Model.ITelemetryData
|
||||||
{
|
{
|
||||||
|
var defaultTimeout = db.Database.GetCommandTimeout();
|
||||||
|
db.Database.SetCommandTimeout(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
throw new Exception("Multiple cache loading detected.");
|
throw new Exception("Multiple cache loading detected.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
var defaultTimeout = db.Database.GetCommandTimeout();
|
|
||||||
db.Database.SetCommandTimeout(TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
Well[] wells = await db.Set<Well>()
|
Well[] wells = await db.Set<Well>()
|
||||||
.Include(well => well.Telemetry)
|
.Include(well => well.Telemetry)
|
||||||
.Include(well => well.Cluster)
|
.Include(well => well.Cluster)
|
||||||
@ -197,10 +209,13 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
if (cacheItem is not null)
|
if (cacheItem is not null)
|
||||||
caches.TryAdd(idTelemetry, cacheItem);
|
caches.TryAdd(idTelemetry, cacheItem);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
db.Database.SetCommandTimeout(defaultTimeout);
|
db.Database.SetCommandTimeout(defaultTimeout);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<TelemetryDataCacheItem?> GetOrDefaultCacheDataFromDbAsync<TEntity>(IAsbCloudDbContext db, int idTelemetry, int capacity, double hoursOffset, CancellationToken token)
|
private static async Task<TelemetryDataCacheItem?> GetOrDefaultCacheDataFromDbAsync<TEntity>(IAsbCloudDbContext db, int idTelemetry, int capacity, double hoursOffset, CancellationToken token)
|
||||||
where TEntity : class, AsbCloudDb.Model.ITelemetryData
|
where TEntity : class, AsbCloudDb.Model.ITelemetryData
|
||||||
|
@ -26,7 +26,7 @@ public class WorkSubsystemOperationTimeCalc : Work
|
|||||||
public WorkSubsystemOperationTimeCalc()
|
public WorkSubsystemOperationTimeCalc()
|
||||||
: base("Subsystem operation time calc")
|
: base("Subsystem operation time calc")
|
||||||
{
|
{
|
||||||
Timeout = TimeSpan.FromMinutes(20);
|
Timeout = TimeSpan.FromMinutes(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task Action(string id, IServiceProvider services, Action<string, double?> onProgressCallback, CancellationToken token)
|
protected override async Task Action(string id, IServiceProvider services, Action<string, double?> onProgressCallback, CancellationToken token)
|
||||||
|
@ -13,7 +13,7 @@ namespace AsbCloudInfrastructure.Services;
|
|||||||
|
|
||||||
public class WellboreService : IWellboreService
|
public class WellboreService : IWellboreService
|
||||||
{
|
{
|
||||||
const string WellboreNameFormat = "Ñòâîë {0}";
|
const string WellboreNameFormat = "Ствол {0}";
|
||||||
private readonly IWellService wellService;
|
private readonly IWellService wellService;
|
||||||
private readonly IWellOperationRepository wellOperationRepository;
|
private readonly IWellOperationRepository wellOperationRepository;
|
||||||
private readonly ITelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache;
|
private readonly ITelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache;
|
||||||
|
@ -33,12 +33,12 @@ namespace AsbCloudInfrastructure
|
|||||||
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSaubDto>>();
|
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSaubDto>>();
|
||||||
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSpinDto>>();
|
_ = provider.GetRequiredService<ITelemetryDataCache<TelemetryDataSpinDto>>();
|
||||||
|
|
||||||
var backgroundWorker = provider.GetRequiredService<BackgroundWorker>();
|
var backgroundWorker = provider.GetRequiredService<PeriodicBackgroundWorker>();
|
||||||
backgroundWorker.WorkStore.AddPeriodic<WellInfoService.WorkWellInfoUpdate>(TimeSpan.FromMinutes(30));
|
backgroundWorker.Add<WellInfoService.WorkWellInfoUpdate>(TimeSpan.FromMinutes(30));
|
||||||
backgroundWorker.WorkStore.AddPeriodic<WorkOperationDetection>(TimeSpan.FromMinutes(15));
|
backgroundWorker.Add<WorkOperationDetection>(TimeSpan.FromMinutes(15));
|
||||||
backgroundWorker.WorkStore.AddPeriodic<WorkSubsystemOperationTimeCalc>(TimeSpan.FromMinutes(30));
|
backgroundWorker.Add<WorkSubsystemOperationTimeCalc>(TimeSpan.FromMinutes(30));
|
||||||
backgroundWorker.WorkStore.AddPeriodic<WorkLimitingParameterCalc>(TimeSpan.FromMinutes(30));
|
backgroundWorker.Add<WorkLimitingParameterCalc>(TimeSpan.FromMinutes(30));
|
||||||
backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
|
backgroundWorker.Add(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1));
|
||||||
|
|
||||||
var notificationBackgroundWorker = provider.GetRequiredService<NotificationBackgroundWorker>();
|
var notificationBackgroundWorker = provider.GetRequiredService<NotificationBackgroundWorker>();
|
||||||
|
|
||||||
|
@ -53,11 +53,21 @@ namespace AsbCloudWebApi.Tests.Middlware
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DatesRangeDto? GetRange(int idWell)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token)
|
public Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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<IEnumerable<TelemetryDataSaubStatDto>> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<Stream> GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token)
|
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);
|
var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(2, state.IdState);
|
Assert.Equal(2, state.IdState);
|
||||||
backgroundWorkerMock.Verify(s => s.WorkStore.RunOnceQueue.Enqueue(It.IsAny<Work>()));
|
backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny<Work>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudDb.Model;
|
||||||
|
using AsbCloudInfrastructure.Background;
|
||||||
|
using AsbCloudInfrastructure.Services.SAUB;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AsbCloudWebApi.Tests.ServicesTests.SAUB;
|
||||||
|
|
||||||
|
public class TelemetryDataSaubCacheTests
|
||||||
|
{
|
||||||
|
private const int idTelemetry = 1;
|
||||||
|
|
||||||
|
private readonly IEnumerable<TelemetryDataSaubDto> fakeTelemetries = new[]
|
||||||
|
{
|
||||||
|
new TelemetryDataSaubDto()
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly IServiceProvider serviceProviderMock = Substitute.For<IServiceProvider>();
|
||||||
|
|
||||||
|
private readonly TelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache;
|
||||||
|
private readonly Type telemetryDataCacheType;
|
||||||
|
|
||||||
|
public TelemetryDataSaubCacheTests()
|
||||||
|
{
|
||||||
|
serviceProviderMock.GetService<BackgroundWorker>().Returns(new BackgroundWorker(serviceProviderMock));
|
||||||
|
|
||||||
|
telemetryDataCache = TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(serviceProviderMock);
|
||||||
|
|
||||||
|
telemetryDataCacheType = telemetryDataCache.GetType();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRange_ShouldReturn_AddedElementToCache()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
telemetryDataCacheType.GetField("isLoading", BindingFlags.NonPublic | BindingFlags.Instance)?.SetValue(telemetryDataCache, false);
|
||||||
|
|
||||||
|
//act
|
||||||
|
telemetryDataCache.AddRange(idTelemetry, fakeTelemetries);
|
||||||
|
var lastTelemetry = telemetryDataCache.GetLastOrDefault(idTelemetry);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(lastTelemetry, fakeTelemetries.Last());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRange_ShouldReturn_NotAddedToCache()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
telemetryDataCacheType.GetField("isLoading", BindingFlags.NonPublic | BindingFlags.Instance)?.SetValue(telemetryDataCache, true);
|
||||||
|
|
||||||
|
//act
|
||||||
|
telemetryDataCache.AddRange(idTelemetry, fakeTelemetries);
|
||||||
|
var lastTelemetry = telemetryDataCache.GetLastOrDefault(idTelemetry);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.NotEqual(lastTelemetry, fakeTelemetries.Last());
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,10 @@
|
|||||||
using AsbCloudInfrastructure.Background;
|
using AsbCloudInfrastructure.Background;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AsbCloudWebApi.Controllers
|
namespace AsbCloudWebApi.Controllers
|
||||||
{
|
{
|
||||||
@ -11,40 +14,56 @@ namespace AsbCloudWebApi.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class BackgroundWorkController : ControllerBase
|
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]
|
[HttpGet]
|
||||||
public IActionResult GetAll()
|
public IActionResult GetAll()
|
||||||
{
|
{
|
||||||
var result = new {
|
var result = new {
|
||||||
CurrentWork = (BackgroundWorkDto?)backgroundWorker.CurrentWork,
|
CurrentWork = (BackgroundWorkDto?)worker.CurrentWork,
|
||||||
RunOnceQueue = backgroundWorker.WorkStore.RunOnceQueue.Select(work => (BackgroundWorkDto)work),
|
worker.MainLoopLastException,
|
||||||
Periodics = backgroundWorker.WorkStore.Periodics.Select(work => (BackgroundWorkDto)work.Work),
|
RunOnceQueue = worker.Works.Select(work => (BackgroundWorkDto)work),
|
||||||
Felled = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work),
|
Done = worker.Done.Select(work => (BackgroundWorkDto)work),
|
||||||
|
Felled = worker.Felled.Select(work => (BackgroundWorkDto)work),
|
||||||
};
|
};
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Current")]
|
[HttpGet("current")]
|
||||||
public IActionResult GetCurrent()
|
public IActionResult GetCurrent()
|
||||||
{
|
{
|
||||||
var work = backgroundWorker.CurrentWork;
|
var work = worker.CurrentWork;
|
||||||
if (work == null)
|
if (work == null)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|
||||||
return Ok(work);
|
return Ok(work);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Failed")]
|
[HttpGet("failed")]
|
||||||
public IActionResult GetFelled()
|
public IActionResult GetFelled()
|
||||||
{
|
{
|
||||||
var result = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work);
|
var result = worker.Felled.Select(work => (BackgroundWorkDto)work);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("done")]
|
||||||
|
public IActionResult GetDone()
|
||||||
|
{
|
||||||
|
var result = worker.Done.Select(work => (BackgroundWorkDto)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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -131,15 +131,17 @@ namespace AsbCloudWebApi.Controllers.SAUB
|
|||||||
/// Возвращает диапазон дат за которые есть телеметрия за период времени
|
/// Возвращает диапазон дат за которые есть телеметрия за период времени
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell"></param>
|
/// <param name="idWell"></param>
|
||||||
/// <param name="start"></param>
|
/// <param name="geDate"></param>
|
||||||
/// <param name="end"></param>
|
/// <param name="leDate"></param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("{idWell}/dateRange")]
|
[HttpGet("{idWell}/dateRange")]
|
||||||
|
[ProducesResponseType(typeof(DatesRangeDto), (int)System.Net.HttpStatusCode.OK)]
|
||||||
|
[ProducesResponseType((int)System.Net.HttpStatusCode.NotFound)]
|
||||||
public virtual async Task<ActionResult<DatesRangeDto?>> GetRangeAsync(
|
public virtual async Task<ActionResult<DatesRangeDto?>> GetRangeAsync(
|
||||||
[FromRoute] int idWell,
|
[FromRoute] int idWell,
|
||||||
[Required] DateTimeOffset start,
|
[Required] DateTimeOffset geDate,
|
||||||
[Required] DateTimeOffset end,
|
DateTimeOffset? leDate,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
int? idCompany = User.GetCompanyId();
|
int? idCompany = User.GetCompanyId();
|
||||||
@ -153,7 +155,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
|
|||||||
if (!isCompanyOwnsWell)
|
if (!isCompanyOwnsWell)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var content = telemetryDataService.GetRange(idWell, start, end);
|
var content = await telemetryDataService.GetRangeAsync(idWell, geDate, leDate, token);
|
||||||
|
|
||||||
return Ok(content);
|
return Ok(content);
|
||||||
}
|
}
|
||||||
@ -167,7 +169,8 @@ namespace AsbCloudWebApi.Controllers.SAUB
|
|||||||
[HttpGet("{idWell}/datesRange")]
|
[HttpGet("{idWell}/datesRange")]
|
||||||
[Permission]
|
[Permission]
|
||||||
[ProducesResponseType(typeof(DatesRangeDto), (int)System.Net.HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(DatesRangeDto), (int)System.Net.HttpStatusCode.OK)]
|
||||||
public virtual async Task<ActionResult<DatesRangeDto>> GetDataDatesRangeAsync(int idWell,
|
[ProducesResponseType((int)System.Net.HttpStatusCode.NotFound)]
|
||||||
|
public virtual async Task<ActionResult<DatesRangeDto?>> GetDataDatesRangeAsync(int idWell,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
int? idCompany = User.GetCompanyId();
|
int? idCompany = User.GetCompanyId();
|
||||||
|
@ -68,7 +68,7 @@ namespace AsbCloudWebApi.Controllers.Subsystems
|
|||||||
if (!await UserHasAccesToWellAsync(idWell, token))
|
if (!await UserHasAccesToWellAsync(idWell, token))
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var dateRange = telemetryDataSaubService.GetRange(idWell, DateTimeOffset.MinValue, DateTimeOffset.MaxValue);
|
var dateRange = telemetryDataSaubService.GetRange(idWell);
|
||||||
|
|
||||||
return Ok(dateRange);
|
return Ok(dateRange);
|
||||||
}
|
}
|
||||||
|
59
AsbCloudWebApi/Controllers/WellSectionsController.cs
Normal file
59
AsbCloudWebApi/Controllers/WellSectionsController.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AsbCloudWebApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Секции скважины
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class WellSectionsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IWellOperationRepository wellOperationRepository;
|
||||||
|
|
||||||
|
public WellSectionsController(IWellOperationRepository wellOperationRepository)
|
||||||
|
{
|
||||||
|
this.wellOperationRepository = wellOperationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получение списка плановых секций скважин
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idsWells">Идентификаторы скважин</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("plan")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<SectionByOperationsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetPlanAsync([FromQuery] IEnumerable<int> idsWells,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sections = await wellOperationRepository.GetSectionsAsync(idsWells, cancellationToken);
|
||||||
|
sections = sections.Where(section => section.IdType == 0);
|
||||||
|
return Ok(sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получение списка фактических секций скважин
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idsWells">Идентификаторы скважин</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("fact")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<SectionByOperationsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetFactAsync([FromQuery] IEnumerable<int> idsWells,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sections = await wellOperationRepository.GetSectionsAsync(idsWells, cancellationToken);
|
||||||
|
sections = sections.Where(section => section.IdType == 1);
|
||||||
|
return Ok(sections);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
using AsbCloudApp.Exceptions;
|
|
||||||
using AsbCloudApp.Requests;
|
|
||||||
using AsbCloudApp.Services;
|
using AsbCloudApp.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Org.BouncyCastle.Asn1.Ocsp;
|
|
||||||
|
|
||||||
namespace AsbCloudWebApi.Controllers;
|
namespace AsbCloudWebApi.Controllers;
|
||||||
|
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudInfrastructure.Background;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AsbCloudWebApi.SignalR
|
namespace AsbCloudWebApi.SignalR
|
||||||
{
|
{
|
||||||
@ -8,6 +14,39 @@ namespace AsbCloudWebApi.SignalR
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class ReportsHub : BaseHub<IReportHubClient>
|
public class ReportsHub : BaseHub<IReportHubClient>
|
||||||
{
|
{
|
||||||
|
private readonly BackgroundWorker backgroundWorker;
|
||||||
|
|
||||||
|
public ReportsHub(BackgroundWorker backgroundWorker)
|
||||||
|
{
|
||||||
|
this.backgroundWorker = backgroundWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task AddToGroup(string groupName)
|
||||||
|
{
|
||||||
|
await base.AddToGroup(groupName);
|
||||||
|
|
||||||
|
var workId = groupName.Replace("Report_", "");
|
||||||
|
var work = backgroundWorker.Works.FirstOrDefault(work => work.Id == workId);
|
||||||
|
|
||||||
|
var progress = new ReportProgressDto()
|
||||||
|
{
|
||||||
|
Operation = "Ожидает начала в очереди.",
|
||||||
|
Progress = 0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
var state = work?.CurrentState;
|
||||||
|
if (state is not null)
|
||||||
|
{
|
||||||
|
progress.Operation = state.State;
|
||||||
|
progress.Progress = (float)state.Progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clients.Group(groupName).SendAsync(
|
||||||
|
nameof(IReportHubClient.GetReportProgress),
|
||||||
|
progress,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -29,12 +29,12 @@ public class SignalRNotificationTransportService : INotificationTransportService
|
|||||||
{
|
{
|
||||||
var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x");
|
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;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
var workAction = MakeSignalRSendWorkAction(notifications);
|
var workAction = MakeSignalRSendWorkAction(notifications);
|
||||||
var work = Work.CreateByDelegate(workId, workAction);
|
var work = Work.CreateByDelegate(workId, workAction);
|
||||||
backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work);
|
backgroundWorker.Enqueue(work);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user