Add campaign manager

This commit is contained in:
2025-10-27 10:02:58 +09:00
parent 891d462af1
commit 7c35c9a7df
18 changed files with 250 additions and 41 deletions

View File

@ -0,0 +1,55 @@
using Hcs.WebApp.BackgroundServices.CampaignManagers;
using Hcs.WebApp.Services;
namespace Hcs.WebApp.BackgroundServices
{
public class CampaignManagementService(
CampaignManagementState campaignManagementState,
ManagerFactory managerFactory,
IServiceScopeFactory scopeFactory) : BackgroundService
{
private const int SLEEP_TIME = 30000;
private readonly CampaignManagementState campaignManagementState = campaignManagementState;
private readonly ManagerFactory managerFactory = managerFactory;
private readonly IServiceScopeFactory scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await InitializeStateAsync();
while (!stoppingToken.IsCancellationRequested)
{
while (campaignManagementState.TryDequeueCampaign(out var campaign))
{
if (stoppingToken.IsCancellationRequested) return;
try
{
var manager = managerFactory.CreateManager(campaign);
await manager.StartAsync(stoppingToken);
}
catch
{
// TODO: Добавить таймауты
campaignManagementState.EnqueueCampaign(campaign);
}
}
await Task.Delay(SLEEP_TIME, stoppingToken);
}
}
private async Task InitializeStateAsync()
{
using var scope = scopeFactory.CreateScope();
var headquartersService = scope.ServiceProvider.GetRequiredService<HeadquartersService>();
var campaigns = await headquartersService.GetInitiatedCampaignAsync();
foreach (var campaign in campaigns)
{
campaignManagementState.EnqueueCampaign(campaign);
}
}
}
}

View File

@ -0,0 +1,27 @@
using Hcs.WebApp.Data.Hcs;
using System.Collections.Concurrent;
namespace Hcs.WebApp.BackgroundServices
{
public class CampaignManagementState
{
private readonly ConcurrentQueue<Campaign> campaigns = new();
public event Action<Campaign> OnCampaignStarted;
public void EnqueueCampaign(Campaign campaign)
{
campaigns.Enqueue(campaign);
}
public bool TryDequeueCampaign(out Campaign campaign)
{
return campaigns.TryDequeue(out campaign);
}
public void InvokeOnCampaignStarted(Campaign campaign)
{
OnCampaignStarted?.Invoke(campaign);
}
}
}

View File

