diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 6204857d8c..4586f2660b 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -120,6 +120,33 @@ 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) + +You can add a runtime worker without pre-defining a dedicated worker class by passing a handler directly to `AddAsync`. + +```csharp +await backgroundWorkerManager.AddAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 30000 // 30 seconds + // CronExpression = "*/30 * * * * *" // optional (provider dependent) + }, + async (context, cancellationToken) => + { + var inventorySyncAppService = context.ServiceProvider.GetRequiredService(); + await inventorySyncAppService.SyncAsync(cancellationToken); + } +); +``` + +Key points: + +* `workerName` is the runtime identifier of the dynamic worker. +* 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. + ## Options `AbpBackgroundWorkerOptions` class is used to [set options](../../fundamentals/options.md) for the background workers. Currently, there is only one option: 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 c17ccdc443..fa54ab5538 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 6b6d822c50..e8a840035f 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 ae7e098070..4ef2d2c19d 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 8790243334..d2ac1ed9cc 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 1e2280b78a..e3435efc16 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 3d93fc68a1..76e3aee2d5 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 a015e32d66..159f11a6d3 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 64a4a1be64..e49a99005e 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,6 +9,7 @@ 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; @@ -20,11 +21,12 @@ namespace Volo.Abp.BackgroundWorkers.Hangfire; public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISingletonDependency { protected AbpHangfireBackgroundJobServer BackgroundJobServer { get; set; } = default!; - protected IServiceProvider ServiceProvider { get; } - public HangfireBackgroundWorkerManager(IServiceProvider serviceProvider) + public HangfireBackgroundWorkerManager( + IServiceProvider serviceProvider, + IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) + : base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry) { - ServiceProvider = serviceProvider; } public void Initialize() @@ -137,6 +139,74 @@ 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)); + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + + 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 + }); + } + + return Task.CompletedTask; + } + 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 new file mode 100644 index 0000000000..6ae84da915 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +public class HangfireDynamicBackgroundWorkerAdapter : ITransientDependency +{ + protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } + protected IServiceProvider ServiceProvider { get; } + + public HangfireDynamicBackgroundWorkerAdapter( + IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry, + IServiceProvider serviceProvider) + { + DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; + ServiceProvider = serviceProvider; + } + + public virtual async Task DoWorkAsync(string workerName, CancellationToken cancellationToken = default) + { + var handler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName); + if (handler == null) + { + return; + } + + await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken); + } +} 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 9854379c79..c383bdd8f4 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,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Quartz; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -10,9 +11,15 @@ 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) + public QuartzBackgroundWorkerManager( + IScheduler scheduler, + IServiceProvider serviceProvider, + IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) + : base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry) { Scheduler = scheduler; } @@ -96,4 +103,72 @@ 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)); + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + + 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); + } + } } 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 new file mode 100644 index 0000000000..f0c9e09ad8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using Quartz; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.Quartz; + +public class QuartzDynamicBackgroundWorkerAdapter : IJob, ITransientDependency +{ + protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } + protected IServiceProvider ServiceProvider { get; } + + public QuartzDynamicBackgroundWorkerAdapter( + IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry, + IServiceProvider serviceProvider) + { + DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; + ServiceProvider = serviceProvider; + } + + public virtual async Task Execute(IJobExecutionContext context) + { + var workerName = context.MergedJobDataMap.GetString(QuartzBackgroundWorkerManager.DynamicWorkerNameKey); + if (string.IsNullOrWhiteSpace(workerName)) + { + return; + } + + var nonNullWorkerName = workerName!; + var handler = DynamicBackgroundWorkerHandlerRegistry.Get(nonNullWorkerName); + if (handler == null) + { + return; + } + + await handler( + new DynamicBackgroundWorkerExecutionContext(nonNullWorkerName, ServiceProvider), + context.CancellationToken + ); + } +} 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 922cad294d..8eb839b9e3 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,8 +1,11 @@ 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; @@ -14,15 +17,21 @@ 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; } @@ -70,6 +79,66 @@ 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)); + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + + 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 + }); + } + protected virtual string GetCron(int period) { var time = TimeSpan.FromMilliseconds(period); 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 8d05ce1239..ea3e18492b 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs @@ -1,8 +1,10 @@ -using System; +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; @@ -16,13 +18,19 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen private bool _isDisposed; private readonly List _backgroundWorkers; + protected IServiceProvider ServiceProvider { get; } + protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } /// /// Initializes a new instance of the class. /// - public BackgroundWorkerManager() + public BackgroundWorkerManager( + IServiceProvider serviceProvider, + IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) { _backgroundWorkers = new List(); + ServiceProvider = serviceProvider; + DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; } public virtual async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) @@ -35,6 +43,54 @@ 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)); + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + + if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) + { + throw new AbpException("Default background worker manager does not support cron expression without period."); + } + + var timer = ServiceProvider.GetRequiredService(); + var serviceScopeFactory = ServiceProvider.GetRequiredService(); + var worker = new InMemoryDynamicBackgroundWorker( + workerName, + schedule, + timer, + serviceScopeFactory, + DynamicBackgroundWorkerHandlerRegistry + ); + worker.ServiceProvider = ServiceProvider; + worker.LazyServiceProvider = ServiceProvider.GetRequiredService(); + + await AddAsync(worker, cancellationToken); + } + public virtual void Dispose() { if (_isDisposed) diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs new file mode 100644 index 0000000000..edb810d105 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs @@ -0,0 +1,16 @@ +using System; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerExecutionContext +{ + public string WorkerName { get; } + + public IServiceProvider ServiceProvider { get; } + + public DynamicBackgroundWorkerExecutionContext(string workerName, IServiceProvider serviceProvider) + { + WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + ServiceProvider = Check.NotNull(serviceProvider, nameof(serviceProvider)); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs new file mode 100644 index 0000000000..0f9ea8c022 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerHandlerRegistry : IDynamicBackgroundWorkerHandlerRegistry, ISingletonDependency +{ + protected ConcurrentDictionary> Handlers { get; } + + public DynamicBackgroundWorkerHandlerRegistry() + { + Handlers = new ConcurrentDictionary>(); + } + + public virtual void Register(string workerName, Func handler) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(handler, nameof(handler)); + + Handlers[workerName] = handler; + } + + public virtual bool Unregister(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.TryRemove(workerName, out _); + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.ContainsKey(workerName); + } + + public virtual Func? Get(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.TryGetValue(workerName, out var handler) ? handler : null; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs new file mode 100644 index 0000000000..3652088c25 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerSchedule +{ + public const int DefaultPeriod = 60000; + + public int? Period { get; set; } + + public string? CronExpression { get; 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 499524f7ca..dbff4ff067 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.Threading; @@ -17,4 +18,21 @@ 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); } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs new file mode 100644 index 0000000000..eb0db0e6d0 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +public interface IDynamicBackgroundWorkerHandlerRegistry +{ + void Register(string workerName, Func handler); + + bool Unregister(string workerName); + + bool IsRegistered(string workerName); + + Func? Get(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 new file mode 100644 index 0000000000..7d5cb4dd7f --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundWorkers; + +public class InMemoryDynamicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase +{ + protected string WorkerName { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + + public InMemoryDynamicBackgroundWorker( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + AbpAsyncTimer timer, + IServiceScopeFactory serviceScopeFactory, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + : base(timer, serviceScopeFactory) + { + WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + HandlerRegistry = Check.NotNull(handlerRegistry, nameof(handlerRegistry)); + + Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + CronExpression = schedule.CronExpression; + } + + 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); + } +} 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 new file mode 100644 index 0000000000..51bae67fbe --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.BackgroundWorkers; +using Xunit; + +namespace Volo.Abp.BackgroundJobs; + +public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase +{ + private readonly IBackgroundWorkerManager _backgroundWorkerManager; + private readonly IDynamicBackgroundWorkerHandlerRegistry _handlerRegistry; + + public DynamicBackgroundWorkerManager_Tests() + { + _backgroundWorkerManager = GetRequiredService(); + _handlerRegistry = GetRequiredService(); + } + + [Fact] + public async Task Should_Register_Dynamic_Handler_When_Added() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 1000 + }, + (_, _) => Task.CompletedTask + ); + + _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + } + + [Fact] + public async Task Should_Execute_Dynamic_Handler() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 50 + }, + (context, _) => + { + if (context.WorkerName == workerName) + { + tcs.TrySetResult(true); + } + + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(tcs.Task); + (await tcs.Task).ShouldBeTrue(); + } +}