From 7744ebcd128d3edba76e33107ffa22fc3cd37301 Mon Sep 17 00:00:00 2001 From: ad Date: Fri, 23 May 2025 14:49:29 -0700 Subject: [PATCH 1/7] work in progress --- .../Shared/JobRunner.cs | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/ApplicationControl.Client/Shared/JobRunner.cs b/src/ApplicationControl.Client/Shared/JobRunner.cs index 0a08d7c..fa829ec 100644 --- a/src/ApplicationControl.Client/Shared/JobRunner.cs +++ b/src/ApplicationControl.Client/Shared/JobRunner.cs @@ -37,47 +37,34 @@ public async Task RunJobAsync(List jobs, CancellationToken cancellationTok foreach (var job in jobs) { _semaphore.Wait(cancellationToken); - - try + + // Run the job asynchronously + var task = Task.Run(async () => { - // Run the job asynchronously - var task = Task.Run(async () => + try { - try - { - _logger.LogInformation($"Executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + _logger.LogInformation($"Executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - await _commandProcessor.PprocessAsync(job.Command); + await _commandProcessor.PprocessAsync(job.Command); - _logger.LogInformation($"Job {job.Id} with command {job.Command} executed successfully, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - finally - { - // Release the semaphore when the job is done - _semaphore.Release(); - } - }, cancellationToken); + _logger.LogInformation($"Job {job.Id} with command {job.Command} executed successfully, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + } + finally + { + // Release the semaphore when the job is done + _semaphore.Release(); + } + }, cancellationToken); - tasks.Add(task); - } - catch (Exception ex) - { - // Handle exceptions as needed - _logger.LogError(ex, $"Error executing jobs, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - finally - { - // Ensure the semaphore is released even if an exception occurs - _semaphore.Release(); - } + tasks.Add(task); } await Task.WhenAll(tasks); - _logger.LogInformation($"JobRunner completed, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); + _logger.LogInformation($"JobRunner completed, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); } } From a37790b4d1f28ea036b8acb693001a82e1076d56 Mon Sep 17 00:00:00 2001 From: ad Date: Sat, 24 May 2025 11:50:53 -0700 Subject: [PATCH 2/7] work in progress --- .../ApplicationControl.Client.csproj | 3 ++ ...ttings.cs => ApplicationControlOptions.cs} | 2 +- .../DatabaseQueuedJobHostedService.cs | 5 --- .../Database/Entities/QueuedApplicationJob.cs | 2 - .../DatabaseQueuedJobHostedService.cs | 4 +- .../QueuedApplicationJobRepository.cs | 4 +- src/ApplicationControl.Client/Extensions.cs | 37 +++++++++++++++++++ .../Shared/JobRunner.cs | 32 ++++++++++++++-- 8 files changed, 73 insertions(+), 16 deletions(-) rename src/ApplicationControl.Client/Configuration/{Settings.cs => ApplicationControlOptions.cs} (92%) delete mode 100644 src/ApplicationControl.Client/Database/DatabaseQueuedJobHostedService.cs diff --git a/src/ApplicationControl.Client/ApplicationControl.Client.csproj b/src/ApplicationControl.Client/ApplicationControl.Client.csproj index 5d656bf..e1acf09 100644 --- a/src/ApplicationControl.Client/ApplicationControl.Client.csproj +++ b/src/ApplicationControl.Client/ApplicationControl.Client.csproj @@ -5,8 +5,11 @@ enable + + + diff --git a/src/ApplicationControl.Client/Configuration/Settings.cs b/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs similarity index 92% rename from src/ApplicationControl.Client/Configuration/Settings.cs rename to src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs index 249d0a2..f8b0ccc 100644 --- a/src/ApplicationControl.Client/Configuration/Settings.cs +++ b/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs @@ -1,6 +1,6 @@ namespace ApplicationControl.Client.Configuration; -public class Settings +public class ApplicationControlOptions { public string? ConnectionString { get; set; } diff --git a/src/ApplicationControl.Client/Database/DatabaseQueuedJobHostedService.cs b/src/ApplicationControl.Client/Database/DatabaseQueuedJobHostedService.cs deleted file mode 100644 index 0db0f51..0000000 --- a/src/ApplicationControl.Client/Database/DatabaseQueuedJobHostedService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ApplicationControl.Client.Database; - -public class DatabaseQueuedJobHostedService -{ -} \ No newline at end of file diff --git a/src/ApplicationControl.Client/Database/Entities/QueuedApplicationJob.cs b/src/ApplicationControl.Client/Database/Entities/QueuedApplicationJob.cs index 2f02899..9350d1a 100644 --- a/src/ApplicationControl.Client/Database/Entities/QueuedApplicationJob.cs +++ b/src/ApplicationControl.Client/Database/Entities/QueuedApplicationJob.cs @@ -13,8 +13,6 @@ public class QueuedApplicationJob : IEntity, IJob [Required] public required string Command { get; set; } - public string ApplicationName { get; set; } = "Not Available"; - [Required] public required Guid ApplicationId { get; set; } diff --git a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs b/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs index b854e23..46c2a91 100644 --- a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs +++ b/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs @@ -7,10 +7,10 @@ namespace ApplicationControl.Client.Database.hostedServices; -public class DatabaseQueuedJobHostedService(IServiceProvider services, ILogger logger, IOptions options) : BackgroundService +public class DatabaseQueuedJobHostedService(IServiceProvider services, ILogger logger, IOptions options) : BackgroundService { private readonly ILogger _logger = logger; - private readonly Settings _options = options.Value; + private readonly ApplicationControlOptions _options = options.Value; public IServiceProvider Services { get; } = services; Guid _processId = Guid.NewGuid(); diff --git a/src/ApplicationControl.Client/Database/Repositories/QueuedApplicationJobRepository.cs b/src/ApplicationControl.Client/Database/Repositories/QueuedApplicationJobRepository.cs index 7c73e08..66a4cb4 100644 --- a/src/ApplicationControl.Client/Database/Repositories/QueuedApplicationJobRepository.cs +++ b/src/ApplicationControl.Client/Database/Repositories/QueuedApplicationJobRepository.cs @@ -10,7 +10,7 @@ public async Task> GetNextJobListAsync(CancellationToken cancellation { var commandList = await Entity - .Where(p => p.Status == JobStatus.Queued) + .Where(p => p.Status == JobStatus.Queued && p.IsDeleted == false) .OrderBy(p => p.AddedDateTime) .ToListAsync(cancellationToken); @@ -25,7 +25,7 @@ public async Task SetJobStatusAsync(Guid applicaitonId, Guid jobId, string setBy .SingleOrDefaultAsync(cancellationToken); if (cmd is null) - throw new Exception($"Job Id {jobId} with applicationId {applicaitonId} not found."); + throw new Exception($"QueuedApplicationJobRepository, Job Id {jobId} with applicationId {applicaitonId} not found."); cmd.Status = queuedJobStatus; diff --git a/src/ApplicationControl.Client/Extensions.cs b/src/ApplicationControl.Client/Extensions.cs index d12ef0b..3ae8364 100644 --- a/src/ApplicationControl.Client/Extensions.cs +++ b/src/ApplicationControl.Client/Extensions.cs @@ -1,5 +1,42 @@ +using ApplicationControl.Client.Configuration; +using ApplicationControl.Client.Database; +using ApplicationControl.Client.Database.hostedServices; +using ApplicationControl.Client.Shared; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using ApplicationControl.Client.Database.Repositories; + + namespace ApplicationControl.Client; public static class Extensions { + public static IHostBuilder AddDatabaseApplicationControl(this IHostBuilder hostBuilder) + { + + hostBuilder.ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection("ApplicationControl")); + + var config = context.Configuration.GetSection("ApplicationControl").Get(); + + services.AddDbContext(options => + { + if (config is not null) + { + var connectionString = config.ConnectionString; + options.UseSqlServer(connectionString); + } + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHostedService(); + }); + + return hostBuilder; + } } diff --git a/src/ApplicationControl.Client/Shared/JobRunner.cs b/src/ApplicationControl.Client/Shared/JobRunner.cs index fa829ec..e4da01f 100644 --- a/src/ApplicationControl.Client/Shared/JobRunner.cs +++ b/src/ApplicationControl.Client/Shared/JobRunner.cs @@ -1,7 +1,10 @@ +using ApplicationControl.Client.Configuration; +using ApplicationControl.Client.Database.Repositories; using ApplicationControl.CommandProcessor; using ApplicationControl.Core.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace ApplicationControl.Client.Shared; @@ -10,18 +13,29 @@ public class JobRunner : IJobRunner private readonly SemaphoreSlim _semaphore; private readonly ILogger _logger; private readonly ICommandProcessor _commandProcessor; + private readonly IQueuedApplicationJobRepository _queuedApplicationJobRepository; - public JobRunner(ILogger logger, ICommandProcessor commandProcessor) + private readonly ApplicationControlOptions _options; + + public JobRunner(ILogger logger, ICommandProcessor commandProcessor, IQueuedApplicationJobRepository queuedApplicationJobRepository, IOptions options) { ArgumentNullException.ThrowIfNull(commandProcessor, nameof(commandProcessor)); ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - + ArgumentNullException.ThrowIfNull(queuedApplicationJobRepository, nameof(queuedApplicationJobRepository)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + // Initialize the logger _logger = logger; // Initialize the logger and command processor _commandProcessor = commandProcessor; - + + // Initialize the queued application job repository + _queuedApplicationJobRepository = queuedApplicationJobRepository; + + // Initialize the options + _options = options.Value; + // Initialize the semaphore with a maximum count of 10 _semaphore = new SemaphoreSlim(10); } @@ -43,14 +57,24 @@ public async Task RunJobAsync(List jobs, CancellationToken cancellationTok { try { + await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.InProgress, null, cancellationToken) + .ConfigureAwait(false); + _logger.LogInformation($"Executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - await _commandProcessor.PprocessAsync(job.Command); + await _commandProcessor.PprocessAsync(job.Command) + .ConfigureAwait(false); + + await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Completed, null, cancellationToken) + .ConfigureAwait(false); _logger.LogInformation($"Job {job.Id} with command {job.Command} executed successfully, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); } catch (Exception ex) { + await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Failed, ex.Message, cancellationToken) + .ConfigureAwait(false); + _logger.LogError(ex, $"Error executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); } finally From 828623851087dd7e31a3a4fc08ba39e49efd4962 Mon Sep 17 00:00:00 2001 From: ad Date: Sat, 24 May 2025 16:12:31 -0700 Subject: [PATCH 3/7] remove some files --- .../IApplicationControlServiceClient.cs | 7 ----- .../ApplicationControl.Extensions.csproj | 10 ------- .../ApplicaitonControlConfiguration.cs | 6 ----- .../Configuration/applicationControl.json | 5 ---- src/scripts/command-queue-item.sql | 26 ------------------- 5 files changed, 54 deletions(-) delete mode 100644 src/ApplicationControl.Extensions/ApitClient/IApplicationControlServiceClient.cs delete mode 100644 src/ApplicationControl.Extensions/ApplicationControl.Extensions.csproj delete mode 100644 src/ApplicationControl.Extensions/Configuration/ApplicaitonControlConfiguration.cs delete mode 100644 src/ApplicationControl.Extensions/Configuration/applicationControl.json delete mode 100644 src/scripts/command-queue-item.sql diff --git a/src/ApplicationControl.Extensions/ApitClient/IApplicationControlServiceClient.cs b/src/ApplicationControl.Extensions/ApitClient/IApplicationControlServiceClient.cs deleted file mode 100644 index 3540fdb..0000000 --- a/src/ApplicationControl.Extensions/ApitClient/IApplicationControlServiceClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationControl.Extensions.ApitClient; - -public interface IApplicationControlServiceClient -{ - //Task GetNextCommandAsync(Guid applicationId, CancellationToken cancellationToken = default); - //Task SetCommandStatusAsync(Guid applicationId, Guid commandId, string setBy, CommandStatus commandStatus, string message, CancellationToken cancellationToken = default); -} diff --git a/src/ApplicationControl.Extensions/ApplicationControl.Extensions.csproj b/src/ApplicationControl.Extensions/ApplicationControl.Extensions.csproj deleted file mode 100644 index c8fdc44..0000000 --- a/src/ApplicationControl.Extensions/ApplicationControl.Extensions.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - net9.0 - enable - enable - - \ No newline at end of file diff --git a/src/ApplicationControl.Extensions/Configuration/ApplicaitonControlConfiguration.cs b/src/ApplicationControl.Extensions/Configuration/ApplicaitonControlConfiguration.cs deleted file mode 100644 index 2967f7d..0000000 --- a/src/ApplicationControl.Extensions/Configuration/ApplicaitonControlConfiguration.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ApplicationControl.Extensions.Configuration; - -public class ApplicaitonControlConfiguration -{ - public required string APIURL { get; set; } -} diff --git a/src/ApplicationControl.Extensions/Configuration/applicationControl.json b/src/ApplicationControl.Extensions/Configuration/applicationControl.json deleted file mode 100644 index 34f8c36..0000000 --- a/src/ApplicationControl.Extensions/Configuration/applicationControl.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ApplicationControl" { - "APIURL": "https://api.example.com", - } -} \ No newline at end of file diff --git a/src/scripts/command-queue-item.sql b/src/scripts/command-queue-item.sql deleted file mode 100644 index 4e03710..0000000 --- a/src/scripts/command-queue-item.sql +++ /dev/null @@ -1,26 +0,0 @@ -use master - -if DB_ID('ApplicationControl') is null - create database ApplicationControl -go - -use ApplicationControl - -if not exists (select * from ApplicationControl.sys.schemas where name = 'ApplicationControl') - exec ('create schema ApplicationControl') -go - -create table [ApplicationControl].[CommandQueueItem] ( - Id UNIQUEIDENTIFIER DEFAULT NEWID() not null, - Command VARCHAR(512) not null, - ApplicationName VARCHAR(512) null, - ApplicationId UNIQUEIDENTIFIER not null, - AddedDateTime DATETIME DEFAULT GETDATE(), - AddedBy VARCHAR(255) NOT NULL, - UpdatedDateTime DATETIME null, - UpdatedBy VARCHAR(255) null, - [Status] int not null default 0, - [Message] VARCHAR(MAX) null, - [IsDeleted] bit null default 0, - CONSTRAINT [PK_EmailQueue_Id] PRIMARY KEY CLUSTERED ([Id] ASC) -); \ No newline at end of file From 80e76ca041479e744815ad2992ca32c2bb6c5d06 Mon Sep 17 00:00:00 2001 From: ad Date: Tue, 27 May 2025 20:18:30 -0700 Subject: [PATCH 4/7] work in progress --- application-control.sln | 15 +++ .../Api/ApiQueuedJobHostedService.cs | 5 - .../Api/ApiQueuedJobsHostedSource.cs | 17 ++++ .../ApplicationControlOptions.cs | 5 +- .../Configuration/ClinetType.cs | 8 ++ .../Database/DatabaseQueuedJobsSource.cs | 25 +++++ .../HostedServices/DatabaseQueuedJobWorker.cs | 42 --------- .../IDatabaseQueuedJobWorker.cs | 6 -- src/ApplicationControl.Client/Extensions.cs | 28 ++++-- ...dService.cs => QueuedJobsHostedService.cs} | 26 ++--- .../Shared/IJobRunner.cs | 8 -- .../Shared/IJobWorker.cs | 8 ++ .../Shared/IJobsListener.cs | 6 ++ .../Shared/IJobsRunner.cs | 8 ++ .../Shared/IJobsSource.cs | 9 ++ .../Shared/JobRunner.cs | 94 ------------------- .../Shared/JobWorker.cs | 54 +++++++++++ .../Shared/JobsListener.cs | 43 +++++++++ .../Shared/JobsRunner.cs | 77 +++++++++++++++ .../ApplicationControl.Host.csproj | 15 +++ .../CommandHandlers/TestOneCommandHandler.cs | 22 +++++ .../TestThreeCommandHandler.cs | 23 +++++ .../CommandHandlers/TestTwoCommandHandler.cs | 21 +++++ src/ApplicationControl.Host/Program.cs | 18 ++++ src/ApplicationControl.Host/appsettings.json | 20 ++++ src/scripts/tables.sql | 4 +- 26 files changed, 426 insertions(+), 181 deletions(-) delete mode 100644 src/ApplicationControl.Client/Api/ApiQueuedJobHostedService.cs create mode 100644 src/ApplicationControl.Client/Api/ApiQueuedJobsHostedSource.cs create mode 100644 src/ApplicationControl.Client/Configuration/ClinetType.cs create mode 100644 src/ApplicationControl.Client/Database/DatabaseQueuedJobsSource.cs delete mode 100644 src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobWorker.cs delete mode 100644 src/ApplicationControl.Client/Database/HostedServices/IDatabaseQueuedJobWorker.cs rename src/ApplicationControl.Client/{Database/HostedServices/DatabaseQueuedJobHostedService.cs => QueuedJobsHostedService.cs} (55%) delete mode 100644 src/ApplicationControl.Client/Shared/IJobRunner.cs create mode 100644 src/ApplicationControl.Client/Shared/IJobWorker.cs create mode 100644 src/ApplicationControl.Client/Shared/IJobsListener.cs create mode 100644 src/ApplicationControl.Client/Shared/IJobsRunner.cs create mode 100644 src/ApplicationControl.Client/Shared/IJobsSource.cs delete mode 100644 src/ApplicationControl.Client/Shared/JobRunner.cs create mode 100644 src/ApplicationControl.Client/Shared/JobWorker.cs create mode 100644 src/ApplicationControl.Client/Shared/JobsListener.cs create mode 100644 src/ApplicationControl.Client/Shared/JobsRunner.cs create mode 100644 src/ApplicationControl.Host/ApplicationControl.Host.csproj create mode 100644 src/ApplicationControl.Host/CommandHandlers/TestOneCommandHandler.cs create mode 100644 src/ApplicationControl.Host/CommandHandlers/TestThreeCommandHandler.cs create mode 100644 src/ApplicationControl.Host/CommandHandlers/TestTwoCommandHandler.cs create mode 100644 src/ApplicationControl.Host/Program.cs create mode 100644 src/ApplicationControl.Host/appsettings.json diff --git a/application-control.sln b/application-control.sln index 0918144..ca690b1 100644 --- a/application-control.sln +++ b/application-control.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationControl.Client", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationControl.Core.Extensions", "src\ApplicationControl.Core.Extensions\ApplicationControl.Core.Extensions.csproj", "{BCA06128-F763-4F3D-88A7-F07D1F38D96C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationControl.Host", "src\ApplicationControl.Host\ApplicationControl.Host.csproj", "{074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +87,18 @@ Global {BCA06128-F763-4F3D-88A7-F07D1F38D96C}.Release|x64.Build.0 = Release|Any CPU {BCA06128-F763-4F3D-88A7-F07D1F38D96C}.Release|x86.ActiveCfg = Release|Any CPU {BCA06128-F763-4F3D-88A7-F07D1F38D96C}.Release|x86.Build.0 = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|x64.Build.0 = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Debug|x86.Build.0 = Debug|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|Any CPU.Build.0 = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|x64.ActiveCfg = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|x64.Build.0 = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|x86.ActiveCfg = Release|Any CPU + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -95,6 +109,7 @@ Global {CB24425A-60A4-47E7-AB77-4B4ECA73B93A} = {2065A797-D245-4B2E-8A03-A9E305863D38} {2BC5663E-E336-405C-B382-B606B7D5B6AC} = {2065A797-D245-4B2E-8A03-A9E305863D38} {BCA06128-F763-4F3D-88A7-F07D1F38D96C} = {2065A797-D245-4B2E-8A03-A9E305863D38} + {074DAD8E-9919-4AF4-BA36-5CE8E38B39C5} = {2065A797-D245-4B2E-8A03-A9E305863D38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9BCF421D-DD0F-4677-BB13-68EAEAFB6B2E} diff --git a/src/ApplicationControl.Client/Api/ApiQueuedJobHostedService.cs b/src/ApplicationControl.Client/Api/ApiQueuedJobHostedService.cs deleted file mode 100644 index cf69783..0000000 --- a/src/ApplicationControl.Client/Api/ApiQueuedJobHostedService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ApplicationControl.Client.Api; - -public class ApiQueuedJobHostedService -{ -} diff --git a/src/ApplicationControl.Client/Api/ApiQueuedJobsHostedSource.cs b/src/ApplicationControl.Client/Api/ApiQueuedJobsHostedSource.cs new file mode 100644 index 0000000..a54248b --- /dev/null +++ b/src/ApplicationControl.Client/Api/ApiQueuedJobsHostedSource.cs @@ -0,0 +1,17 @@ +using ApplicationControl.Client.Shared; +using ApplicationControl.Core.Extensions; + +namespace ApplicationControl.Client.Api; + +public class ApiQueuedJobsHostedSource : IJobsSource +{ + public Task> GetNextJobListAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task SetJobStatusAsync(Guid applicationId, Guid jobId, string setBy, JobStatus jobStatus, string? message, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs b/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs index f8b0ccc..18c56a1 100644 --- a/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs +++ b/src/ApplicationControl.Client/Configuration/ApplicationControlOptions.cs @@ -6,9 +6,12 @@ public class ApplicationControlOptions public string? ApplicationName { get; set; } public Guid ApplicaitonId { get; set; } + public ClinetType ApplicationClientType { get; set; } = ClinetType.Database; + // The time in seconds to wait before checking for new jobs public int QueuedHastedServiceResetProcessCycle { get; set; } = 60; - public int QueuedHastedServiceCycle { get; set; } = 60; + public int QueuedHastedServiceCycle { get; set; } = 60; public int QueuedHastedResetServiceCycle { get; set; } = 120; + public int? MaxJobsCount { get; set; } = 10; } diff --git a/src/ApplicationControl.Client/Configuration/ClinetType.cs b/src/ApplicationControl.Client/Configuration/ClinetType.cs new file mode 100644 index 0000000..9f8791d --- /dev/null +++ b/src/ApplicationControl.Client/Configuration/ClinetType.cs @@ -0,0 +1,8 @@ +namespace ApplicationControl.Client.Configuration; + +public enum ClinetType +{ + WebApi = 1, + Database = 2, + PubSub = 3 +} diff --git a/src/ApplicationControl.Client/Database/DatabaseQueuedJobsSource.cs b/src/ApplicationControl.Client/Database/DatabaseQueuedJobsSource.cs new file mode 100644 index 0000000..e571a0d --- /dev/null +++ b/src/ApplicationControl.Client/Database/DatabaseQueuedJobsSource.cs @@ -0,0 +1,25 @@ +using ApplicationControl.Client.Database.Repositories; +using ApplicationControl.Client.Shared; +using ApplicationControl.Core.Extensions; + +namespace ApplicationControl.Client.Database; + +public class DatabaseQueuedJobsSource : IJobsSource +{ + private readonly IQueuedApplicationJobRepository _queuedApplicationJobRepository; + +public DatabaseQueuedJobsSource(IQueuedApplicationJobRepository queuedApplicationJobRepository) +{ + ArgumentNullException.ThrowIfNull(queuedApplicationJobRepository, nameof(queuedApplicationJobRepository)); + _queuedApplicationJobRepository = queuedApplicationJobRepository; +} + public async Task> GetNextJobListAsync(CancellationToken cancellationToken = default) + { + return await _queuedApplicationJobRepository.GetNextJobListAsync(cancellationToken); + } + + public async Task SetJobStatusAsync(Guid applicationId, Guid jobId, string setBy, JobStatus jobStatus, string? message, CancellationToken cancellationToken = default) + { + await _queuedApplicationJobRepository.SetJobStatusAsync(applicationId, jobId, setBy, jobStatus, message, cancellationToken); + } +} diff --git a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobWorker.cs b/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobWorker.cs deleted file mode 100644 index 8f7d466..0000000 --- a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobWorker.cs +++ /dev/null @@ -1,42 +0,0 @@ - -using ApplicationControl.Client.Database.Repositories; -using ApplicationControl.Client.Shared; -using Microsoft.Extensions.Logging; - -namespace ApplicationControl.Client.Database.HostedServices; - -public class DatabaseQueuedJobWorker : IDatabaseQueuedJobWorker -{ - private readonly IQueuedApplicationJobRepository _queuedApplicationJobRepository; - private readonly ILogger _logger; - private readonly IJobRunner _jobRunner; - - public DatabaseQueuedJobWorker( - IQueuedApplicationJobRepository queuedApplicationJobRepository, - ILogger logger, - IJobRunner jobRunner) - { - _queuedApplicationJobRepository = queuedApplicationJobRepository; - _logger = logger; - _jobRunner = jobRunner; - } - - public async Task DoWorkAsync(CancellationToken cancellationToken) - { - _logger.LogInformation($"QueueProcessingWorker started, time {DateTime.UtcNow}"); - - var jobList = await _queuedApplicationJobRepository.GetNextJobListAsync(cancellationToken); - - if (jobList.Count == 0) - { - _logger.LogInformation($"QueueProcessingWorker found no jobs to process, time {DateTime.UtcNow}"); - return; - } - - _logger.LogInformation($"QueueProcessingWorker found {jobList.Count} jobs to process, time {DateTime.UtcNow}"); - - await _jobRunner.RunJobAsync(jobList, cancellationToken); - - _logger.LogInformation($"QueueProcessingWorker completed, time {DateTime.UtcNow}"); - } -} diff --git a/src/ApplicationControl.Client/Database/HostedServices/IDatabaseQueuedJobWorker.cs b/src/ApplicationControl.Client/Database/HostedServices/IDatabaseQueuedJobWorker.cs deleted file mode 100644 index 7b2b32c..0000000 --- a/src/ApplicationControl.Client/Database/HostedServices/IDatabaseQueuedJobWorker.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ApplicationControl.Client.Database.HostedServices; - -public interface IDatabaseQueuedJobWorker -{ - Task DoWorkAsync(CancellationToken cancellationToken); -} diff --git a/src/ApplicationControl.Client/Extensions.cs b/src/ApplicationControl.Client/Extensions.cs index 3ae8364..1913169 100644 --- a/src/ApplicationControl.Client/Extensions.cs +++ b/src/ApplicationControl.Client/Extensions.cs @@ -1,6 +1,5 @@ using ApplicationControl.Client.Configuration; using ApplicationControl.Client.Database; -using ApplicationControl.Client.Database.hostedServices; using ApplicationControl.Client.Shared; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -9,11 +8,12 @@ using ApplicationControl.Client.Database.Repositories; + namespace ApplicationControl.Client; public static class Extensions { - public static IHostBuilder AddDatabaseApplicationControl(this IHostBuilder hostBuilder) + public static IHostBuilder AddQueduedJobsApplicationControl(this IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((context, services) => @@ -21,20 +21,28 @@ public static IHostBuilder AddDatabaseApplicationControl(this IHostBuilder hostB services.Configure(context.Configuration.GetSection("ApplicationControl")); var config = context.Configuration.GetSection("ApplicationControl").Get(); - + services.AddDbContext(options => { if (config is not null) { var connectionString = config.ConnectionString; - options.UseSqlServer(connectionString); + options.UseSqlServer(connectionString); } - }); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddHostedService(); + }, ServiceLifetime.Transient); + + if (config is not null && config.ApplicationClientType == ClinetType.Database) + { + services.AddScoped(); + services.AddTransient(); + services.AddTransient(); + } + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHostedService(); + }); return hostBuilder; diff --git a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs b/src/ApplicationControl.Client/QueuedJobsHostedService.cs similarity index 55% rename from src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs rename to src/ApplicationControl.Client/QueuedJobsHostedService.cs index 46c2a91..22440a5 100644 --- a/src/ApplicationControl.Client/Database/HostedServices/DatabaseQueuedJobHostedService.cs +++ b/src/ApplicationControl.Client/QueuedJobsHostedService.cs @@ -1,15 +1,15 @@ using ApplicationControl.Client.Configuration; -using ApplicationControl.Client.Database.HostedServices; +using ApplicationControl.Client.Shared; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace ApplicationControl.Client.Database.hostedServices; +namespace ApplicationControl.Client; -public class DatabaseQueuedJobHostedService(IServiceProvider services, ILogger logger, IOptions options) : BackgroundService +public class QueuedJobsHostedService(IServiceProvider services, ILogger logger, IOptions options) : BackgroundService { - private readonly ILogger _logger = logger; + private readonly ILogger _logger = logger; private readonly ApplicationControlOptions _options = options.Value; public IServiceProvider Services { get; } = services; Guid _processId = Guid.NewGuid(); @@ -25,7 +25,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) catch (Exception ex) { var delayInSeconds = _options.QueuedHastedServiceResetProcessCycle * 1000; - _logger.LogError(ex, $"An error occurred while executing the DatabaseQueuedJobHostedService. Service will be restarted in {delayInSeconds} seconds."); + _logger.LogError(ex, $"An error occurred while executing the QueuedJobsHostedService. Service will be restarted in {delayInSeconds} seconds."); Task.Delay(delayInSeconds, stoppingToken).Wait(stoppingToken); } @@ -33,7 +33,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } internal async Task DoWorkAsync(CancellationToken cancellationToken) { - _logger.LogInformation($"QueueProcessingHostedService started, process id {_processId}, time {DateTime.UtcNow}"); + _logger.LogInformation($"QueuedJobsHostedService started, process id {_processId}, datetime {DateTime.UtcNow}"); int cycleCount = 0; while (!cancellationToken.IsCancellationRequested) { @@ -43,15 +43,15 @@ internal async Task DoWorkAsync(CancellationToken cancellationToken) { using (var scope = Services.CreateScope()) { - _logger.LogInformation($"QueueProcessingHostedService started new cycle, number {cycleCount}, process id {_processId}, scope id {cycleId}, ime {DateTime.UtcNow}"); + _logger.LogInformation($"QueuedJobsHostedService started new cycle, number {cycleCount}, process id {_processId}, cycle id {cycleId}, datetime {DateTime.UtcNow}"); - var worker = + var listner = scope.ServiceProvider - .GetRequiredService(); + .GetRequiredService(); - await worker.DoWorkAsync(cancellationToken); + await listner.ListenAsync(cancellationToken); - _logger.LogInformation($"QueueProcessingHostedService completed new cycle, number {cycleCount}, process id {_processId}, scope id {cycleId}, time {DateTime.UtcNow}"); + _logger.LogInformation($"QueuedJobsHostedService completed new cycle, number {cycleCount}, process id {_processId}, cycle id {cycleId}, datetime {DateTime.UtcNow}"); } var delayInSeconds = _options.QueuedHastedServiceCycle * 1000; @@ -60,7 +60,7 @@ internal async Task DoWorkAsync(CancellationToken cancellationToken) catch (Exception ex) { var delayInSeconds = _options.QueuedHastedResetServiceCycle * 1000; - _logger.LogError(ex, $"QueueProcessingHostedService failed cycle, number {cycleCount}, process id {_processId}, scope id {cycleId}, time {DateTime.UtcNow}. Cycle will be restarted in {delayInSeconds} seconds."); + _logger.LogError(ex, $"QueuedJobsHostedService failed, cycle number {cycleCount}, process id {_processId}, cycle id {cycleId}, datetime {DateTime.UtcNow}. Cycle will restart in {delayInSeconds} seconds."); await Task.Delay(delayInSeconds, cancellationToken); } @@ -68,7 +68,7 @@ internal async Task DoWorkAsync(CancellationToken cancellationToken) } public override async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogInformation($"QueueProcessingHostedService is stopping, process id {_processId} time {DateTime.UtcNow}"); + _logger.LogInformation($"QueuedJobsHostedService is stopping, process id {_processId} datetime {DateTime.UtcNow}"); await base.StopAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/ApplicationControl.Client/Shared/IJobRunner.cs b/src/ApplicationControl.Client/Shared/IJobRunner.cs deleted file mode 100644 index ecedd11..0000000 --- a/src/ApplicationControl.Client/Shared/IJobRunner.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ApplicationControl.Core.Extensions; - -namespace ApplicationControl.Client.Shared; - -public interface IJobRunner -{ - Task RunJobAsync(Listjobs, CancellationToken cancellationToken); -} diff --git a/src/ApplicationControl.Client/Shared/IJobWorker.cs b/src/ApplicationControl.Client/Shared/IJobWorker.cs new file mode 100644 index 0000000..d24d924 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/IJobWorker.cs @@ -0,0 +1,8 @@ +using ApplicationControl.Core.Extensions; + +namespace ApplicationControl.Client.Shared; + +public interface IJobWorker +{ + Task RunJobAsync(IJob job, CancellationToken cancellationToken = default); +} diff --git a/src/ApplicationControl.Client/Shared/IJobsListener.cs b/src/ApplicationControl.Client/Shared/IJobsListener.cs new file mode 100644 index 0000000..f254536 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/IJobsListener.cs @@ -0,0 +1,6 @@ +namespace ApplicationControl.Client.Shared; + +public interface IJobsListener +{ + Task ListenAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ApplicationControl.Client/Shared/IJobsRunner.cs b/src/ApplicationControl.Client/Shared/IJobsRunner.cs new file mode 100644 index 0000000..5d905d4 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/IJobsRunner.cs @@ -0,0 +1,8 @@ +using ApplicationControl.Core.Extensions; + +namespace ApplicationControl.Client.Shared; + +public interface IJobsRunner +{ + Task RunJobsAsync(List jobs, CancellationToken cancellationToken = default); +} diff --git a/src/ApplicationControl.Client/Shared/IJobsSource.cs b/src/ApplicationControl.Client/Shared/IJobsSource.cs new file mode 100644 index 0000000..241c5a2 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/IJobsSource.cs @@ -0,0 +1,9 @@ +using ApplicationControl.Core.Extensions; + +namespace ApplicationControl.Client.Shared; + +public interface IJobsSource +{ + Task> GetNextJobListAsync(CancellationToken cancellationToken = default); + Task SetJobStatusAsync(Guid applicationId, Guid jobId, string setBy, JobStatus jobStatus, string? message, CancellationToken cancellationToken= default); +} \ No newline at end of file diff --git a/src/ApplicationControl.Client/Shared/JobRunner.cs b/src/ApplicationControl.Client/Shared/JobRunner.cs deleted file mode 100644 index e4da01f..0000000 --- a/src/ApplicationControl.Client/Shared/JobRunner.cs +++ /dev/null @@ -1,94 +0,0 @@ - -using ApplicationControl.Client.Configuration; -using ApplicationControl.Client.Database.Repositories; -using ApplicationControl.CommandProcessor; -using ApplicationControl.Core.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ApplicationControl.Client.Shared; - -public class JobRunner : IJobRunner -{ - private readonly SemaphoreSlim _semaphore; - private readonly ILogger _logger; - private readonly ICommandProcessor _commandProcessor; - private readonly IQueuedApplicationJobRepository _queuedApplicationJobRepository; - - private readonly ApplicationControlOptions _options; - - public JobRunner(ILogger logger, ICommandProcessor commandProcessor, IQueuedApplicationJobRepository queuedApplicationJobRepository, IOptions options) - { - ArgumentNullException.ThrowIfNull(commandProcessor, nameof(commandProcessor)); - ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - ArgumentNullException.ThrowIfNull(queuedApplicationJobRepository, nameof(queuedApplicationJobRepository)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); - - // Initialize the logger - _logger = logger; - - // Initialize the logger and command processor - _commandProcessor = commandProcessor; - - // Initialize the queued application job repository - _queuedApplicationJobRepository = queuedApplicationJobRepository; - - // Initialize the options - _options = options.Value; - - // Initialize the semaphore with a maximum count of 10 - _semaphore = new SemaphoreSlim(10); - } - - public async Task RunJobAsync(List jobs, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(jobs, nameof(jobs)); - - var tasks = new List(); - var jobRunnerId = Guid.NewGuid(); - _logger.LogInformation($"JobRunner started, tatal jobs number {jobs.Count}, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); - - foreach (var job in jobs) - { - _semaphore.Wait(cancellationToken); - - // Run the job asynchronously - var task = Task.Run(async () => - { - try - { - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.InProgress, null, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation($"Executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - - await _commandProcessor.PprocessAsync(job.Command) - .ConfigureAwait(false); - - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Completed, null, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation($"Job {job.Id} with command {job.Command} executed successfully, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - catch (Exception ex) - { - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Failed, ex.Message, cancellationToken) - .ConfigureAwait(false); - - _logger.LogError(ex, $"Error executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - finally - { - // Release the semaphore when the job is done - _semaphore.Release(); - } - }, cancellationToken); - - tasks.Add(task); - } - - await Task.WhenAll(tasks); - - _logger.LogInformation($"JobRunner completed, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); - } -} diff --git a/src/ApplicationControl.Client/Shared/JobWorker.cs b/src/ApplicationControl.Client/Shared/JobWorker.cs new file mode 100644 index 0000000..d7101e8 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/JobWorker.cs @@ -0,0 +1,54 @@ +using ApplicationControl.Client.Configuration; +using ApplicationControl.CommandProcessor; +using ApplicationControl.Core.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ApplicationControl.Client.Shared; + +public class JobWorker : IJobWorker +{ + private readonly ILogger _logger; + private readonly ICommandProcessor _commandProcessor; + private readonly IJobsSource _jobsSource; + private readonly ApplicationControlOptions _options; + + private readonly Guid jobWorkerId = Guid.NewGuid(); + public JobWorker(ILogger logger, ICommandProcessor commandProcessor, IJobsSource jobsSource, IOptions options) + { + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + ArgumentNullException.ThrowIfNull(commandProcessor, nameof(commandProcessor)); + ArgumentNullException.ThrowIfNull(jobsSource, nameof(jobsSource)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + + _logger = logger; + _commandProcessor = commandProcessor; + _jobsSource = jobsSource; + _options = options.Value; + } + public async Task RunJobAsync(IJob job, CancellationToken cancellationToken = default) + { + try + { + await _jobsSource.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Worker", JobStatus.InProgress, null, cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Executing job {job.Id}, command {job.Command}, jobWorkerId {jobWorkerId}, datetime {DateTime.UtcNow}"); + + await _commandProcessor.PprocessAsync(job.Command) + .ConfigureAwait(false); + + await _jobsSource.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Worker", JobStatus.Completed, null, cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Job {job.Id}, command {job.Command} executed successfully, jobWorkerId {jobWorkerId}, datetime {DateTime.UtcNow}"); + } + catch (Exception ex) + { + await _jobsSource.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Worker", JobStatus.Failed, ex.Message, cancellationToken) + .ConfigureAwait(false); + + _logger.LogError(ex, $"Error executing job {job.Id}, command {job.Command}, jobWorkerId {jobWorkerId}, datetime {DateTime.UtcNow}"); + } + } +} \ No newline at end of file diff --git a/src/ApplicationControl.Client/Shared/JobsListener.cs b/src/ApplicationControl.Client/Shared/JobsListener.cs new file mode 100644 index 0000000..43afedf --- /dev/null +++ b/src/ApplicationControl.Client/Shared/JobsListener.cs @@ -0,0 +1,43 @@ + +using Microsoft.Extensions.Logging; + +namespace ApplicationControl.Client.Shared; + +public class JobsListener : IJobsListener +{ + private readonly IJobsSource _jobsSource; + private readonly ILogger _logger; + private readonly IJobsRunner _jobsRunner; + + public JobsListener( + IJobsSource jobsSource, + ILogger logger, + IJobsRunner jobsRunner) + { + ArgumentNullException.ThrowIfNull(jobsSource, nameof(jobsSource)); + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + ArgumentNullException.ThrowIfNull(jobsRunner, nameof(jobsRunner)); + + _jobsSource = jobsSource; + _logger = logger; + _jobsRunner = jobsRunner; + } + public async Task ListenAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation($"JobsListener started, time {DateTime.UtcNow}"); + + var jobList = await _jobsSource.GetNextJobListAsync(cancellationToken); + + if (jobList.Count == 0) + { + _logger.LogInformation($"JobsListener found no jobs to process, time {DateTime.UtcNow}"); + return; + } + + _logger.LogInformation($"JobsListener found {jobList.Count} jobs to process, time {DateTime.UtcNow}"); + + await _jobsRunner.RunJobsAsync(jobList, cancellationToken); + + _logger.LogInformation($"JobsListener completed, time {DateTime.UtcNow}"); + } +} diff --git a/src/ApplicationControl.Client/Shared/JobsRunner.cs b/src/ApplicationControl.Client/Shared/JobsRunner.cs new file mode 100644 index 0000000..72d80a8 --- /dev/null +++ b/src/ApplicationControl.Client/Shared/JobsRunner.cs @@ -0,0 +1,77 @@ +using ApplicationControl.Client.Configuration; +using ApplicationControl.Core.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ApplicationControl.Client.Shared; + +public class JobsRunner : IJobsRunner +{ + private readonly SemaphoreSlim _semaphore; + private readonly ILogger _logger; + private readonly ApplicationControlOptions _options; + private readonly IServiceProvider _services; + public JobsRunner(ILogger logger, IOptions options, IServiceProvider services) + { + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + // Initialize the logger + _logger = logger; + + // Initialize the options + _options = options.Value; + + // Initialize the service provider + _services = services; + + var maxJobsCount = _options.MaxJobsCount?? 10; + _semaphore = new SemaphoreSlim(maxJobsCount) ; + } + public async Task RunJobsAsync(List jobs, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(jobs, nameof(jobs)); + + var tasks = new List(); + var jobRunnerId = Guid.NewGuid(); + _logger.LogInformation($"JobsRunner started, tatal jobs number {jobs.Count}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + + foreach (var job in jobs) + { + _semaphore.Wait(cancellationToken); + + // Run the job asynchronously + var task = Task.Run(async () => + { + try + { + using (var scope = _services.CreateScope()) + { + var worker = + scope.ServiceProvider + .GetRequiredService(); + + await worker.RunJobAsync(job, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error executing job worker for jobId {job.Id}, command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + } + finally + { + // Release the semaphore when the job is done + _semaphore.Release(); + } + }, cancellationToken); + + tasks.Add(task); + } + + await Task.WhenAll(tasks); + + _logger.LogInformation($"JobsRunner completed, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + } +} diff --git a/src/ApplicationControl.Host/ApplicationControl.Host.csproj b/src/ApplicationControl.Host/ApplicationControl.Host.csproj new file mode 100644 index 0000000..46ebf42 --- /dev/null +++ b/src/ApplicationControl.Host/ApplicationControl.Host.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + Exe + net9.0 + enable + enable + + \ No newline at end of file diff --git a/src/ApplicationControl.Host/CommandHandlers/TestOneCommandHandler.cs b/src/ApplicationControl.Host/CommandHandlers/TestOneCommandHandler.cs new file mode 100644 index 0000000..ce523ef --- /dev/null +++ b/src/ApplicationControl.Host/CommandHandlers/TestOneCommandHandler.cs @@ -0,0 +1,22 @@ +using ApplicationControl.CommandProcessor; + +namespace ApplicationControl.Host.CommandHandlers; + +[Command("testone")] +public class TestOneCommandHandler : ICommandHandler +{ + public IServiceProvider Services { get; } + + public TestOneCommandHandler(IServiceProvider services) + { + Services = services; + } + + public async Task HandleAsync(Command command) + { + Task.Delay(1000).Wait(); + Console.WriteLine($"TestOneCommandHandler command {command.Name} excecuted "); + + Console.WriteLine(string.Empty); + } +} diff --git a/src/ApplicationControl.Host/CommandHandlers/TestThreeCommandHandler.cs b/src/ApplicationControl.Host/CommandHandlers/TestThreeCommandHandler.cs new file mode 100644 index 0000000..55787a2 --- /dev/null +++ b/src/ApplicationControl.Host/CommandHandlers/TestThreeCommandHandler.cs @@ -0,0 +1,23 @@ +using ApplicationControl.CommandProcessor; + +namespace ApplicationControl.Host.CommandHandlers; + +[Command("testthree")] +public class TestThreeCommandHandler : ICommandHandler +{ + public IServiceProvider Services { get; } + + public TestThreeCommandHandler(IServiceProvider services) + { + Services = services; + } + public async Task HandleAsync(Command command) + { + Task.Delay(5000).Wait(); + Console.WriteLine($"TestThreeCommandHandler command {command.Name} executed "); + + Console.WriteLine(string.Empty); + + throw new Exception("TestThreeCommandHandler exception"); + } +} \ No newline at end of file diff --git a/src/ApplicationControl.Host/CommandHandlers/TestTwoCommandHandler.cs b/src/ApplicationControl.Host/CommandHandlers/TestTwoCommandHandler.cs new file mode 100644 index 0000000..87aef19 --- /dev/null +++ b/src/ApplicationControl.Host/CommandHandlers/TestTwoCommandHandler.cs @@ -0,0 +1,21 @@ +using ApplicationControl.CommandProcessor; + +namespace ApplicationControl.Host.CommandHandlers; + +[Command("testtwo")] +public class TestTwoCommandHandler : ICommandHandler +{ + public IServiceProvider Services { get; } + + public TestTwoCommandHandler(IServiceProvider services) + { + Services = services; + } + public async Task HandleAsync(Command command) + { + Task.Delay(10000).Wait(); + Console.WriteLine($"TestTwoCommandHandler command {command.Name} excecuted "); + + Console.WriteLine(string.Empty); + } +} \ No newline at end of file diff --git a/src/ApplicationControl.Host/Program.cs b/src/ApplicationControl.Host/Program.cs new file mode 100644 index 0000000..72cfed3 --- /dev/null +++ b/src/ApplicationControl.Host/Program.cs @@ -0,0 +1,18 @@ +using ApplicationControl.Client; +using ApplicationControl.CommandProcessor.Extensions; +using ApplicationControl.Host.CommandHandlers; + +var host = Host.CreateDefaultBuilder(args); +host.ConfigureServices((context, services) => +{ + services.AddCommandProcessor(p=>{ + p.RegisterServicesFromAssembly(typeof(TestOneCommandHandler).Assembly); + }); +}); + +host.AddQueduedJobsApplicationControl(); + +var app = host.Build(); +await app.StartAsync(); + +await app.WaitForShutdownAsync(); \ No newline at end of file diff --git a/src/ApplicationControl.Host/appsettings.json b/src/ApplicationControl.Host/appsettings.json new file mode 100644 index 0000000..2266e5b --- /dev/null +++ b/src/ApplicationControl.Host/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ApplicationControl":{ + "ConnectionString": "Server=127.0.0.1,1433;Database=ApplicationControl;User Id=sa;Password=abc123ABC___;MultipleActiveResultSets=true;Encrypt=False;TrustServerCertificate=True;", + "ApplicationName": "ApplicationControl.Host", + "ApplicaitonId": "5c7330b9-3654-48fb-b092-787d1ab14659", + "_queuedApplicationJobRepository": "2", + + "QueuedHastedServiceResetProcessCycle": "60", + "QueuedHastedServiceCycle": "60", + "QueuedHastedResetServiceCycle": "120", + + "MaxJobsCount": "20" + } +} \ No newline at end of file diff --git a/src/scripts/tables.sql b/src/scripts/tables.sql index d269d55..eb00d79 100644 --- a/src/scripts/tables.sql +++ b/src/scripts/tables.sql @@ -49,7 +49,7 @@ go if OBJECT_ID('ApplicationControl.QueuedApplicationJob', 'U') is null begin - create table ApplicationControl.CommandQueueItem ( + create table ApplicationControl.QueuedApplicationJob ( Id UNIQUEIDENTIFIER DEFAULT NEWID() not null, Command VARCHAR(512) not null, ApplicationName VARCHAR(512) null, @@ -68,7 +68,7 @@ end go if OBJECT_ID('ApplicationControl.ScheduledApplicationJob', 'U') is null begin - create table ApplicationControl.CommandQueueItem ( + create table ApplicationControl.ScheduledApplicationJob ( Id UNIQUEIDENTIFIER DEFAULT NEWID() not null, Command VARCHAR(512) not null, ApplicationName VARCHAR(512) null, From cc39d5f2669437c5bc952fca5a22ffcbf41aa53f Mon Sep 17 00:00:00 2001 From: ad Date: Tue, 27 May 2025 20:22:47 -0700 Subject: [PATCH 5/7] work in progress --- src/ApplicationControl.Client/Shared/JobsRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApplicationControl.Client/Shared/JobsRunner.cs b/src/ApplicationControl.Client/Shared/JobsRunner.cs index 72d80a8..6023bf7 100644 --- a/src/ApplicationControl.Client/Shared/JobsRunner.cs +++ b/src/ApplicationControl.Client/Shared/JobsRunner.cs @@ -58,7 +58,7 @@ public async Task RunJobsAsync(List jobs, CancellationToken cancellationTo } catch (Exception ex) { - _logger.LogError(ex, $"Error executing job worker for jobId {job.Id}, command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); + _logger.LogError(ex, $"Error executing job worker, jobId {job.Id}, command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); } finally { From 04f7aeb249f129247176950369da7d9cfb9294f9 Mon Sep 17 00:00:00 2001 From: ad Date: Wed, 28 May 2025 12:44:33 -0700 Subject: [PATCH 6/7] work in progress --- .../Shared/JobRunner.cs | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 src/ApplicationControl.Client/Shared/JobRunner.cs diff --git a/src/ApplicationControl.Client/Shared/JobRunner.cs b/src/ApplicationControl.Client/Shared/JobRunner.cs deleted file mode 100644 index e4da01f..0000000 --- a/src/ApplicationControl.Client/Shared/JobRunner.cs +++ /dev/null @@ -1,94 +0,0 @@ - -using ApplicationControl.Client.Configuration; -using ApplicationControl.Client.Database.Repositories; -using ApplicationControl.CommandProcessor; -using ApplicationControl.Core.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ApplicationControl.Client.Shared; - -public class JobRunner : IJobRunner -{ - private readonly SemaphoreSlim _semaphore; - private readonly ILogger _logger; - private readonly ICommandProcessor _commandProcessor; - private readonly IQueuedApplicationJobRepository _queuedApplicationJobRepository; - - private readonly ApplicationControlOptions _options; - - public JobRunner(ILogger logger, ICommandProcessor commandProcessor, IQueuedApplicationJobRepository queuedApplicationJobRepository, IOptions options) - { - ArgumentNullException.ThrowIfNull(commandProcessor, nameof(commandProcessor)); - ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - ArgumentNullException.ThrowIfNull(queuedApplicationJobRepository, nameof(queuedApplicationJobRepository)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); - - // Initialize the logger - _logger = logger; - - // Initialize the logger and command processor - _commandProcessor = commandProcessor; - - // Initialize the queued application job repository - _queuedApplicationJobRepository = queuedApplicationJobRepository; - - // Initialize the options - _options = options.Value; - - // Initialize the semaphore with a maximum count of 10 - _semaphore = new SemaphoreSlim(10); - } - - public async Task RunJobAsync(List jobs, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(jobs, nameof(jobs)); - - var tasks = new List(); - var jobRunnerId = Guid.NewGuid(); - _logger.LogInformation($"JobRunner started, tatal jobs number {jobs.Count}, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); - - foreach (var job in jobs) - { - _semaphore.Wait(cancellationToken); - - // Run the job asynchronously - var task = Task.Run(async () => - { - try - { - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.InProgress, null, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation($"Executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - - await _commandProcessor.PprocessAsync(job.Command) - .ConfigureAwait(false); - - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Completed, null, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation($"Job {job.Id} with command {job.Command} executed successfully, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - catch (Exception ex) - { - await _queuedApplicationJobRepository.SetJobStatusAsync(_options.ApplicaitonId, job.Id, "Job Runner", JobStatus.Failed, ex.Message, cancellationToken) - .ConfigureAwait(false); - - _logger.LogError(ex, $"Error executing job {job.Id} with command {job.Command}, jobRunnerId {jobRunnerId}, datetime {DateTime.UtcNow}"); - } - finally - { - // Release the semaphore when the job is done - _semaphore.Release(); - } - }, cancellationToken); - - tasks.Add(task); - } - - await Task.WhenAll(tasks); - - _logger.LogInformation($"JobRunner completed, jobRunnerId {jobRunnerId}, time {DateTime.UtcNow}"); - } -} From 6b06d7f411cb770496cba38d3714093f53332285 Mon Sep 17 00:00:00 2001 From: ad Date: Wed, 28 May 2025 17:05:00 -0700 Subject: [PATCH 7/7] work in progress