@ -0,0 +1,12 @@
using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.CampaignManagers
{
public class ExportRequiredRegistryElementsManager_15_7_0_1(OperationExecutionState state, IServiceScopeFactory scopeFactory, Campaign campaign) : ManagerBase(state, scopeFactory, campaign)
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
// TODO
}
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.WebApp.BackgroundServices.CampaignManagers
{
public interface IManager
{
Task StartAsync(CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,13 @@
using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.CampaignManagers
{
public abstract class ManagerBase(OperationExecutionState state, IServiceScopeFactory scopeFactory, Campaign campaign) : IManager
{
protected readonly OperationExecutionState state = state;
protected readonly IServiceScopeFactory scopeFactory = scopeFactory;
protected readonly Campaign campaign = campaign;
public abstract Task StartAsync(CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,21 @@
using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.CampaignManagers
{
public class ManagerFactory(OperationExecutionState state, IServiceScopeFactory scopeFactory)
{
protected readonly OperationExecutionState state = state;
protected readonly IServiceScopeFactory scopeFactory = scopeFactory;
public IManager CreateManager(Campaign campaign)
{
switch (campaign.Type)
{
case Campaign.CampaignType.ExportRequiredRegistryElements_15_7_0_1:
return new ExportRequiredRegistryElementsManager_15_7_0_1(state, scopeFactory, campaign);
}
throw new NotImplementedException();
}
}
}

View File

@ -1,7 +1,7 @@
using Hcs.Broker; using Hcs.Broker;
using Hcs.Broker.Logger; using Hcs.Broker.Logger;
using Hcs.Broker.MessageCapturer; using Hcs.Broker.MessageCapturer;
using Hcs.WebApp.BackgroundServices.Executors; using Hcs.WebApp.BackgroundServices.OperationExecutors;
using Hcs.WebApp.Config; using Hcs.WebApp.Config;
using Hcs.WebApp.Services; using Hcs.WebApp.Services;
@ -26,12 +26,14 @@ namespace Hcs.WebApp.BackgroundServices
InitializeClient(); InitializeClient();
var scope = scopeFactory.CreateScope(); var scope = scopeFactory.CreateScope();
var operationService = scope.ServiceProvider.GetRequiredService<OperationService>(); var headquartersService = scope.ServiceProvider.GetRequiredService<HeadquartersService>();
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
while (state.TryDequeueOperation(out var operation)) while (state.TryDequeueOperation(out var operation))
{ {
if (stoppingToken.IsCancellationRequested) return;
var messageGuid = string.Empty; var messageGuid = string.Empty;
try try
{ {
@ -40,12 +42,13 @@ namespace Hcs.WebApp.BackgroundServices
} }
catch catch
{ {
// TODO: Добавить таймауты и макс количество попыток выполнения операции
state.EnqueueOperation(operation); state.EnqueueOperation(operation);
} }
if (!string.IsNullOrEmpty(messageGuid)) if (!string.IsNullOrEmpty(messageGuid))
{ {
await operationService.SetOperationMessageGuidAsync(operation.Id, messageGuid); await headquartersService.SetOperationMessageGuidAsync(operation.Id, messageGuid);
} }
} }
@ -56,9 +59,9 @@ namespace Hcs.WebApp.BackgroundServices
private async Task InitializeStateAsync() private async Task InitializeStateAsync()
{ {
using var scope = scopeFactory.CreateScope(); using var scope = scopeFactory.CreateScope();
var operationService = scope.ServiceProvider.GetRequiredService<OperationService>(); var headquartersService = scope.ServiceProvider.GetRequiredService<HeadquartersService>();
var operations = await operationService.GetInitiatedOperationsAsync(); var operations = await headquartersService.GetInitiatedOperationsAsync();
foreach (var operation in operations) foreach (var operation in operations)
{ {
state.EnqueueOperation(operation); state.EnqueueOperation(operation);

View File

@ -1,7 +1,7 @@
using Hcs.Broker; using Hcs.Broker;
using Hcs.WebApp.Data.Hcs; using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.Executors namespace Hcs.WebApp.BackgroundServices.OperationExecutors
{ {
public abstract class ExecutorBase(IClient client, Operation operation) : IExecutor public abstract class ExecutorBase(IClient client, Operation operation) : IExecutor
{ {

View File

@ -1,8 +1,8 @@
using Hcs.Broker; using Hcs.Broker;
using Hcs.WebApp.BackgroundServices.Executors._15_7_0_1.NsiCommon; using Hcs.WebApp.BackgroundServices.OperationExecutors.NsiCommon;
using Hcs.WebApp.Data.Hcs; using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.Executors namespace Hcs.WebApp.BackgroundServices.OperationExecutors
{ {
public class ExecutorFactory public class ExecutorFactory
{ {
@ -10,8 +10,8 @@ namespace Hcs.WebApp.BackgroundServices.Executors
{ {
switch (operation.Type) switch (operation.Type)
{ {
case Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements: case Operation.OperationType.NsiCommon_ExportNsiItem_15_7_0_1:
return new ExportAllRegistryElementsExecutor(client, operation); return new ExportNsiItemExecutor_15_7_0_1(client, operation);
} }
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -1,4 +1,4 @@
namespace Hcs.WebApp.BackgroundServices.Executors namespace Hcs.WebApp.BackgroundServices.OperationExecutors
{ {
public interface IExecutor public interface IExecutor
{ {

View File

@ -1,9 +1,9 @@
using Hcs.Broker; using Hcs.Broker;
using Hcs.WebApp.Data.Hcs; using Hcs.WebApp.Data.Hcs;
namespace Hcs.WebApp.BackgroundServices.Executors._15_7_0_1.NsiCommon namespace Hcs.WebApp.BackgroundServices.OperationExecutors.NsiCommon
{ {
public class ExportAllRegistryElementsExecutor(IClient client, Operation operation) : ExecutorBase(client, operation) public class ExportNsiItemExecutor_15_7_0_1(IClient client, Operation operation) : ExecutorBase(client, operation)
{ {
public override async Task<string> ExecuteAsync(CancellationToken cancellationToken) public override async Task<string> ExecuteAsync(CancellationToken cancellationToken)
{ {

View File

@ -13,10 +13,10 @@
@attribute [Authorize] @attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject OperationService OperationService @inject HeadquartersService HeadquartersService
@inject RegistryService RegistryService @inject RegistryService RegistryService
@inject DialogService DialogService @inject DialogService DialogService
@inject OperationExecutionState OperationExecutionState @inject CampaignManagementState CampaignManagementState
<PageTitle>Общие справочники подсистемы НСИ</PageTitle> <PageTitle>Общие справочники подсистемы НСИ</PageTitle>
@ -83,7 +83,7 @@
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (state.User.IsInRole(AppRole.ADMINISTRATOR_TYPE) || state.User.IsInRole(AppRole.OPERATOR_TYPE)) if (state.User.IsInRole(AppRole.ADMINISTRATOR_TYPE) || state.User.IsInRole(AppRole.OPERATOR_TYPE))
{ {
var operationInProgress = await OperationService.HasActiveOperationAsync(Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements); var operationInProgress = await HeadquartersService.HasActiveCampaignAsync(Campaign.CampaignType.ExportRequiredRegistryElements_15_7_0_1);
if (operationInProgress) if (operationInProgress)
{ {
finalState = CommonPageState.OperationWaiting; finalState = CommonPageState.OperationWaiting;
@ -93,7 +93,7 @@
registries = await RegistryService.GetAllRegistriesAsync(true); registries = await RegistryService.GetAllRegistriesAsync(true);
} }
OperationExecutionState.OnOperationStarted += OnOperationStarted; CampaignManagementState.OnCampaignStarted += OnCampaignStarted;
} }
ChangeState(finalState); ChangeState(finalState);
@ -106,14 +106,15 @@
ChangeState(CommonPageState.OperationWaiting); ChangeState(CommonPageState.OperationWaiting);
if (await OperationService.HasActiveOperationAsync(Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements)) if (await HeadquartersService.HasActiveCampaignAsync(Campaign.CampaignType.ExportRequiredRegistryElements_15_7_0_1))
{ {
ChangeState(CommonPageState.Idle); ChangeState(CommonPageState.Idle);
} }
else else
{ {
var operation = await OperationService.InitiateOperationAsync(Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements, ""); // TODO: Use user id
OperationExecutionState.EnqueueOperation(operation); var campaign = await HeadquartersService.InitiateCampaignAsync(Campaign.CampaignType.ExportRequiredRegistryElements_15_7_0_1, "");
CampaignManagementState.EnqueueCampaign(campaign);
} }
} }
@ -161,9 +162,9 @@
} }
} }
void OnOperationStarted(Operation operation) void OnCampaignStarted(Campaign campaign)
{ {
if (operation.Type == Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements) if (campaign.Type == Campaign.CampaignType.ExportRequiredRegistryElements_15_7_0_1)
{ {
InvokeAsync(() => ChangeState(CommonPageState.OperationWaiting)); InvokeAsync(() => ChangeState(CommonPageState.OperationWaiting));
} }
@ -171,6 +172,6 @@
public void Dispose() public void Dispose()
{ {
OperationExecutionState.OnOperationStarted -= OnOperationStarted; CampaignManagementState.OnCampaignStarted -= OnCampaignStarted;
} }
} }

View File

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Hcs.WebApp.Data.Hcs
{
public class Campaign
{
public enum CampaignType
{
ExportRequiredRegistryElements_15_7_0_1
}
public int Id { get; set; }
public CampaignType Type { get; set; }
public string InitiatorId { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
public virtual ICollection<Operation> Operations { get; set; } = [];
[NotMapped]
public bool Completed => EndedAt.HasValue;
}
}

View File

@ -9,10 +9,16 @@ namespace Hcs.WebApp.Data.Hcs
public DbSet<RegistryElement> Elements { get; set; } public DbSet<RegistryElement> Elements { get; set; }
public DbSet<Campaign> Campaigns { get; set; }
public DbSet<Operation> Operations { get; set; } public DbSet<Operation> Operations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<Campaign>()
.Property(x => x.Type)
.HasConversion(new EnumToStringConverter<Campaign.CampaignType>());
modelBuilder.Entity<Operation>() modelBuilder.Entity<Operation>()
.Property(x => x.Type) .Property(x => x.Type)
.HasConversion(new EnumToStringConverter<Operation.OperationType>()); .HasConversion(new EnumToStringConverter<Operation.OperationType>());

View File

@ -6,14 +6,16 @@ namespace Hcs.WebApp.Data.Hcs
{ {
public enum OperationType public enum OperationType
{ {
NsiCommon_15_7_0_1_ExportAllRegistryElements NsiCommon_ExportNsiItem_15_7_0_1
} }
public int Id { get; set; } public int Id { get; set; }
public OperationType Type { get; set; } public int CampaignId { get; set; }
public string InitiatorId { get; set; } public virtual Campaign Campaign { get; set; } = null!;
public OperationType Type { get; set; }
public DateTime StartedAt { get; set; } public DateTime StartedAt { get; set; }
@ -21,6 +23,8 @@ namespace Hcs.WebApp.Data.Hcs
public string? MessageGuid { get; set; } public string? MessageGuid { get; set; }
public virtual ICollection<Registry> Registries { get; set; } = [];
[NotMapped] [NotMapped]
public bool Completed => EndedAt.HasValue; public bool Completed => EndedAt.HasValue;
} }

View File

@ -10,7 +10,13 @@
public bool IsCommon { get; set; } public bool IsCommon { get; set; }
public DateTime UpdatedAt { get; set; } public DateTime SyncedAt { get; set; }
public int LastSyncOperationId { get; set; }
public virtual Operation LastSyncOperation { get; set; }
public string LastSyncError { get; set; }
public virtual ICollection<RegistryElement> Elements { get; set; } = []; public virtual ICollection<RegistryElement> Elements { get; set; } = [];
} }

View File

@ -1,4 +1,5 @@
using Hcs.WebApp.BackgroundServices; using Hcs.WebApp.BackgroundServices;
using Hcs.WebApp.BackgroundServices.CampaignManagers;
using Hcs.WebApp.Components; using Hcs.WebApp.Components;
using Hcs.WebApp.Components.Shared; using Hcs.WebApp.Components.Shared;
using Hcs.WebApp.Data.Hcs; using Hcs.WebApp.Data.Hcs;
@ -58,10 +59,14 @@ builder.Services.AddTransient<IClientProvider, ClientProvider>();
#endif #endif
builder.Services.AddScoped<IdentityService>(); builder.Services.AddScoped<IdentityService>();
builder.Services.AddScoped<UsersService>(); builder.Services.AddScoped<UsersService>();
builder.Services.AddScoped<OperationService>(); builder.Services.AddScoped<HeadquartersService>();
builder.Services.AddScoped<RegistryService>(); builder.Services.AddScoped<RegistryService>();
builder.Services.AddSingleton<CampaignManagementState>();
builder.Services.AddSingleton<OperationExecutionState>(); builder.Services.AddSingleton<OperationExecutionState>();
builder.Services.AddSingleton<ManagerFactory>();
builder.Services.AddHostedService<CampaignManagementService>();
builder.Services.AddHostedService<OperationExecutionService>(); builder.Services.AddHostedService<OperationExecutionService>();
var activator = new RadzenComponentActivator(); var activator = new RadzenComponentActivator();

View File

@ -3,28 +3,22 @@ using Microsoft.EntityFrameworkCore;
namespace Hcs.WebApp.Services namespace Hcs.WebApp.Services
{ {
public class OperationService(IDbContextFactory<HcsDbContext> factory) public class HeadquartersService(IDbContextFactory<HcsDbContext> factory)
{ {
private readonly IDbContextFactory<HcsDbContext> factory = factory; private readonly IDbContextFactory<HcsDbContext> factory = factory;
public async Task<bool> HasActiveOperationAsync(Operation.OperationType type) public async Task<bool> HasActiveCampaignAsync(Campaign.CampaignType type)
{ {
using var context = factory.CreateDbContext(); using var context = factory.CreateDbContext();
return await context.Operations.AnyAsync(x => x.Type == type && !x.EndedAt.HasValue); return await context.Campaigns.AnyAsync(x => x.Type == type && !x.EndedAt.HasValue);
} }
public async Task<Operation> InitiateOperationAsync(Operation.OperationType type, string initiatorId) public async Task<IEnumerable<Campaign>> GetInitiatedCampaignAsync()
{ {
using var context = factory.CreateDbContext(); using var context = factory.CreateDbContext();
var operation = new Operation() return await (from campaign in context.Campaigns
{ where !campaign.EndedAt.HasValue
Type = type, select campaign).ToListAsync();
InitiatorId = initiatorId,
StartedAt = DateTime.UtcNow
};
await context.Operations.AddAsync(operation);
await context.SaveChangesAsync();
return operation;
} }
public async Task<IEnumerable<Operation>> GetInitiatedOperationsAsync() public async Task<IEnumerable<Operation>> GetInitiatedOperationsAsync()
@ -35,6 +29,34 @@ namespace Hcs.WebApp.Services
select operation).ToListAsync(); select operation).ToListAsync();
} }
public async Task<Campaign> InitiateCampaignAsync(Campaign.CampaignType type, string initiatorId)
{
using var context = factory.CreateDbContext();
var campaign = new Campaign()
{
Type = type,
InitiatorId = initiatorId,
StartedAt = DateTime.UtcNow
};
await context.Campaigns.AddAsync(campaign);
await context.SaveChangesAsync();
return campaign;
}
public async Task<Operation> InitiateOperationAsync(int campaignId, Operation.OperationType type)
{
using var context = factory.CreateDbContext();
var operation = new Operation()
{
CampaignId = campaignId,
Type = type,
StartedAt = DateTime.UtcNow
};
await context.Operations.AddAsync(operation);
await context.SaveChangesAsync();
return operation;
}
public async Task SetOperationMessageGuidAsync(int operationId, string messageGuid) public async Task SetOperationMessageGuidAsync(int operationId, string messageGuid)
{ {
using var context = factory.CreateDbContext(); using var context = factory.CreateDbContext();