diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagementService.cs b/Hcs.WebApp/BackgroundServices/CampaignManagementService.cs new file mode 100644 index 0000000..589402e --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagementService.cs @@ -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(); + + var campaigns = await headquartersService.GetInitiatedCampaignAsync(); + foreach (var campaign in campaigns) + { + campaignManagementState.EnqueueCampaign(campaign); + } + } + } +} diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagementState.cs b/Hcs.WebApp/BackgroundServices/CampaignManagementState.cs new file mode 100644 index 0000000..20d086d --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagementState.cs @@ -0,0 +1,27 @@ +using Hcs.WebApp.Data.Hcs; +using System.Collections.Concurrent; + +namespace Hcs.WebApp.BackgroundServices +{ + public class CampaignManagementState + { + private readonly ConcurrentQueue campaigns = new(); + + public event Action 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); + } + } +} diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagers/ExportRequiredRegistryElementsManager_15_7_0_1.cs b/Hcs.WebApp/BackgroundServices/CampaignManagers/ExportRequiredRegistryElementsManager_15_7_0_1.cs new file mode 100644 index 0000000..59abf00 --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagers/ExportRequiredRegistryElementsManager_15_7_0_1.cs @@ -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 + } + } +} diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagers/IManager.cs b/Hcs.WebApp/BackgroundServices/CampaignManagers/IManager.cs new file mode 100644 index 0000000..5eeb06b --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagers/IManager.cs @@ -0,0 +1,7 @@ +namespace Hcs.WebApp.BackgroundServices.CampaignManagers +{ + public interface IManager + { + Task StartAsync(CancellationToken cancellationToken); + } +} diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerBase.cs b/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerBase.cs new file mode 100644 index 0000000..fd1b276 --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerBase.cs @@ -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); + } +} diff --git a/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerFactory.cs b/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerFactory.cs new file mode 100644 index 0000000..809fe92 --- /dev/null +++ b/Hcs.WebApp/BackgroundServices/CampaignManagers/ManagerFactory.cs @@ -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(); + } + } +} diff --git a/Hcs.WebApp/BackgroundServices/OperationExecutionService.cs b/Hcs.WebApp/BackgroundServices/OperationExecutionService.cs index 9d86881..6784890 100644 --- a/Hcs.WebApp/BackgroundServices/OperationExecutionService.cs +++ b/Hcs.WebApp/BackgroundServices/OperationExecutionService.cs @@ -1,7 +1,7 @@ using Hcs.Broker; using Hcs.Broker.Logger; using Hcs.Broker.MessageCapturer; -using Hcs.WebApp.BackgroundServices.Executors; +using Hcs.WebApp.BackgroundServices.OperationExecutors; using Hcs.WebApp.Config; using Hcs.WebApp.Services; @@ -26,12 +26,14 @@ namespace Hcs.WebApp.BackgroundServices InitializeClient(); var scope = scopeFactory.CreateScope(); - var operationService = scope.ServiceProvider.GetRequiredService(); + var headquartersService = scope.ServiceProvider.GetRequiredService(); while (!stoppingToken.IsCancellationRequested) { while (state.TryDequeueOperation(out var operation)) { + if (stoppingToken.IsCancellationRequested) return; + var messageGuid = string.Empty; try { @@ -40,12 +42,13 @@ namespace Hcs.WebApp.BackgroundServices } catch { + // TODO: Добавить таймауты и макс количество попыток выполнения операции state.EnqueueOperation(operation); } 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() { using var scope = scopeFactory.CreateScope(); - var operationService = scope.ServiceProvider.GetRequiredService(); + var headquartersService = scope.ServiceProvider.GetRequiredService(); - var operations = await operationService.GetInitiatedOperationsAsync(); + var operations = await headquartersService.GetInitiatedOperationsAsync(); foreach (var operation in operations) { state.EnqueueOperation(operation); diff --git a/Hcs.WebApp/BackgroundServices/Executors/ExecutorBase.cs b/Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorBase.cs similarity index 85% rename from Hcs.WebApp/BackgroundServices/Executors/ExecutorBase.cs rename to Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorBase.cs index d1d8737..d13fc5c 100644 --- a/Hcs.WebApp/BackgroundServices/Executors/ExecutorBase.cs +++ b/Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorBase.cs @@ -1,7 +1,7 @@ using Hcs.Broker; using Hcs.WebApp.Data.Hcs; -namespace Hcs.WebApp.BackgroundServices.Executors +namespace Hcs.WebApp.BackgroundServices.OperationExecutors { public abstract class ExecutorBase(IClient client, Operation operation) : IExecutor { diff --git a/Hcs.WebApp/BackgroundServices/Executors/ExecutorFactory.cs b/Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorFactory.cs similarity index 51% rename from Hcs.WebApp/BackgroundServices/Executors/ExecutorFactory.cs rename to Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorFactory.cs index 62a5245..24f87b5 100644 --- a/Hcs.WebApp/BackgroundServices/Executors/ExecutorFactory.cs +++ b/Hcs.WebApp/BackgroundServices/OperationExecutors/ExecutorFactory.cs @@ -1,8 +1,8 @@ using Hcs.Broker; -using Hcs.WebApp.BackgroundServices.Executors._15_7_0_1.NsiCommon; +using Hcs.WebApp.BackgroundServices.OperationExecutors.NsiCommon; using Hcs.WebApp.Data.Hcs; -namespace Hcs.WebApp.BackgroundServices.Executors +namespace Hcs.WebApp.BackgroundServices.OperationExecutors { public class ExecutorFactory { @@ -10,8 +10,8 @@ namespace Hcs.WebApp.BackgroundServices.Executors { switch (operation.Type) { - case Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements: - return new ExportAllRegistryElementsExecutor(client, operation); + case Operation.OperationType.NsiCommon_ExportNsiItem_15_7_0_1: + return new ExportNsiItemExecutor_15_7_0_1(client, operation); } throw new NotImplementedException(); diff --git a/Hcs.WebApp/BackgroundServices/Executors/IExecutor.cs b/Hcs.WebApp/BackgroundServices/OperationExecutors/IExecutor.cs similarity index 65% rename from Hcs.WebApp/BackgroundServices/Executors/IExecutor.cs rename to Hcs.WebApp/BackgroundServices/OperationExecutors/IExecutor.cs index df02e3f..2f8339d 100644 --- a/Hcs.WebApp/BackgroundServices/Executors/IExecutor.cs +++ b/Hcs.WebApp/BackgroundServices/OperationExecutors/IExecutor.cs @@ -1,4 +1,4 @@ -namespace Hcs.WebApp.BackgroundServices.Executors +namespace Hcs.WebApp.BackgroundServices.OperationExecutors { public interface IExecutor { diff --git a/Hcs.WebApp/BackgroundServices/Executors/15_7_0_1/NsiCommon/ExportAllRegistryElementsExecutor.cs b/Hcs.WebApp/BackgroundServices/OperationExecutors/NsiCommon/ExportNsiItemExecutor_15_7_0_1.cs similarity index 61% rename from Hcs.WebApp/BackgroundServices/Executors/15_7_0_1/NsiCommon/ExportAllRegistryElementsExecutor.cs rename to Hcs.WebApp/BackgroundServices/OperationExecutors/NsiCommon/ExportNsiItemExecutor_15_7_0_1.cs index aa44fbd..5562565 100644 --- a/Hcs.WebApp/BackgroundServices/Executors/15_7_0_1/NsiCommon/ExportAllRegistryElementsExecutor.cs +++ b/Hcs.WebApp/BackgroundServices/OperationExecutors/NsiCommon/ExportNsiItemExecutor_15_7_0_1.cs @@ -1,9 +1,9 @@ using Hcs.Broker; 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 ExecuteAsync(CancellationToken cancellationToken) { diff --git a/Hcs.WebApp/Components/Pages/Registry/Common.razor b/Hcs.WebApp/Components/Pages/Registry/Common.razor index c031a3b..601cd30 100644 --- a/Hcs.WebApp/Components/Pages/Registry/Common.razor +++ b/Hcs.WebApp/Components/Pages/Registry/Common.razor @@ -13,10 +13,10 @@ @attribute [Authorize] @inject AuthenticationStateProvider AuthenticationStateProvider -@inject OperationService OperationService +@inject HeadquartersService HeadquartersService @inject RegistryService RegistryService @inject DialogService DialogService -@inject OperationExecutionState OperationExecutionState +@inject CampaignManagementState CampaignManagementState Общие справочники подсистемы НСИ @@ -83,7 +83,7 @@ var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 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) { finalState = CommonPageState.OperationWaiting; @@ -93,7 +93,7 @@ registries = await RegistryService.GetAllRegistriesAsync(true); } - OperationExecutionState.OnOperationStarted += OnOperationStarted; + CampaignManagementState.OnCampaignStarted += OnCampaignStarted; } ChangeState(finalState); @@ -106,14 +106,15 @@ 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); } else { - var operation = await OperationService.InitiateOperationAsync(Operation.OperationType.NsiCommon_15_7_0_1_ExportAllRegistryElements, ""); - OperationExecutionState.EnqueueOperation(operation); + // TODO: Use user id + 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)); } @@ -171,6 +172,6 @@ public void Dispose() { - OperationExecutionState.OnOperationStarted -= OnOperationStarted; + CampaignManagementState.OnCampaignStarted -= OnCampaignStarted; } } diff --git a/Hcs.WebApp/Data/Hcs/Campaign.cs b/Hcs.WebApp/Data/Hcs/Campaign.cs new file mode 100644 index 0000000..15bd1e3 --- /dev/null +++ b/Hcs.WebApp/Data/Hcs/Campaign.cs @@ -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 Operations { get; set; } = []; + + [NotMapped] + public bool Completed => EndedAt.HasValue; + } +} diff --git a/Hcs.WebApp/Data/Hcs/HcsDbContext.cs b/Hcs.WebApp/Data/Hcs/HcsDbContext.cs index 8ceb35e..432dca8 100644 --- a/Hcs.WebApp/Data/Hcs/HcsDbContext.cs +++ b/Hcs.WebApp/Data/Hcs/HcsDbContext.cs @@ -9,10 +9,16 @@ namespace Hcs.WebApp.Data.Hcs public DbSet Elements { get; set; } + public DbSet Campaigns { get; set; } + public DbSet Operations { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .Property(x => x.Type) + .HasConversion(new EnumToStringConverter()); + modelBuilder.Entity() .Property(x => x.Type) .HasConversion(new EnumToStringConverter()); diff --git a/Hcs.WebApp/Data/Hcs/Operation.cs b/Hcs.WebApp/Data/Hcs/Operation.cs index 0963b7a..89a8367 100644 --- a/Hcs.WebApp/Data/Hcs/Operation.cs +++ b/Hcs.WebApp/Data/Hcs/Operation.cs @@ -6,14 +6,16 @@ namespace Hcs.WebApp.Data.Hcs { public enum OperationType { - NsiCommon_15_7_0_1_ExportAllRegistryElements + NsiCommon_ExportNsiItem_15_7_0_1 } 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; } @@ -21,6 +23,8 @@ namespace Hcs.WebApp.Data.Hcs public string? MessageGuid { get; set; } + public virtual ICollection Registries { get; set; } = []; + [NotMapped] public bool Completed => EndedAt.HasValue; } diff --git a/Hcs.WebApp/Data/Hcs/Registry.cs b/Hcs.WebApp/Data/Hcs/Registry.cs index a2fdf84..55e0023 100644 --- a/Hcs.WebApp/Data/Hcs/Registry.cs +++ b/Hcs.WebApp/Data/Hcs/Registry.cs @@ -10,7 +10,13 @@ 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 Elements { get; set; } = []; } diff --git a/Hcs.WebApp/Program.cs b/Hcs.WebApp/Program.cs index 516948f..ea8bf71 100644 --- a/Hcs.WebApp/Program.cs +++ b/Hcs.WebApp/Program.cs @@ -1,4 +1,5 @@ using Hcs.WebApp.BackgroundServices; +using Hcs.WebApp.BackgroundServices.CampaignManagers; using Hcs.WebApp.Components; using Hcs.WebApp.Components.Shared; using Hcs.WebApp.Data.Hcs; @@ -58,10 +59,14 @@ builder.Services.AddTransient(); #endif builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddHostedService(); builder.Services.AddHostedService(); var activator = new RadzenComponentActivator(); diff --git a/Hcs.WebApp/Services/OperationService.cs b/Hcs.WebApp/Services/HeadquartersService.cs similarity index 55% rename from Hcs.WebApp/Services/OperationService.cs rename to Hcs.WebApp/Services/HeadquartersService.cs index 31a94cd..feb0106 100644 --- a/Hcs.WebApp/Services/OperationService.cs +++ b/Hcs.WebApp/Services/HeadquartersService.cs @@ -3,28 +3,22 @@ using Microsoft.EntityFrameworkCore; namespace Hcs.WebApp.Services { - public class OperationService(IDbContextFactory factory) + public class HeadquartersService(IDbContextFactory factory) { private readonly IDbContextFactory factory = factory; - public async Task HasActiveOperationAsync(Operation.OperationType type) + public async Task HasActiveCampaignAsync(Campaign.CampaignType type) { 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 InitiateOperationAsync(Operation.OperationType type, string initiatorId) + public async Task> GetInitiatedCampaignAsync() { using var context = factory.CreateDbContext(); - var operation = new Operation() - { - Type = type, - InitiatorId = initiatorId, - StartedAt = DateTime.UtcNow - }; - await context.Operations.AddAsync(operation); - await context.SaveChangesAsync(); - return operation; + return await (from campaign in context.Campaigns + where !campaign.EndedAt.HasValue + select campaign).ToListAsync(); } public async Task> GetInitiatedOperationsAsync() @@ -35,6 +29,34 @@ namespace Hcs.WebApp.Services select operation).ToListAsync(); } + public async Task 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 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) { using var context = factory.CreateDbContext();