diff --git a/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs b/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs new file mode 100644 index 0000000..ead8be1 --- /dev/null +++ b/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Options; + +namespace ECMJobRunner.WebCron.ProfileWorker; + +public static class DependencyInjection +{ + public static IServiceCollection AddProfileWorker(this IServiceCollection services, IConfiguration configuration) + { + // Configure ProfileWorker options from appsettings.json + services.Configure( + configuration.GetSection(ProfileWorkerOptions.SectionName)); + + // Validate options at startup + services.AddSingleton, ProfileWorkerOptionsValidator>(); + + services.AddHostedService(); + services.AddSingleton(); + services.AddScoped(); + return services; + } +} + +/// +/// Validates ProfileWorkerOptions configuration at startup. +/// +internal class ProfileWorkerOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ProfileWorkerOptions options) + { + if (options.IntervalMS <= 0) + { + return ValidateOptionsResult.Fail("ProfileWorker:IntervalMs must be greater than 0"); + } + + if (options.IntervalMS < 100) + { + return ValidateOptionsResult.Fail("ProfileWorker:IntervalMs should be at least 100ms to avoid excessive CPU usage"); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs new file mode 100644 index 0000000..70ac756 --- /dev/null +++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs @@ -0,0 +1,23 @@ +using ECMJobRunner.Application.Common.Dtos; +using System.Collections.Concurrent; + +namespace ECMJobRunner.WebCron.ProfileWorker; + +/// +/// Thread-safe cache for storing active profile configurations. +/// Used to track profile state and detect changes in schedule or removal. +/// +public class ProfileCache +{ + private readonly ConcurrentDictionary _cache = new(); + + public CfgProfileDto? Get(string jobId) => _cache.TryGetValue(jobId, out var profile) ? profile : null; + + public void AddOrUpdate(string jobId, CfgProfileDto profile) => _cache[jobId] = profile; + + public bool TryRemove(string jobId, out CfgProfileDto? profile) => _cache.TryRemove(jobId, out profile); + + public IEnumerable GetAllJobIds() => _cache.Keys; + + public bool TryGetValue(string jobId, out CfgProfileDto? profile) => _cache.TryGetValue(jobId, out profile); +} diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs new file mode 100644 index 0000000..982e9da --- /dev/null +++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Options; + +namespace ECMJobRunner.WebCron.ProfileWorker; + +public class ProfileWorker( + ILogger Logger, + IServiceScopeFactory ScopeFactory, + IOptions Options) : BackgroundService +{ + private readonly ProfileWorkerOptions _options = Options.Value; + + private int _workCount = 0; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + _workCount++; + if (Logger.IsEnabled(LogLevel.Information)) + { + Logger.LogInformation("Worker running {workCount} at: {time}", _workCount, DateTimeOffset.Now); + } + + try + { + // Create a scope to resolve scoped services (ISQLExecutor used by MediatR pipeline) + using var scope = ScopeFactory.CreateScope(); + var work = scope.ServiceProvider.GetRequiredService(); + await work.ExecuteAsync(stoppingToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "An unexpected error occurred in ProfileWorker. Work count: {workCount}", _workCount); + } + + await Task.Delay(_options.IntervalMS, stoppingToken); + } + } +} diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs new file mode 100644 index 0000000..c69d367 --- /dev/null +++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs @@ -0,0 +1,15 @@ +namespace ECMJobRunner.WebCron.ProfileWorker; + +/// +/// Configuration options for ProfileWorker background service. +/// +public class ProfileWorkerOptions +{ + public const string SectionName = "ProfileWorker"; + + /// + /// Interval in milliseconds between profile synchronization checks. + /// Default: 1000ms (1 second) + /// + public int IntervalMS { get; set; } = 1000; +}