diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 8200be023f..0b53a09c48 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -120,17 +120,17 @@ So, it resolves the given background worker and adds to the `IBackgroundWorkerMa While we generally add workers in `OnApplicationInitializationAsync`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. -### Add Dynamic Workers at Runtime (Handler in Add) +### Dynamic Workers (Runtime Registration) -You can add a runtime worker without pre-defining a dedicated worker class by passing a handler directly to `AddAsync`. +You can add a runtime worker without pre-defining a dedicated worker class by injecting `IDynamicBackgroundWorkerManager` and passing a handler directly. ```csharp -await backgroundWorkerManager.AddAsync( +await dynamicBackgroundWorkerManager.AddAsync( "InventorySyncWorker", new DynamicBackgroundWorkerSchedule { Period = 30000 // 30 seconds - // CronExpression = "*/30 * * * * *" // optional (provider dependent) + // CronExpression = "*/30 * * * *" // every 30 minutes, supported by Hangfire, Quartz, TickerQ }, async (context, cancellationToken) => { @@ -144,10 +144,10 @@ You can also **remove** a dynamic worker or **update its schedule** at runtime: ```csharp // Remove a dynamic worker -var removed = await backgroundWorkerManager.RemoveAsync("InventorySyncWorker"); +var removed = await dynamicBackgroundWorkerManager.RemoveAsync("InventorySyncWorker"); // Update the schedule of a dynamic worker -var updated = await backgroundWorkerManager.UpdateScheduleAsync( +var updated = await dynamicBackgroundWorkerManager.UpdateScheduleAsync( "InventorySyncWorker", new DynamicBackgroundWorkerSchedule { @@ -158,10 +158,11 @@ var updated = await backgroundWorkerManager.UpdateScheduleAsync( Key points: +* `IDynamicBackgroundWorkerManager` is a separate interface from `IBackgroundWorkerManager`, dedicated to runtime (non-type-safe) worker management. * `workerName` is the runtime identifier of the dynamic worker. If a worker with the same name already exists, it will be replaced. -* The `handler` is registered at runtime and executed through the provider-specific worker manager. -* Provider behavior is preserved. For example, providers with persistent schedulers keep their own scheduling semantics. -* The default in-process manager uses in-memory periodic execution based on `Period`. **`CronExpression` is only supported by scheduler-backed providers (Hangfire, Quartz, TickerQ).** The default in-memory provider ignores `CronExpression`. +* The `handler` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. +* At least one of `Period` or `CronExpression` must be set in `DynamicBackgroundWorkerSchedule`. +* **`CronExpression` is only supported by scheduler-backed providers (Hangfire, Quartz, TickerQ).** The default in-memory provider requires `Period` and does not support `CronExpression` alone. * `RemoveAsync` stops and removes a dynamic worker. Returns `true` if the worker was found and removed. * `UpdateScheduleAsync` changes the schedule of an existing dynamic worker. Returns `true` if the worker was found and updated. The handler itself is not changed. diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs index fa54ab5538..c17ccdc443 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Volo.Abp.BackgroundJobs; diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs index e8a840035f..6b6d822c50 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs index 4ef2d2c19d..ae7e098070 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using Volo.Abp.DependencyInjection; diff --git a/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs b/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs index d2ac1ed9cc..8790243334 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Hangfire; using Microsoft.Extensions.DependencyInjection; diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index dff2a4e33a..ddc2613b47 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Threading; diff --git a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs index 4ff3adbfe4..157f0cc050 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.DependencyInjection; diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs index 159f11a6d3..a015e32d66 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index 12fd49db04..64a4a1be64 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs @@ -9,7 +9,6 @@ using Hangfire.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Volo.Abp.BackgroundWorkers; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; using Volo.Abp.Hangfire; @@ -21,12 +20,11 @@ namespace Volo.Abp.BackgroundWorkers.Hangfire; public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISingletonDependency { protected AbpHangfireBackgroundJobServer BackgroundJobServer { get; set; } = default!; + protected IServiceProvider ServiceProvider { get; } - public HangfireBackgroundWorkerManager( - IServiceProvider serviceProvider, - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) - : base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry) + public HangfireBackgroundWorkerManager(IServiceProvider serviceProvider) { + ServiceProvider = serviceProvider; } public void Initialize() @@ -139,142 +137,6 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet } } - public override Task AddAsync( - string workerName, - Func handler, - CancellationToken cancellationToken = default) - { - return AddAsync( - workerName, - new DynamicBackgroundWorkerSchedule - { - Period = DynamicBackgroundWorkerSchedule.DefaultPeriod - }, - handler, - cancellationToken - ); - } - - public override Task AddAsync( - string workerName, - DynamicBackgroundWorkerSchedule schedule, - Func handler, - CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - Check.NotNull(handler, nameof(handler)); - - schedule.Validate(); - - var cronExpression = schedule.CronExpression; - if (cronExpression.IsNullOrWhiteSpace()) - { - var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; - cronExpression = GetCron(period); - } - - var logger = ServiceProvider.GetRequiredService>(); - var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; - var queueName = abpHangfireOptions.DefaultQueue; - var recurringJobId = $"DynamicWorker:{workerName}"; - - if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) - { - logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead."); - RecurringJob.AddOrUpdate( - recurringJobId, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), - cronExpression, - new RecurringJobOptions - { - TimeZone = TimeZoneInfo.Utc - }); - } - else - { - RecurringJob.AddOrUpdate( - recurringJobId, - queueName, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), - cronExpression, - new RecurringJobOptions - { - TimeZone = TimeZoneInfo.Utc - }); - } - - DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); - - return Task.CompletedTask; - } - - public override Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return Task.FromResult(false); - } - - var recurringJobId = $"DynamicWorker:{workerName}"; - RecurringJob.RemoveIfExists(recurringJobId); - DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); - - return Task.FromResult(true); - } - - public override Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - - schedule.Validate(); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return Task.FromResult(false); - } - - var cronExpression = schedule.CronExpression; - if (cronExpression.IsNullOrWhiteSpace()) - { - var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; - cronExpression = GetCron(period); - } - - var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; - var queueName = abpHangfireOptions.DefaultQueue; - var recurringJobId = $"DynamicWorker:{workerName}"; - - if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) - { - RecurringJob.AddOrUpdate( - recurringJobId, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), - cronExpression, - new RecurringJobOptions - { - TimeZone = TimeZoneInfo.Utc - }); - } - else - { - RecurringJob.AddOrUpdate( - recurringJobId, - queueName, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), - cronExpression, - new RecurringJobOptions - { - TimeZone = TimeZoneInfo.Utc - }); - } - - return Task.FromResult(true); - } - private static readonly MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static); protected virtual string? GetRecurringJobId(IBackgroundWorker worker, Expression> methodCall) { diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs index 6ae84da915..9e6d5f2f0c 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs @@ -1,31 +1,48 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.BackgroundWorkers.Hangfire; public class HangfireDynamicBackgroundWorkerAdapter : ITransientDependency { - protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } public HangfireDynamicBackgroundWorkerAdapter( - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry, IServiceProvider serviceProvider) { - DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; + HandlerRegistry = handlerRegistry; ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; } public virtual async Task DoWorkAsync(string workerName, CancellationToken cancellationToken = default) { - var handler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName); + var handler = HandlerRegistry.Get(workerName); if (handler == null) { + Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName); return; } - await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken); + try + { + await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken); + } + catch (Exception ex) + { + await ServiceProvider.GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + throw; + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..53820d0522 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Hangfire.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Hangfire; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +[Dependency(ReplaceServices = true)] +public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + protected IServiceProvider ServiceProvider { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public ILogger Logger { get; set; } + + public HangfireDynamicBackgroundWorkerManager( + IServiceProvider serviceProvider, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + { + ServiceProvider = serviceProvider; + HandlerRegistry = handlerRegistry; + Logger = NullLogger.Instance; + } + + public virtual Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + var cronExpression = schedule.CronExpression; + if (cronExpression.IsNullOrWhiteSpace()) + { + var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + cronExpression = GetCron(period); + } + + ScheduleRecurringJob(workerName, cronExpression, cancellationToken); + HandlerRegistry.Register(workerName, handler); + + return Task.CompletedTask; + } + + public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return Task.FromResult(false); + } + + var recurringJobId = $"DynamicWorker:{workerName}"; + RecurringJob.RemoveIfExists(recurringJobId); + HandlerRegistry.Unregister(workerName); + + return Task.FromResult(true); + } + + public virtual Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return Task.FromResult(false); + } + + var cronExpression = schedule.CronExpression; + if (cronExpression.IsNullOrWhiteSpace()) + { + var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + cronExpression = GetCron(period); + } + + ScheduleRecurringJob(workerName, cronExpression, cancellationToken); + + return Task.FromResult(true); + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return HandlerRegistry.IsRegistered(workerName); + } + + protected virtual void ScheduleRecurringJob(string workerName, string cronExpression, CancellationToken cancellationToken) + { + var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; + var queueName = abpHangfireOptions.DefaultQueue; + var recurringJobId = $"DynamicWorker:{workerName}"; + + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + Logger.LogWarning( + "Current storage doesn't support specifying queues ({QueueName}) directly for a specific job. Please use the QueueAttribute instead.", + queueName); + + RecurringJob.AddOrUpdate( + recurringJobId, + adapter => adapter.DoWorkAsync(workerName, cancellationToken), + cronExpression, + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Utc + }); + } + else + { + RecurringJob.AddOrUpdate( + recurringJobId, + queueName, + adapter => adapter.DoWorkAsync(workerName, cancellationToken), + cronExpression, + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Utc + }); + } + } + + protected virtual string GetCron(int period) + { + var time = TimeSpan.FromMilliseconds(period); + string cron; + + if (time.TotalSeconds <= 59) + { + cron = $"*/{time.TotalSeconds} * * * * *"; + } + else if (time.TotalMinutes <= 59) + { + cron = $"*/{time.TotalMinutes} * * * *"; + } + else if (time.TotalHours <= 23) + { + cron = $"0 */{time.TotalHours} * * *"; + } + else if (time.TotalDays <= 31) + { + cron = $"0 0 0 1/{time.TotalDays} * *"; + } + else + { + throw new AbpException($"Cannot convert period: {period} to cron expression."); + } + + return cron; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs index d963106e02..9854379c79 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Quartz; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -11,15 +10,9 @@ namespace Volo.Abp.BackgroundWorkers.Quartz; [Dependency(ReplaceServices = true)] public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingletonDependency { - public const string DynamicWorkerNameKey = "AbpDynamicWorkerName"; - protected IScheduler Scheduler { get; } - public QuartzBackgroundWorkerManager( - IScheduler scheduler, - IServiceProvider serviceProvider, - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) - : base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry) + public QuartzBackgroundWorkerManager(IScheduler scheduler) { Scheduler = scheduler; } @@ -103,130 +96,4 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton await Scheduler.ScheduleJob(quartzWork.JobDetail, quartzWork.Trigger, cancellationToken); } } - - public override Task AddAsync( - string workerName, - Func handler, - CancellationToken cancellationToken = default) - { - return AddAsync( - workerName, - new DynamicBackgroundWorkerSchedule - { - Period = DynamicBackgroundWorkerSchedule.DefaultPeriod - }, - handler, - cancellationToken - ); - } - - public override async Task AddAsync( - string workerName, - DynamicBackgroundWorkerSchedule schedule, - Func handler, - CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - Check.NotNull(handler, nameof(handler)); - - schedule.Validate(); - - if (schedule.Period == null && schedule.CronExpression.IsNullOrWhiteSpace()) - { - throw new AbpException($"Both 'Period' and 'CronExpression' are not set for dynamic worker {workerName}. You must set at least one of them."); - } - - var jobKey = new JobKey($"DynamicWorker:{workerName}"); - var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); - var jobDetail = JobBuilder.Create() - .WithIdentity(jobKey) - .UsingJobData(DynamicWorkerNameKey, workerName) - .Build(); - - var triggerBuilder = TriggerBuilder.Create() - .ForJob(jobDetail) - .WithIdentity(triggerKey); - - if (!schedule.CronExpression.IsNullOrWhiteSpace()) - { - triggerBuilder.WithCronSchedule(schedule.CronExpression); - } - else - { - triggerBuilder.WithSimpleSchedule(builder => - builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); - } - - var trigger = triggerBuilder.Build(); - - if (await Scheduler.CheckExists(jobDetail.Key, cancellationToken)) - { - await Scheduler.AddJob(jobDetail, true, true, cancellationToken); - await Scheduler.ResumeJob(jobDetail.Key, cancellationToken); - await Scheduler.RescheduleJob(trigger.Key, trigger, cancellationToken); - } - else - { - await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken); - } - - DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); - } - - public override async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return false; - } - - var jobKey = new JobKey($"DynamicWorker:{workerName}"); - var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken); - if (deleted) - { - DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); - } - - return deleted; - } - - public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - - schedule.Validate(); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return false; - } - - if (schedule.Period == null && schedule.CronExpression.IsNullOrWhiteSpace()) - { - throw new AbpException($"Both 'Period' and 'CronExpression' are not set for dynamic worker {workerName}. You must set at least one of them."); - } - - var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); - var triggerBuilder = TriggerBuilder.Create() - .WithIdentity(triggerKey) - .ForJob(new JobKey($"DynamicWorker:{workerName}")); - - if (!schedule.CronExpression.IsNullOrWhiteSpace()) - { - triggerBuilder.WithCronSchedule(schedule.CronExpression); - } - else - { - triggerBuilder.WithSimpleSchedule(builder => - builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); - } - - var result = await Scheduler.RescheduleJob(triggerKey, triggerBuilder.Build(), cancellationToken); - - return result != null; - } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs index f0c9e09ad8..ba8d98091f 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs @@ -1,41 +1,56 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Quartz; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.BackgroundWorkers.Quartz; public class QuartzDynamicBackgroundWorkerAdapter : IJob, ITransientDependency { - protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } public QuartzDynamicBackgroundWorkerAdapter( - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry, IServiceProvider serviceProvider) { - DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; + HandlerRegistry = handlerRegistry; ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; } public virtual async Task Execute(IJobExecutionContext context) { - var workerName = context.MergedJobDataMap.GetString(QuartzBackgroundWorkerManager.DynamicWorkerNameKey); + var workerName = context.MergedJobDataMap.GetString(QuartzDynamicBackgroundWorkerManager.DynamicWorkerNameKey); if (string.IsNullOrWhiteSpace(workerName)) { return; } - var nonNullWorkerName = workerName!; - var handler = DynamicBackgroundWorkerHandlerRegistry.Get(nonNullWorkerName); + var handler = HandlerRegistry.Get(workerName!); if (handler == null) { + Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName); return; } - await handler( - new DynamicBackgroundWorkerExecutionContext(nonNullWorkerName, ServiceProvider), - context.CancellationToken - ); + try + { + await handler( + new DynamicBackgroundWorkerExecutionContext(workerName!, ServiceProvider), + context.CancellationToken); + } + catch (Exception ex) + { + await ServiceProvider.GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + throw; + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..57fdda79eb --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs @@ -0,0 +1,143 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Quartz; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.Quartz; + +[Dependency(ReplaceServices = true)] +public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + public const string DynamicWorkerNameKey = "AbpDynamicWorkerName"; + + protected IScheduler Scheduler { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public ILogger Logger { get; set; } + + public QuartzDynamicBackgroundWorkerManager( + IScheduler scheduler, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + { + Scheduler = scheduler; + HandlerRegistry = handlerRegistry; + Logger = NullLogger.Instance; + } + + public virtual async Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); + var jobDetail = JobBuilder.Create() + .WithIdentity(jobKey) + .UsingJobData(DynamicWorkerNameKey, workerName) + .Build(); + + var trigger = BuildTrigger(schedule, jobDetail, triggerKey); + + if (await Scheduler.CheckExists(jobDetail.Key, cancellationToken)) + { + await Scheduler.AddJob(jobDetail, true, true, cancellationToken); + await Scheduler.ResumeJob(jobDetail.Key, cancellationToken); + await Scheduler.RescheduleJob(trigger.Key, trigger, cancellationToken); + } + else + { + await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken); + } + + HandlerRegistry.Register(workerName, handler); + } + + public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return false; + } + + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken); + if (deleted) + { + HandlerRegistry.Unregister(workerName); + } + + return deleted; + } + + public virtual async Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return false; + } + + var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + + var triggerBuilder = TriggerBuilder.Create() + .WithIdentity(triggerKey) + .ForJob(jobKey); + + if (!schedule.CronExpression.IsNullOrWhiteSpace()) + { + triggerBuilder.WithCronSchedule(schedule.CronExpression); + } + else + { + triggerBuilder.WithSimpleSchedule(builder => + builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); + } + + var result = await Scheduler.RescheduleJob(triggerKey, triggerBuilder.Build(), cancellationToken); + return result != null; + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return HandlerRegistry.IsRegistered(workerName); + } + + protected virtual ITrigger BuildTrigger(DynamicBackgroundWorkerSchedule schedule, IJobDetail jobDetail, TriggerKey triggerKey) + { + var triggerBuilder = TriggerBuilder.Create() + .ForJob(jobDetail) + .WithIdentity(triggerKey); + + if (!schedule.CronExpression.IsNullOrWhiteSpace()) + { + triggerBuilder.WithCronSchedule(schedule.CronExpression); + } + else + { + triggerBuilder.WithSimpleSchedule(builder => + builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); + } + + return triggerBuilder.Build(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs index 1fbe1c8ad0..cc6c847197 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs @@ -1,11 +1,8 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using TickerQ.Utilities.Entities; using TickerQ.Utilities.Enums; -using TickerQ.Utilities.Interfaces.Managers; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; using Volo.Abp.TickerQ; @@ -17,21 +14,15 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl { protected AbpTickerQFunctionProvider AbpTickerQFunctionProvider { get; } protected AbpTickerQBackgroundWorkersProvider AbpTickerQBackgroundWorkersProvider { get; } - protected ICronTickerManager CronTickerManager { get; } protected AbpBackgroundWorkersTickerQOptions Options { get; } public AbpTickerQBackgroundWorkerManager( AbpTickerQFunctionProvider abpTickerQFunctionProvider, AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider, - ICronTickerManager cronTickerManager, - IServiceProvider serviceProvider, - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry, IOptions options) - : base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry) { AbpTickerQFunctionProvider = abpTickerQFunctionProvider; AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider; - CronTickerManager = cronTickerManager; Options = options.Value; } @@ -79,114 +70,6 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl await base.AddAsync(worker, cancellationToken); } - public override Task AddAsync( - string workerName, - Func handler, - CancellationToken cancellationToken = default) - { - return AddAsync( - workerName, - new DynamicBackgroundWorkerSchedule - { - Period = DynamicBackgroundWorkerSchedule.DefaultPeriod - }, - handler, - cancellationToken - ); - } - - public override async Task AddAsync( - string workerName, - DynamicBackgroundWorkerSchedule schedule, - Func handler, - CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - Check.NotNull(handler, nameof(handler)); - - schedule.Validate(); - - var cronExpression = schedule.CronExpression ?? GetCron(schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod); - var functionName = $"DynamicWorker:{workerName}"; - - AbpTickerQFunctionProvider.Functions[functionName] = - (string.Empty, TickerTaskPriority.LongRunning, async (tickerCancellationToken, serviceProvider, _) => - { - var registeredHandler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName); - if (registeredHandler == null) - { - return; - } - - await registeredHandler( - new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider), - tickerCancellationToken - ); - }); - - AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker - { - Function = functionName, - CronExpression = cronExpression, - WorkerType = typeof(AbpTickerQBackgroundWorkerManager) - }; - - await CronTickerManager.AddAsync(new CronTickerEntity - { - Function = functionName, - Expression = cronExpression - }); - - DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); - } - - public override Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return Task.FromResult(false); - } - - var functionName = $"DynamicWorker:{workerName}"; - AbpTickerQFunctionProvider.Functions.Remove(functionName); - AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); - DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); - - return Task.FromResult(true); - } - - public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - - schedule.Validate(); - - if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) - { - return false; - } - - var cronExpression = schedule.CronExpression ?? GetCron(schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod); - var functionName = $"DynamicWorker:{workerName}"; - - if (AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.TryGetValue(functionName, out var existingWorker)) - { - existingWorker.CronExpression = cronExpression; - } - - await CronTickerManager.AddAsync(new CronTickerEntity - { - Function = functionName, - Expression = cronExpression - }); - - return true; - } - protected virtual string GetCron(int period) { var time = TimeSpan.FromMilliseconds(period); diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..efe8f3eebf --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using TickerQ.Utilities.Entities; +using TickerQ.Utilities.Enums; +using TickerQ.Utilities.Interfaces.Managers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.TickerQ; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +[Dependency(ReplaceServices = true)] +public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + protected AbpTickerQFunctionProvider AbpTickerQFunctionProvider { get; } + protected AbpTickerQBackgroundWorkersProvider AbpTickerQBackgroundWorkersProvider { get; } + protected ICronTickerManager CronTickerManager { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public ILogger Logger { get; set; } + + public TickerQDynamicBackgroundWorkerManager( + AbpTickerQFunctionProvider abpTickerQFunctionProvider, + AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider, + ICronTickerManager cronTickerManager, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + { + AbpTickerQFunctionProvider = abpTickerQFunctionProvider; + AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider; + CronTickerManager = cronTickerManager; + HandlerRegistry = handlerRegistry; + Logger = NullLogger.Instance; + } + + public virtual async Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + var cronExpression = schedule.CronExpression ?? GetCron(schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod); + var functionName = $"DynamicWorker:{workerName}"; + + AbpTickerQFunctionProvider.Functions[functionName] = + (string.Empty, TickerTaskPriority.LongRunning, async (tickerCancellationToken, serviceProvider, _) => + { + var registeredHandler = HandlerRegistry.Get(workerName); + if (registeredHandler == null) + { + return; + } + + await registeredHandler( + new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider), + tickerCancellationToken); + }, 0); + + AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker + { + Function = functionName, + CronExpression = cronExpression, + WorkerType = typeof(TickerQDynamicBackgroundWorkerManager) + }; + + await CronTickerManager.AddAsync(new CronTickerEntity + { + Function = functionName, + Expression = cronExpression + }); + + HandlerRegistry.Register(workerName, handler); + } + + public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return Task.FromResult(false); + } + + var functionName = $"DynamicWorker:{workerName}"; + AbpTickerQFunctionProvider.Functions.Remove(functionName); + AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); + HandlerRegistry.Unregister(workerName); + + return Task.FromResult(true); + } + + public virtual async Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + if (!HandlerRegistry.IsRegistered(workerName)) + { + return false; + } + + var cronExpression = schedule.CronExpression ?? GetCron(schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod); + var functionName = $"DynamicWorker:{workerName}"; + + if (AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.TryGetValue(functionName, out var existingWorker)) + { + existingWorker.CronExpression = cronExpression; + } + + await CronTickerManager.AddAsync(new CronTickerEntity + { + Function = functionName, + Expression = cronExpression + }); + + return true; + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return HandlerRegistry.IsRegistered(workerName); + } + + protected virtual string GetCron(int period) + { + var time = TimeSpan.FromMilliseconds(period); + if (time.TotalMinutes < 1) + { + return "* * * * *"; + } + + if (time.TotalMinutes < 60) + { + var minutes = (int)Math.Round(time.TotalMinutes); + return $"*/{minutes} * * * *"; + } + + if (time.TotalHours < 24) + { + var hours = (int)Math.Round(time.TotalHours); + return $"0 */{hours} * * *"; + } + + if (time.TotalDays <= 31) + { + var days = (int)Math.Round(time.TotalDays); + return $"0 0 */{days} * *"; + } + + throw new AbpException($"Cannot convert period: {period} to cron expression."); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs index 84640ff1bf..8d05ce1239 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Concurrent; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; -using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers; @@ -19,21 +16,13 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen private bool _isDisposed; private readonly List _backgroundWorkers; - private readonly ConcurrentDictionary _dynamicWorkers; - protected IServiceProvider ServiceProvider { get; } - protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } /// /// Initializes a new instance of the class. /// - public BackgroundWorkerManager( - IServiceProvider serviceProvider, - IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) + public BackgroundWorkerManager() { _backgroundWorkers = new List(); - _dynamicWorkers = new ConcurrentDictionary(); - ServiceProvider = serviceProvider; - DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; } public virtual async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) @@ -46,127 +35,6 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen } } - public virtual Task AddAsync( - string workerName, - Func handler, - CancellationToken cancellationToken = default) - { - return AddAsync( - workerName, - new DynamicBackgroundWorkerSchedule - { - Period = DynamicBackgroundWorkerSchedule.DefaultPeriod - }, - handler, - cancellationToken - ); - } - - public virtual async Task AddAsync( - string workerName, - DynamicBackgroundWorkerSchedule schedule, - Func handler, - CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - Check.NotNull(handler, nameof(handler)); - - schedule.Validate(); - - if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) - { - throw new AbpException("Default background worker manager does not support cron expression without period."); - } - - if (_dynamicWorkers.TryRemove(workerName, out var existingWorker)) - { - await existingWorker.StopAsync(cancellationToken); - _backgroundWorkers.Remove(existingWorker); - } - - DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); - - var timer = ServiceProvider.GetRequiredService(); - var serviceScopeFactory = ServiceProvider.GetRequiredService(); - var worker = new InMemoryDynamicBackgroundWorker( - workerName, - schedule, - timer, - serviceScopeFactory, - DynamicBackgroundWorkerHandlerRegistry - ); - worker.ServiceProvider = ServiceProvider; - worker.LazyServiceProvider = ServiceProvider.GetRequiredService(); - - _dynamicWorkers[workerName] = worker; - - await AddAsync(worker, cancellationToken); - } - - public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - - if (!_dynamicWorkers.TryGetValue(workerName, out var worker)) - { - return false; - } - - await worker.StopAsync(cancellationToken); - _backgroundWorkers.Remove(worker); - _dynamicWorkers.TryRemove(workerName, out _); - DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); - - return true; - } - - public virtual async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) - { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - Check.NotNull(schedule, nameof(schedule)); - - schedule.Validate(); - - if (!_dynamicWorkers.TryGetValue(workerName, out var oldWorker)) - { - return false; - } - - if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) - { - throw new AbpException("Default background worker manager does not support cron expression without period."); - } - - var handler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName); - if (handler == null) - { - return false; - } - - await oldWorker.StopAsync(cancellationToken); - _backgroundWorkers.Remove(oldWorker); - _dynamicWorkers.TryRemove(workerName, out _); - - var timer = ServiceProvider.GetRequiredService(); - var serviceScopeFactory = ServiceProvider.GetRequiredService(); - var newWorker = new InMemoryDynamicBackgroundWorker( - workerName, - schedule, - timer, - serviceScopeFactory, - DynamicBackgroundWorkerHandlerRegistry - ); - newWorker.ServiceProvider = ServiceProvider; - newWorker.LazyServiceProvider = ServiceProvider.GetRequiredService(); - - _dynamicWorkers[workerName] = newWorker; - - await AddAsync(newWorker, cancellationToken); - - return true; - } - public virtual void Dispose() { if (_isDisposed) diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..3d239215dc --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundWorkers; + +public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency, IDisposable +{ + protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } + + private readonly ConcurrentDictionary _dynamicWorkers; + private bool _isDisposed; + + public DefaultDynamicBackgroundWorkerManager(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; + _dynamicWorkers = new ConcurrentDictionary(); + } + + public virtual async Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) + { + throw new AbpException( + $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + + "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); + } + + if (_dynamicWorkers.TryRemove(workerName, out var existingWorker)) + { + await existingWorker.StopAsync(cancellationToken); + Logger.LogInformation("Replaced existing dynamic worker: {WorkerName}", workerName); + } + + var worker = CreateDynamicWorker(workerName, schedule, handler); + _dynamicWorkers[workerName] = worker; + + await worker.StartAsync(cancellationToken); + } + + public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + if (!_dynamicWorkers.TryRemove(workerName, out var worker)) + { + return false; + } + + await worker.StopAsync(cancellationToken); + return true; + } + + public virtual Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) + { + throw new AbpException( + $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + + "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); + } + + if (!_dynamicWorkers.TryGetValue(workerName, out var worker)) + { + return Task.FromResult(false); + } + + worker.UpdateSchedule(schedule); + return Task.FromResult(true); + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return _dynamicWorkers.ContainsKey(workerName); + } + + public virtual void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + foreach (var kvp in _dynamicWorkers) + { + try + { + kvp.Value.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Logger.LogException(ex); + } + } + + _dynamicWorkers.Clear(); + } + + protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler) + { + var timer = ServiceProvider.GetRequiredService(); + var serviceScopeFactory = ServiceProvider.GetRequiredService(); + + var worker = new InMemoryDynamicBackgroundWorker( + workerName, schedule, handler, timer, serviceScopeFactory); + + worker.ServiceProvider = ServiceProvider; + worker.LazyServiceProvider = ServiceProvider.GetRequiredService(); + + return worker; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs new file mode 100644 index 0000000000..498a37e28b --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +public static class DynamicBackgroundWorkerManagerExtensions +{ + /// + /// Adds a dynamic worker with the default schedule (). + /// + public static Task AddAsync( + this IDynamicBackgroundWorkerManager manager, + string workerName, + Func handler, + CancellationToken cancellationToken = default) + { + return manager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = DynamicBackgroundWorkerSchedule.DefaultPeriod + }, + handler, + cancellationToken); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs index 6698e2f170..6505492e44 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs @@ -18,5 +18,11 @@ public class DynamicBackgroundWorkerSchedule $"Period must be greater than 0 when provided. Given value: {Period.Value}.", nameof(Period)); } + + if (Period == null && string.IsNullOrWhiteSpace(CronExpression)) + { + throw new ArgumentException( + "At least one of 'Period' or 'CronExpression' must be set."); + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs index eb22729d57..499524f7ca 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.Threading; @@ -18,33 +17,4 @@ public interface IBackgroundWorkerManager : IRunnable /// /// Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default); - - /// - /// Adds a dynamic worker by name and handler. - /// - Task AddAsync( - string workerName, - Func handler, - CancellationToken cancellationToken = default); - - /// - /// Adds a dynamic worker by name, schedule and handler. - /// - Task AddAsync( - string workerName, - DynamicBackgroundWorkerSchedule schedule, - Func handler, - CancellationToken cancellationToken = default); - - /// - /// Removes a previously added dynamic worker by name. - /// Returns true if the worker was found and removed; false otherwise. - /// - Task RemoveAsync(string workerName, CancellationToken cancellationToken = default); - - /// - /// Updates the schedule of a previously added dynamic worker. - /// Returns true if the worker was found and updated; false otherwise. - /// - Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default); } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..317aa36433 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +/// +/// Manages dynamic background workers that are registered at runtime +/// without requiring a strongly-typed worker class. +/// +public interface IDynamicBackgroundWorkerManager +{ + /// + /// Adds a dynamic worker by name, schedule and handler. + /// If a worker with the same name already exists, it will be replaced. + /// + Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + Func handler, + CancellationToken cancellationToken = default); + + /// + /// Removes a previously added dynamic worker by name. + /// Returns true if the worker was found and removed; false otherwise. + /// + Task RemoveAsync(string workerName, CancellationToken cancellationToken = default); + + /// + /// Updates the schedule of a previously added dynamic worker. + /// Returns true if the worker was found and updated; false otherwise. + /// + Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default); + + /// + /// Checks whether a dynamic worker with the given name is registered. + /// + bool IsRegistered(string workerName); +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs index 7d5cb4dd7f..5001e96ff4 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,34 +9,45 @@ namespace Volo.Abp.BackgroundWorkers; public class InMemoryDynamicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase { - protected string WorkerName { get; } - protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public string WorkerName { get; } + + private readonly Func _handler; public InMemoryDynamicBackgroundWorker( string workerName, DynamicBackgroundWorkerSchedule schedule, + Func handler, AbpAsyncTimer timer, - IServiceScopeFactory serviceScopeFactory, - IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory) { WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(schedule, nameof(schedule)); - HandlerRegistry = Check.NotNull(handlerRegistry, nameof(handlerRegistry)); + _handler = Check.NotNull(handler, nameof(handler)); Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; CronExpression = schedule.CronExpression; } + public virtual void UpdateSchedule(DynamicBackgroundWorkerSchedule schedule) + { + Check.NotNull(schedule, nameof(schedule)); + + Timer.Stop(); + Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + CronExpression = schedule.CronExpression; + Timer.Start(StartCancellationToken); + } + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { - var handler = HandlerRegistry.Get(WorkerName); - if (handler == null) - { - Logger.LogWarning("No dynamic background worker handler registered for: {WorkerName}", WorkerName); - return; - } - - await handler(new DynamicBackgroundWorkerExecutionContext(WorkerName, workerContext.ServiceProvider), workerContext.CancellationToken); + await _handler( + new DynamicBackgroundWorkerExecutionContext(WorkerName, workerContext.ServiceProvider), + workerContext.CancellationToken); + } + + public override string ToString() + { + return $"DynamicWorker:{WorkerName}"; } } diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs index acece7fd4b..2c3f8414e6 100644 --- a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs @@ -9,21 +9,19 @@ namespace Volo.Abp.BackgroundJobs; public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase { - private readonly IBackgroundWorkerManager _backgroundWorkerManager; - private readonly IDynamicBackgroundWorkerHandlerRegistry _handlerRegistry; + private readonly IDynamicBackgroundWorkerManager _dynamicWorkerManager; public DynamicBackgroundWorkerManager_Tests() { - _backgroundWorkerManager = GetRequiredService(); - _handlerRegistry = GetRequiredService(); + _dynamicWorkerManager = GetRequiredService(); } [Fact] - public async Task Should_Register_Dynamic_Handler_When_Added() + public async Task Should_Register_Dynamic_Worker() { var workerName = "dynamic-worker-" + Guid.NewGuid(); - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { @@ -32,7 +30,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase (_, _) => Task.CompletedTask ); - _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); } [Fact] @@ -41,7 +39,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase var workerName = "dynamic-worker-" + Guid.NewGuid(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { @@ -63,12 +61,25 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase (await tcs.Task).ShouldBeTrue(); } + [Fact] + public async Task Should_Add_Dynamic_Worker_With_Default_Schedule() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + } + [Fact] public async Task Should_Remove_Dynamic_Worker() { var workerName = "dynamic-worker-" + Guid.NewGuid(); - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { @@ -77,17 +88,17 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase (_, _) => Task.CompletedTask ); - _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); - var result = await _backgroundWorkerManager.RemoveAsync(workerName); + var result = await _dynamicWorkerManager.RemoveAsync(workerName); result.ShouldBeTrue(); - _handlerRegistry.IsRegistered(workerName).ShouldBeFalse(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); } [Fact] public async Task Should_Return_False_When_Removing_NonExistent_Worker() { - var result = await _backgroundWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid()); + var result = await _dynamicWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid()); result.ShouldBeFalse(); } @@ -97,7 +108,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase var workerName = "dynamic-worker-" + Guid.NewGuid(); var executionCount = 0; - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { @@ -110,7 +121,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase } ); - var result = await _backgroundWorkerManager.UpdateScheduleAsync( + var result = await _dynamicWorkerManager.UpdateScheduleAsync( workerName, new DynamicBackgroundWorkerSchedule { @@ -119,7 +130,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase ); result.ShouldBeTrue(); - _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); var timeout = TimeSpan.FromSeconds(5); var startTime = DateTime.UtcNow; @@ -134,7 +145,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase [Fact] public async Task Should_Return_False_When_Updating_NonExistent_Worker() { - var result = await _backgroundWorkerManager.UpdateScheduleAsync( + var result = await _dynamicWorkerManager.UpdateScheduleAsync( "non-existent-worker-" + Guid.NewGuid(), new DynamicBackgroundWorkerSchedule { Period = 1000 } ); @@ -146,20 +157,15 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase public async Task Should_Replace_Existing_Worker_When_Same_Name_Added() { var workerName = "dynamic-worker-" + Guid.NewGuid(); - var firstHandlerCalled = false; var secondHandlerTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { Period = 60000 }, - (_, _) => - { - firstHandlerCalled = true; - return Task.CompletedTask; - } + (_, _) => Task.CompletedTask ); - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { Period = 50 }, (_, _) => @@ -173,11 +179,11 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase completedTask.ShouldBe(secondHandlerTcs.Task); (await secondHandlerTcs.Task).ShouldBeTrue(); - _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); - var removed = await _backgroundWorkerManager.RemoveAsync(workerName); + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); removed.ShouldBeTrue(); - _handlerRegistry.IsRegistered(workerName).ShouldBeFalse(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); } [Fact] @@ -187,7 +193,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase await Assert.ThrowsAsync(async () => { - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { Period = 0 }, (_, _) => Task.CompletedTask @@ -202,11 +208,73 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase await Assert.ThrowsAsync(async () => { - await _backgroundWorkerManager.AddAsync( + await _dynamicWorkerManager.AddAsync( workerName, new DynamicBackgroundWorkerSchedule { Period = -1000 }, (_, _) => Task.CompletedTask ); }); } + + [Fact] + public async Task Should_Throw_When_No_Period_And_No_CronExpression() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule(), + (_, _) => Task.CompletedTask + ); + }); + } + + [Fact] + public async Task Should_Continue_Running_After_Handler_Throws_Exception() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var callCount = 0; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 50 }, + (_, _) => + { + var count = Interlocked.Increment(ref callCount); + if (count == 1) + { + throw new InvalidOperationException("Simulated failure"); + } + + tcs.TrySetResult(true); + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(tcs.Task); + callCount.ShouldBeGreaterThan(1); + } + + [Fact] + public async Task Should_Not_Be_Registered_After_Remove() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 1000 }, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + await _dynamicWorkerManager.RemoveAsync(workerName); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } }