From dcf08ec627d867a0ebf94440b06d58c59533b72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 22:51:23 +0300 Subject: [PATCH 01/12] Add dynamic background worker support Introduce runtime dynamic background workers: add DynamicBackgroundWorkerExecutionContext, DynamicBackgroundWorkerSchedule, IDynamicBackgroundWorkerHandlerRegistry and its implementation. Extend IBackgroundWorkerManager with AddAsync overloads to register handlers by name and schedule. Provide InMemoryDynamicBackgroundWorker for in-process execution and provider-specific adapters/implementations for Hangfire, Quartz and TickerQ (including Hangfire/Quartz/TickerQ adapters and manager changes) to schedule and execute dynamic handlers. Update BackgroundWorkerManager to hold IServiceProvider and the handler registry and wire DI through constructors. Add a docs example and unit tests to verify handler registration and execution. --- .../background-workers/index.md | 27 +++++++ .../BackgroundJobConfiguration.cs | 2 +- .../BackgroundJobs/BackgroundJobExecuter.cs | 2 +- .../Abp/BackgroundJobs/JobExecutionContext.cs | 2 +- .../Hangfire/HangfireJobExecutionAdapter.cs | 2 +- .../Abp/BackgroundJobs/RabbitMQ/JobQueue.cs | 2 +- .../TickerQ/AbpBackgroundJobsTickerQModule.cs | 2 +- .../Abp/BackgroundJobs/BackgroundJobWorker.cs | 2 +- .../HangfireBackgroundWorkerManager.cs | 76 +++++++++++++++++- .../HangfireDynamicBackgroundWorkerAdapter.cs | 31 ++++++++ .../Quartz/QuartzBackgroundWorkerManager.cs | 77 ++++++++++++++++++- .../QuartzDynamicBackgroundWorkerAdapter.cs | 41 ++++++++++ .../AbpTickerQBackgroundWorkerManager.cs | 69 +++++++++++++++++ .../BackgroundWorkerManager.cs | 60 ++++++++++++++- ...DynamicBackgroundWorkerExecutionContext.cs | 16 ++++ .../DynamicBackgroundWorkerHandlerRegistry.cs | 43 +++++++++++ .../DynamicBackgroundWorkerSchedule.cs | 10 +++ .../IBackgroundWorkerManager.cs | 20 ++++- ...IDynamicBackgroundWorkerHandlerRegistry.cs | 16 ++++ .../InMemoryDynamicBackgroundWorker.cs | 40 ++++++++++ .../DynamicBackgroundWorkerManager_Tests.cs | 64 +++++++++++++++ 21 files changed, 590 insertions(+), 14 deletions(-) create mode 100644 framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs create mode 100644 framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs 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(); + } +} From 15202a910cfae0e9c679a7cc1df8ada9fa046606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 23:31:05 +0300 Subject: [PATCH 02/12] Add dynamic worker remove/update APIs Introduce RemoveAsync and UpdateScheduleAsync to IBackgroundWorkerManager and implement them for the default in-memory manager, Hangfire, Quartz and TickerQ providers. The default BackgroundWorkerManager now tracks dynamic workers in a dictionary, supports stopping/removing workers and recreating workers with a new schedule; provider-specific managers update scheduler entries or remove recurring jobs accordingly. Documentation updated with usage examples for removing and updating schedules, and unit tests added to cover removal, schedule updates and non-existent worker behavior. --- .../background-workers/index.md | 18 +++++ .../HangfireBackgroundWorkerManager.cs | 64 ++++++++++++++++ .../Quartz/QuartzBackgroundWorkerManager.cs | 51 +++++++++++++ .../AbpTickerQBackgroundWorkerManager.cs | 44 +++++++++++ .../BackgroundWorkerManager.cs | 65 ++++++++++++++++ .../IBackgroundWorkerManager.cs | 12 +++ .../DynamicBackgroundWorkerManager_Tests.cs | 74 +++++++++++++++++++ 7 files changed, 328 insertions(+) diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 4586f2660b..b56c248ffe 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -140,12 +140,30 @@ await backgroundWorkerManager.AddAsync( ); ``` +You can also **remove** a dynamic worker or **update its schedule** at runtime: + +```csharp +// Remove a dynamic worker +var removed = await backgroundWorkerManager.RemoveAsync("InventorySyncWorker"); + +// Update the schedule of a dynamic worker +var updated = await backgroundWorkerManager.UpdateScheduleAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 60000 // change to 60 seconds + } +); +``` + 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. +* `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. ## Options 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 e49a99005e..6cd778c1fd 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 @@ -207,6 +207,70 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet 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)); + + 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.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs index c383bdd8f4..e0d30b7d3b 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 @@ -171,4 +171,55 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken); } } + + 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}"); + await Scheduler.DeleteJob(jobKey, cancellationToken); + DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); + + return true; + } + + public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + 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()); + } + + await Scheduler.RescheduleJob(triggerKey, triggerBuilder.Build(), cancellationToken); + + return true; + } } 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 8eb839b9e3..6bf9e305e8 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 @@ -139,6 +139,50 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl }); } + public override async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) + { + return false; + } + + var functionName = $"DynamicWorker:{workerName}"; + AbpTickerQFunctionProvider.Functions.Remove(functionName); + AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); + DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); + + return true; + } + + public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + 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/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs index ea3e18492b..a5568cc5d0 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs @@ -18,6 +18,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen private bool _isDisposed; private readonly List _backgroundWorkers; + private readonly Dictionary _dynamicWorkers; protected IServiceProvider ServiceProvider { get; } protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } @@ -29,6 +30,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) { _backgroundWorkers = new List(); + _dynamicWorkers = new Dictionary(); ServiceProvider = serviceProvider; DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; } @@ -88,9 +90,72 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen 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.Remove(workerName); + 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)); + + 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.Remove(workerName); + + 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/IBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs index dbff4ff067..eb22729d57 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs @@ -35,4 +35,16 @@ public interface IBackgroundWorkerManager : IRunnable 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/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 51bae67fbe..bd2685da04 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 @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Shouldly; using Volo.Abp.BackgroundWorkers; @@ -61,4 +62,77 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase completedTask.ShouldBe(tcs.Task); (await tcs.Task).ShouldBeTrue(); } + + [Fact] + public async Task Should_Remove_Dynamic_Worker() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 1000 + }, + (_, _) => Task.CompletedTask + ); + + _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + + var result = await _backgroundWorkerManager.RemoveAsync(workerName); + result.ShouldBeTrue(); + _handlerRegistry.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Return_False_When_Removing_NonExistent_Worker() + { + var result = await _backgroundWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid()); + result.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Update_Dynamic_Worker_Schedule() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var executionCount = 0; + + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 60000 + }, + (_, _) => + { + Interlocked.Increment(ref executionCount); + return Task.CompletedTask; + } + ); + + var result = await _backgroundWorkerManager.UpdateScheduleAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 50 + } + ); + + result.ShouldBeTrue(); + _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + + await Task.Delay(500); + executionCount.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Return_False_When_Updating_NonExistent_Worker() + { + var result = await _backgroundWorkerManager.UpdateScheduleAsync( + "non-existent-worker-" + Guid.NewGuid(), + new DynamicBackgroundWorkerSchedule { Period = 1000 } + ); + + result.ShouldBeFalse(); + } } From b9a265a831757bc0a3cef96c479c56ada12c07e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 11 Mar 2026 10:44:58 +0300 Subject: [PATCH 03/12] Defer dynamic handler registration and fix outcomes Move DynamicBackgroundWorkerHandlerRegistry.Register calls to after scheduling to avoid registering handlers when scheduling fails. Adjust Quartz behaviour to return actual outcomes: only unregister when DeleteJob reports deletion and return whether RescheduleJob succeeded. Convert TickerQ RemoveAsync to a non-async Task (using Task.FromResult) and move its registration similarly. Update test to poll with a timeout (instead of a fixed 500ms delay) to wait for background worker execution. --- .../Hangfire/HangfireBackgroundWorkerManager.cs | 4 ++-- .../Quartz/QuartzBackgroundWorkerManager.cs | 17 ++++++++++------- .../AbpTickerQBackgroundWorkerManager.cs | 10 +++++----- .../BackgroundWorkerManager.cs | 4 ++-- .../DynamicBackgroundWorkerManager_Tests.cs | 8 +++++++- 5 files changed, 26 insertions(+), 17 deletions(-) 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 6cd778c1fd..5d82c14ba9 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 @@ -165,8 +165,6 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet Check.NotNull(schedule, nameof(schedule)); Check.NotNull(handler, nameof(handler)); - DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); - var cronExpression = schedule.CronExpression; if (cronExpression.IsNullOrWhiteSpace()) { @@ -204,6 +202,8 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet }); } + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + return Task.CompletedTask; } 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 e0d30b7d3b..a696f6e8ac 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 @@ -130,8 +130,6 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton 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."); @@ -170,6 +168,8 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton { await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken); } + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); } public override async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) @@ -182,10 +182,13 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton } var jobKey = new JobKey($"DynamicWorker:{workerName}"); - await Scheduler.DeleteJob(jobKey, cancellationToken); - DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); + var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken); + if (deleted) + { + DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); + } - return true; + return deleted; } public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) @@ -218,8 +221,8 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); } - await Scheduler.RescheduleJob(triggerKey, triggerBuilder.Build(), cancellationToken); + var result = await Scheduler.RescheduleJob(triggerKey, triggerBuilder.Build(), cancellationToken); - return true; + return result != null; } } 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 6bf9e305e8..fbfc9eb47d 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 @@ -105,8 +105,6 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl 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}"; @@ -137,15 +135,17 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl Function = functionName, Expression = cronExpression }); + + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); } - public override async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + public override Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) { - return false; + return Task.FromResult(false); } var functionName = $"DynamicWorker:{workerName}"; @@ -153,7 +153,7 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); - return true; + return Task.FromResult(true); } public override async Task UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) 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 a5568cc5d0..a1f9f78fcd 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs @@ -71,13 +71,13 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen 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."); } + DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler); + var timer = ServiceProvider.GetRequiredService(); var serviceScopeFactory = ServiceProvider.GetRequiredService(); var worker = new InMemoryDynamicBackgroundWorker( 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 bd2685da04..3c2977e53c 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 @@ -121,7 +121,13 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase result.ShouldBeTrue(); _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); - await Task.Delay(500); + var timeout = TimeSpan.FromSeconds(5); + var startTime = DateTime.UtcNow; + while (executionCount == 0 && DateTime.UtcNow - startTime < timeout) + { + await Task.Delay(50); + } + executionCount.ShouldBeGreaterThan(0); } From ebb5fae17aaf5514eb8c5acc0ebd983fe22f41bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 11 Mar 2026 11:28:46 +0300 Subject: [PATCH 04/12] Validate schedules and replace dynamic workers Add schedule validation and make dynamic workers replaceable. Introduced DynamicBackgroundWorkerSchedule.Validate (checks Period > 0) and call Validate() in all provider managers (in-memory, Hangfire, Quartz, TickerQ). Switched in-memory dynamic worker storage to ConcurrentDictionary and ensure adding a worker with an existing name stops/removes the previous instance before registering the new one; removals use TryRemove. Updated docs to clarify that adding a worker replaces an existing one and that CronExpression is only supported by scheduler-backed providers. Added tests for replacement behavior and invalid period values. --- .../background-workers/index.md | 4 +- .../HangfireBackgroundWorkerManager.cs | 4 ++ .../Quartz/QuartzBackgroundWorkerManager.cs | 4 ++ .../AbpTickerQBackgroundWorkerManager.cs | 4 ++ .../BackgroundWorkerManager.cs | 19 ++++-- .../DynamicBackgroundWorkerSchedule.cs | 12 ++++ .../DynamicBackgroundWorkerManager_Tests.cs | 68 +++++++++++++++++++ 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index b56c248ffe..8200be023f 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -158,10 +158,10 @@ var updated = await backgroundWorkerManager.UpdateScheduleAsync( Key points: -* `workerName` is the runtime identifier of the dynamic worker. +* `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. +* 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`. * `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.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index 5d82c14ba9..12fd49db04 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 @@ -165,6 +165,8 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet Check.NotNull(schedule, nameof(schedule)); Check.NotNull(handler, nameof(handler)); + schedule.Validate(); + var cronExpression = schedule.CronExpression; if (cronExpression.IsNullOrWhiteSpace()) { @@ -228,6 +230,8 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(schedule, nameof(schedule)); + schedule.Validate(); + if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) { return Task.FromResult(false); 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 a696f6e8ac..d963106e02 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 @@ -130,6 +130,8 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton 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."); @@ -196,6 +198,8 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(schedule, nameof(schedule)); + schedule.Validate(); + if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) { return false; 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 fbfc9eb47d..b2f0de80cf 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 @@ -105,6 +105,8 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl 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}"; @@ -161,6 +163,8 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(schedule, nameof(schedule)); + schedule.Validate(); + if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName)) { return false; 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 a1f9f78fcd..84640ff1bf 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -18,7 +19,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen private bool _isDisposed; private readonly List _backgroundWorkers; - private readonly Dictionary _dynamicWorkers; + private readonly ConcurrentDictionary _dynamicWorkers; protected IServiceProvider ServiceProvider { get; } protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; } @@ -30,7 +31,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry) { _backgroundWorkers = new List(); - _dynamicWorkers = new Dictionary(); + _dynamicWorkers = new ConcurrentDictionary(); ServiceProvider = serviceProvider; DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry; } @@ -71,11 +72,19 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen 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(); @@ -106,7 +115,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen await worker.StopAsync(cancellationToken); _backgroundWorkers.Remove(worker); - _dynamicWorkers.Remove(workerName); + _dynamicWorkers.TryRemove(workerName, out _); DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName); return true; @@ -117,6 +126,8 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(schedule, nameof(schedule)); + schedule.Validate(); + if (!_dynamicWorkers.TryGetValue(workerName, out var oldWorker)) { return false; @@ -135,7 +146,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen await oldWorker.StopAsync(cancellationToken); _backgroundWorkers.Remove(oldWorker); - _dynamicWorkers.Remove(workerName); + _dynamicWorkers.TryRemove(workerName, out _); var timer = ServiceProvider.GetRequiredService(); var serviceScopeFactory = ServiceProvider.GetRequiredService(); 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 3652088c25..6698e2f170 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs @@ -1,3 +1,5 @@ +using System; + namespace Volo.Abp.BackgroundWorkers; public class DynamicBackgroundWorkerSchedule @@ -7,4 +9,14 @@ public class DynamicBackgroundWorkerSchedule public int? Period { get; set; } public string? CronExpression { get; set; } + + public virtual void Validate() + { + if (Period.HasValue && Period.Value <= 0) + { + throw new ArgumentException( + $"Period must be greater than 0 when provided. Given value: {Period.Value}.", + nameof(Period)); + } + } } 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 3c2977e53c..acece7fd4b 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 @@ -141,4 +141,72 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase result.ShouldBeFalse(); } + + [Fact] + 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( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => + { + firstHandlerCalled = true; + return Task.CompletedTask; + } + ); + + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 50 }, + (_, _) => + { + secondHandlerTcs.TrySetResult(true); + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(secondHandlerTcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(secondHandlerTcs.Task); + (await secondHandlerTcs.Task).ShouldBeTrue(); + + _handlerRegistry.IsRegistered(workerName).ShouldBeTrue(); + + var removed = await _backgroundWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _handlerRegistry.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Throw_When_Period_Is_Zero() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 0 }, + (_, _) => Task.CompletedTask + ); + }); + } + + [Fact] + public async Task Should_Throw_When_Period_Is_Negative() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _backgroundWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = -1000 }, + (_, _) => Task.CompletedTask + ); + }); + } } From 5babd75f910dcbbcf94bb13b91eeea6cb825921d Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 14:58:57 +0800 Subject: [PATCH 05/12] feat: Implement dynamic background worker management with Quartz and TickerQ --- .../background-workers/index.md | 19 +- .../BackgroundJobConfiguration.cs | 2 +- .../BackgroundJobs/BackgroundJobExecuter.cs | 2 +- .../Abp/BackgroundJobs/JobExecutionContext.cs | 2 +- .../Hangfire/HangfireJobExecutionAdapter.cs | 2 +- .../Abp/BackgroundJobs/RabbitMQ/JobQueue.cs | 2 +- .../TickerQ/AbpBackgroundJobsTickerQModule.cs | 2 +- .../Abp/BackgroundJobs/BackgroundJobWorker.cs | 2 +- .../HangfireBackgroundWorkerManager.cs | 144 +-------------- .../HangfireDynamicBackgroundWorkerAdapter.cs | 27 ++- .../HangfireDynamicBackgroundWorkerManager.cs | 168 ++++++++++++++++++ .../Quartz/QuartzBackgroundWorkerManager.cs | 135 +------------- .../QuartzDynamicBackgroundWorkerAdapter.cs | 35 ++-- .../QuartzDynamicBackgroundWorkerManager.cs | 143 +++++++++++++++ .../AbpTickerQBackgroundWorkerManager.cs | 117 ------------ .../TickerQDynamicBackgroundWorkerManager.cs | 164 +++++++++++++++++ .../BackgroundWorkerManager.cs | 136 +------------- .../DefaultDynamicBackgroundWorkerManager.cs | 144 +++++++++++++++ ...ynamicBackgroundWorkerManagerExtensions.cs | 27 +++ .../DynamicBackgroundWorkerSchedule.cs | 6 + .../IBackgroundWorkerManager.cs | 32 +--- .../IDynamicBackgroundWorkerManager.cs | 42 +++++ .../InMemoryDynamicBackgroundWorker.cs | 39 ++-- .../DynamicBackgroundWorkerManager_Tests.cs | 128 +++++++++---- 24 files changed, 889 insertions(+), 631 deletions(-) create mode 100644 framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs 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(); + } } From 7e12823cd59d3fb5ac51e6e4178c48ce951451b1 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 15:13:17 +0800 Subject: [PATCH 06/12] fix: Simplify cron expression generation and improve error handling in background worker managers --- .../HangfireDynamicBackgroundWorkerManager.cs | 28 ++++++++++--------- .../TickerQDynamicBackgroundWorkerManager.cs | 7 +++++ .../DefaultDynamicBackgroundWorkerManager.cs | 4 +-- 3 files changed, 24 insertions(+), 15 deletions(-) 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 index 53820d0522..040be6b3e5 100644 --- 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 @@ -140,29 +140,31 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa protected virtual string GetCron(int period) { var time = TimeSpan.FromMilliseconds(period); - string cron; if (time.TotalSeconds <= 59) { - cron = $"*/{time.TotalSeconds} * * * * *"; + var seconds = (int)Math.Round(time.TotalSeconds); + return $"*/{seconds} * * * * *"; } - else if (time.TotalMinutes <= 59) - { - cron = $"*/{time.TotalMinutes} * * * *"; - } - else if (time.TotalHours <= 23) + + if (time.TotalMinutes <= 59) { - cron = $"0 */{time.TotalHours} * * *"; + var minutes = (int)Math.Round(time.TotalMinutes); + return $"*/{minutes} * * * *"; } - else if (time.TotalDays <= 31) + + if (time.TotalHours <= 23) { - cron = $"0 0 0 1/{time.TotalDays} * *"; + var hours = (int)Math.Round(time.TotalHours); + return $"0 */{hours} * * *"; } - else + + if (time.TotalDays <= 31) { - throw new AbpException($"Cannot convert period: {period} to cron expression."); + var days = (int)Math.Round(time.TotalDays); + return $"0 0 */{days} * *"; } - return cron; + throw new AbpException($"Cannot convert period: {period} to cron expression."); } } 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 index efe8f3eebf..e5409b1b9a 100644 --- 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 @@ -92,6 +92,10 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); HandlerRegistry.Unregister(workerName); + // Note: ICronTickerManager does not provide a remove API. + // The handler is unregistered above, so any persisted cron entry will + // find a null handler and skip execution silently. + return Task.FromResult(true); } @@ -138,6 +142,9 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan var time = TimeSpan.FromMilliseconds(period); if (time.TotalMinutes < 1) { + Logger.LogWarning( + "TickerQ does not support sub-minute intervals. Period {Period}ms will be rounded up to every minute.", + period); return "* * * * *"; } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs index 3d239215dc..030b6299d0 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -37,7 +37,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan schedule.Validate(); - if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) + if (schedule.Period == null) { throw new AbpException( $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + @@ -79,7 +79,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan schedule.Validate(); - if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression)) + if (schedule.Period == null) { throw new AbpException( $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + From 43e7365e226bbd946f4d54500564b3d58e7fcfd2 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 15:24:55 +0800 Subject: [PATCH 07/12] fix: Streamline job deletion and enhance exception handling in TickerQ background worker manager --- .../QuartzDynamicBackgroundWorkerManager.cs | 9 +++---- .../TickerQDynamicBackgroundWorkerManager.cs | 27 ++++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) 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 index 57fdda79eb..7ff3f85a1d 100644 --- 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 @@ -71,13 +71,10 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana } var jobKey = new JobKey($"DynamicWorker:{workerName}"); - var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken); - if (deleted) - { - HandlerRegistry.Unregister(workerName); - } + await Scheduler.DeleteJob(jobKey, cancellationToken); + HandlerRegistry.Unregister(workerName); - return deleted; + return true; } public virtual async Task UpdateScheduleAsync( 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 index e5409b1b9a..f4850777dd 100644 --- 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 @@ -1,12 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; 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.ExceptionHandling; using Volo.Abp.TickerQ; namespace Volo.Abp.BackgroundWorkers.TickerQ; @@ -57,9 +59,19 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan return; } - await registeredHandler( - new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider), - tickerCancellationToken); + try + { + await registeredHandler( + new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider), + tickerCancellationToken); + } + catch (Exception ex) + { + await serviceProvider.GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + throw; + } }, 0); AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker @@ -92,9 +104,12 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); HandlerRegistry.Unregister(workerName); - // Note: ICronTickerManager does not provide a remove API. - // The handler is unregistered above, so any persisted cron entry will - // find a null handler and skip execution silently. + // ICronTickerManager does not provide a remove API, so the persisted + // cron entry will remain in storage. The handler is unregistered above, + // so the entry will find a null handler and skip execution silently. + Logger.LogWarning( + "Dynamic worker '{WorkerName}' removed from memory, but the persisted TickerQ cron entry may remain in storage.", + workerName); return Task.FromResult(true); } From fa19685a5bbc87133a2c1184438bcbf9622acc8f Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 15:34:21 +0800 Subject: [PATCH 08/12] fix: Enhance dynamic background worker management by improving cron entry handling --- .../TickerQDynamicBackgroundWorkerManager.cs | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) 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 index f4850777dd..41209cce68 100644 --- 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 @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -22,6 +23,8 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } public ILogger Logger { get; set; } + private readonly ConcurrentDictionary _cronTickerIds; + public TickerQDynamicBackgroundWorkerManager( AbpTickerQFunctionProvider abpTickerQFunctionProvider, AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider, @@ -33,6 +36,7 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan CronTickerManager = cronTickerManager; HandlerRegistry = handlerRegistry; Logger = NullLogger.Instance; + _cronTickerIds = new ConcurrentDictionary(); } public virtual async Task AddAsync( @@ -47,6 +51,12 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan schedule.Validate(); + // If replacing an existing worker, delete the old cron entry first + if (_cronTickerIds.TryRemove(workerName, out var existingId)) + { + await CronTickerManager.DeleteAsync(existingId, cancellationToken); + } + var cronExpression = schedule.CronExpression ?? GetCron(schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod); var functionName = $"DynamicWorker:{workerName}"; @@ -81,22 +91,27 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan WorkerType = typeof(TickerQDynamicBackgroundWorkerManager) }; - await CronTickerManager.AddAsync(new CronTickerEntity + var result = await CronTickerManager.AddAsync(new CronTickerEntity { Function = functionName, Expression = cronExpression - }); + }, cancellationToken); + + if (result.IsSucceeded && result.Result != null) + { + _cronTickerIds[workerName] = result.Result.Id; + } HandlerRegistry.Register(workerName, handler); } - public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); if (!HandlerRegistry.IsRegistered(workerName)) { - return Task.FromResult(false); + return false; } var functionName = $"DynamicWorker:{workerName}"; @@ -104,14 +119,12 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); HandlerRegistry.Unregister(workerName); - // ICronTickerManager does not provide a remove API, so the persisted - // cron entry will remain in storage. The handler is unregistered above, - // so the entry will find a null handler and skip execution silently. - Logger.LogWarning( - "Dynamic worker '{WorkerName}' removed from memory, but the persisted TickerQ cron entry may remain in storage.", - workerName); + if (_cronTickerIds.TryRemove(workerName, out var cronTickerId)) + { + await CronTickerManager.DeleteAsync(cronTickerId, cancellationToken); + } - return Task.FromResult(true); + return true; } public virtual async Task UpdateScheduleAsync( @@ -137,11 +150,22 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan existingWorker.CronExpression = cronExpression; } - await CronTickerManager.AddAsync(new CronTickerEntity + // Delete old entry and create new one with updated expression + if (_cronTickerIds.TryRemove(workerName, out var oldCronTickerId)) + { + await CronTickerManager.DeleteAsync(oldCronTickerId, cancellationToken); + } + + var result = await CronTickerManager.AddAsync(new CronTickerEntity { Function = functionName, Expression = cronExpression - }); + }, cancellationToken); + + if (result.IsSucceeded && result.Result != null) + { + _cronTickerIds[workerName] = result.Result.Id; + } return true; } From bcc13c5337dbdd2f3b63f70a97ea46f7b451b8cc Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 15:36:35 +0800 Subject: [PATCH 09/12] fix: Improve dynamic background worker management with runtime registration examples and clarifications --- .../background-workers/index.md | 65 +++++++++++-------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 0b53a09c48..46b192180e 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -122,47 +122,58 @@ While we generally add workers in `OnApplicationInitializationAsync`, there are ### Dynamic Workers (Runtime Registration) -You can add a runtime worker without pre-defining a dedicated worker class by injecting `IDynamicBackgroundWorkerManager` and passing a handler directly. +You can add a runtime worker without pre-defining a dedicated worker class. Inject `IDynamicBackgroundWorkerManager` and pass a handler directly: -```csharp -await dynamicBackgroundWorkerManager.AddAsync( - "InventorySyncWorker", - new DynamicBackgroundWorkerSchedule - { - Period = 30000 // 30 seconds - // CronExpression = "*/30 * * * *" // every 30 minutes, supported by Hangfire, Quartz, TickerQ - }, - async (context, cancellationToken) => +````csharp +public class MyModule : AbpModule +{ + public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) { - var inventorySyncAppService = context.ServiceProvider.GetRequiredService(); - await inventorySyncAppService.SyncAsync(cancellationToken); + var dynamicWorkerManager = context.ServiceProvider + .GetRequiredService(); + + await dynamicWorkerManager.AddAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 30000 //30 seconds + //CronExpression = "*/30 * * * *" //Every 30 minutes. Only for Hangfire, Quartz or TickerQ integration. + }, + async (workerContext, cancellationToken) => + { + var inventorySyncAppService = workerContext + .ServiceProvider + .GetRequiredService(); + + await inventorySyncAppService.SyncAsync(cancellationToken); + } + ); } -); -``` +} +```` You can also **remove** a dynamic worker or **update its schedule** at runtime: -```csharp -// Remove a dynamic worker -var removed = await dynamicBackgroundWorkerManager.RemoveAsync("InventorySyncWorker"); +````csharp +//Remove a dynamic worker +var removed = await dynamicWorkerManager.RemoveAsync("InventorySyncWorker"); -// Update the schedule of a dynamic worker -var updated = await dynamicBackgroundWorkerManager.UpdateScheduleAsync( +//Update the schedule of a dynamic worker +var updated = await dynamicWorkerManager.UpdateScheduleAsync( "InventorySyncWorker", new DynamicBackgroundWorkerSchedule { - Period = 60000 // change to 60 seconds + Period = 60000 //Change to 60 seconds } ); -``` - -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` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. +* `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` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. It is a good practice to **resolve dependencies** from the `workerContext.ServiceProvider` instead of constructor injection. * 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. +* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md), [TickerQ](./tickerq.md)).** 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. From ad4c47a39c70a456d6a0eb93c73aac78e0653dd8 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 16:42:12 +0800 Subject: [PATCH 10/12] fix: Update documentation and improve dynamic background worker management for TickerQ and Quartz integration --- .../background-workers/index.md | 3 +- .../HangfireDynamicBackgroundWorkerManager.cs | 4 +- .../TickerQDynamicBackgroundWorkerManager.cs | 194 ++---------------- .../DemoAppQuartzModule.cs | 4 +- ...o.Abp.BackgroundJobs.DemoApp.Quartz.csproj | 1 + .../DemoAppSharedModule.cs | 67 +++++- ....cs => 20260320082618_Initial.Designer.cs} | 2 +- ...7_Initial.cs => 20260320082618_Initial.cs} | 0 8 files changed, 88 insertions(+), 187 deletions(-) rename modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/{20260119064307_Initial.Designer.cs => 20260320082618_Initial.Designer.cs} (98%) rename modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/{20260119064307_Initial.cs => 20260320082618_Initial.cs} (100%) diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 46b192180e..ab5f42b207 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -173,7 +173,8 @@ var updated = await dynamicWorkerManager.UpdateScheduleAsync( * `workerName` is the runtime identifier of the dynamic worker. If a worker with the same name already exists, it will be **replaced**. * The `handler` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. It is a good practice to **resolve dependencies** from the `workerContext.ServiceProvider` instead of constructor injection. * At least one of `Period` or `CronExpression` must be set in `DynamicBackgroundWorkerSchedule`. -* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md), [TickerQ](./tickerq.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone. +* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone. +* **[TickerQ](./tickerq.md) does not support dynamic background workers** because it uses `FrozenDictionary` for function registration, which requires all functions to be registered before the application starts. * `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.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs index 040be6b3e5..5f310827a2 100644 --- 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 @@ -116,7 +116,7 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa RecurringJob.AddOrUpdate( recurringJobId, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), + adapter => adapter.DoWorkAsync(workerName, CancellationToken.None), cronExpression, new RecurringJobOptions { @@ -128,7 +128,7 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa RecurringJob.AddOrUpdate( recurringJobId, queueName, - adapter => adapter.DoWorkAsync(workerName, cancellationToken), + adapter => adapter.DoWorkAsync(workerName, CancellationToken.None), cronExpression, new RecurringJobOptions { 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 index 41209cce68..5cbeb12faf 100644 --- 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 @@ -1,210 +1,44 @@ 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 TickerQ.Utilities.Entities; -using TickerQ.Utilities.Enums; -using TickerQ.Utilities.Interfaces.Managers; using Volo.Abp.DependencyInjection; -using Volo.Abp.ExceptionHandling; -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; } - - private readonly ConcurrentDictionary _cronTickerIds; - - public TickerQDynamicBackgroundWorkerManager( - AbpTickerQFunctionProvider abpTickerQFunctionProvider, - AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider, - ICronTickerManager cronTickerManager, - IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) - { - AbpTickerQFunctionProvider = abpTickerQFunctionProvider; - AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider; - CronTickerManager = cronTickerManager; - HandlerRegistry = handlerRegistry; - Logger = NullLogger.Instance; - _cronTickerIds = new ConcurrentDictionary(); - } - - public virtual async Task AddAsync( + 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(); - - // If replacing an existing worker, delete the old cron entry first - if (_cronTickerIds.TryRemove(workerName, out var existingId)) - { - await CronTickerManager.DeleteAsync(existingId, cancellationToken); - } - - 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; - } - - try - { - await registeredHandler( - new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider), - tickerCancellationToken); - } - catch (Exception ex) - { - await serviceProvider.GetRequiredService() - .NotifyAsync(new ExceptionNotificationContext(ex)); - - throw; - } - }, 0); - - AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker - { - Function = functionName, - CronExpression = cronExpression, - WorkerType = typeof(TickerQDynamicBackgroundWorkerManager) - }; - - var result = await CronTickerManager.AddAsync(new CronTickerEntity - { - Function = functionName, - Expression = cronExpression - }, cancellationToken); - - if (result.IsSucceeded && result.Result != null) - { - _cronTickerIds[workerName] = result.Result.Id; - } - - HandlerRegistry.Register(workerName, handler); + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "TickerQ uses FrozenDictionary for function registration, which requires all functions to be registered before the application starts. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); } - public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) { - Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - - if (!HandlerRegistry.IsRegistered(workerName)) - { - return false; - } - - var functionName = $"DynamicWorker:{workerName}"; - AbpTickerQFunctionProvider.Functions.Remove(functionName); - AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName); - HandlerRegistry.Unregister(workerName); - - if (_cronTickerIds.TryRemove(workerName, out var cronTickerId)) - { - await CronTickerManager.DeleteAsync(cronTickerId, cancellationToken); - } - - return true; + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); } - public virtual async Task UpdateScheduleAsync( + 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 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; - } - - // Delete old entry and create new one with updated expression - if (_cronTickerIds.TryRemove(workerName, out var oldCronTickerId)) - { - await CronTickerManager.DeleteAsync(oldCronTickerId, cancellationToken); - } - - var result = await CronTickerManager.AddAsync(new CronTickerEntity - { - Function = functionName, - Expression = cronExpression - }, cancellationToken); - - if (result.IsSucceeded && result.Result != null) - { - _cronTickerIds[workerName] = result.Result.Id; - } - - return true; + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); } 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) - { - Logger.LogWarning( - "TickerQ does not support sub-minute intervals. Period {Period}ms will be rounded up to every minute.", - period); - 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."); + return false; } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs index 0d521baf3d..b8aaf6e519 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs @@ -1,6 +1,7 @@ using Volo.Abp.Autofac; using Volo.Abp.BackgroundJobs.DemoApp.Shared; using Volo.Abp.BackgroundJobs.Quartz; +using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.Modularity; namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz; @@ -8,7 +9,8 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz; [DependsOn( typeof(DemoAppSharedModule), typeof(AbpAutofacModule), - typeof(AbpBackgroundJobsQuartzModule) + typeof(AbpBackgroundJobsQuartzModule), + typeof(AbpBackgroundWorkersQuartzModule) )] public class DemoAppQuartzModule : AbpModule { diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj index 8faef0c291..5f2be2b49b 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj @@ -12,6 +12,7 @@ + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs index 713355636f..53b5d38032 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs @@ -1,5 +1,9 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs; +using Volo.Abp.BackgroundWorkers; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -8,11 +12,70 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared [DependsOn(typeof(AbpMultiTenancyModule))] public class DemoAppSharedModule : AbpModule { - public override void OnPostApplicationInitialization(ApplicationInitializationContext context) + public override async Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context) { context.ServiceProvider .GetRequiredService() .CreateJobs(); + + await DynamicBackgroundWorkerDemoAsync(context); + } + + private async Task DynamicBackgroundWorkerDemoAsync(ApplicationInitializationContext context) + { + var dynamicWorkerManager = context.ServiceProvider + .GetService(); + + if (dynamicWorkerManager == null) + { + return; + } + + // AddAsync: Register a dynamic worker with a schedule and handler + await dynamicWorkerManager.AddAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 5000 //5 seconds + }, + async (workerContext, cancellationToken) => + { + Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed."); + await Task.CompletedTask; + } + ); + + // IsRegistered: Check if a dynamic worker is registered + var isRegistered = dynamicWorkerManager.IsRegistered("DemoHeartbeatWorker"); + Console.WriteLine($"DemoHeartbeatWorker is registered: {isRegistered}"); + + // UpdateScheduleAsync: Update the schedule of an existing dynamic worker + var updated = await dynamicWorkerManager.UpdateScheduleAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 10000 //Change to 10 seconds + } + ); + Console.WriteLine($"DemoHeartbeatWorker schedule updated: {updated}"); + + // RemoveAsync: Remove a dynamic worker + var removed = await dynamicWorkerManager.RemoveAsync("DemoHeartbeatWorker"); + Console.WriteLine($"DemoHeartbeatWorker removed: {removed}"); + + // Re-add the worker to keep it running for demo purposes + await dynamicWorkerManager.AddAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 10000 //10 seconds + }, + async (workerContext, cancellationToken) => + { + Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed."); + await Task.CompletedTask; + } + ); } } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs similarity index 98% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs index 3225815926..fd21dc852f 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] - [Migration("20260119064307_Initial")] + [Migration("20260320082618_Initial")] partial class Initial { /// diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs From fa15bc4cae2acca8cc7a5af37a7e8852ba5f8a2f Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 17:09:04 +0800 Subject: [PATCH 11/12] Add SemaphoreSlim for thread safety and concurrency tests Add SemaphoreSlim to DefaultDynamicBackgroundWorkerManager to protect AddAsync/RemoveAsync/UpdateScheduleAsync from concurrent access on the same worker name. Add 3 concurrency test cases covering concurrent same-name add, concurrent add+remove, and concurrent mixed operations. --- .../DefaultDynamicBackgroundWorkerManager.cs | 63 ++++++++--- .../DynamicBackgroundWorkerManager_Tests.cs | 106 ++++++++++++++++++ 2 files changed, 151 insertions(+), 18 deletions(-) diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs index 030b6299d0..598bd74014 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -16,6 +16,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan public ILogger Logger { get; set; } private readonly ConcurrentDictionary _dynamicWorkers; + private readonly SemaphoreSlim _semaphore; private bool _isDisposed; public DefaultDynamicBackgroundWorkerManager(IServiceProvider serviceProvider) @@ -23,6 +24,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan ServiceProvider = serviceProvider; Logger = NullLogger.Instance; _dynamicWorkers = new ConcurrentDictionary(); + _semaphore = new SemaphoreSlim(1, 1); } public virtual async Task AddAsync( @@ -44,32 +46,48 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); } - if (_dynamicWorkers.TryRemove(workerName, out var existingWorker)) + await _semaphore.WaitAsync(cancellationToken); + try { - await existingWorker.StopAsync(cancellationToken); - Logger.LogInformation("Replaced existing dynamic worker: {WorkerName}", workerName); - } + 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; + var worker = CreateDynamicWorker(workerName, schedule, handler); + _dynamicWorkers[workerName] = worker; - await worker.StartAsync(cancellationToken); + await worker.StartAsync(cancellationToken); + } + finally + { + _semaphore.Release(); + } } public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); - if (!_dynamicWorkers.TryRemove(workerName, out var worker)) + await _semaphore.WaitAsync(cancellationToken); + try { - return false; - } + if (!_dynamicWorkers.TryRemove(workerName, out var worker)) + { + return false; + } - await worker.StopAsync(cancellationToken); - return true; + await worker.StopAsync(cancellationToken); + return true; + } + finally + { + _semaphore.Release(); + } } - public virtual Task UpdateScheduleAsync( + public virtual async Task UpdateScheduleAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default) @@ -86,13 +104,21 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); } - if (!_dynamicWorkers.TryGetValue(workerName, out var worker)) + await _semaphore.WaitAsync(cancellationToken); + try { - return Task.FromResult(false); - } + if (!_dynamicWorkers.TryGetValue(workerName, out var worker)) + { + return false; + } - worker.UpdateSchedule(schedule); - return Task.FromResult(true); + worker.UpdateSchedule(schedule); + return true; + } + finally + { + _semaphore.Release(); + } } public virtual bool IsRegistered(string workerName) @@ -123,6 +149,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan } _dynamicWorkers.Clear(); + _semaphore.Dispose(); } protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker( 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 2c3f8414e6..be99167d85 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 @@ -1,4 +1,7 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Shouldly; @@ -277,4 +280,107 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); } + + [Fact] + public async Task Should_Handle_Concurrent_Add_With_Same_Name() + { + var workerName = "concurrent-worker-" + Guid.NewGuid(); + var executedHandlerIds = new ConcurrentBag(); + + var tasks = Enumerable.Range(0, 10).Select(i => + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => + { + executedHandlerIds.Add(i); + return Task.CompletedTask; + } + ) + ).ToList(); + + await Task.WhenAll(tasks); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Handle_Concurrent_Add_And_Remove() + { + var workerNames = Enumerable.Range(0, 10) + .Select(i => $"concurrent-worker-{i}-" + Guid.NewGuid()) + .ToList(); + + var addTasks = workerNames.Select(name => + _dynamicWorkerManager.AddAsync( + name, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ) + ).ToList(); + + await Task.WhenAll(addTasks); + + foreach (var name in workerNames) + { + _dynamicWorkerManager.IsRegistered(name).ShouldBeTrue(); + } + + var removeTasks = workerNames.Select(name => + _dynamicWorkerManager.RemoveAsync(name) + ).ToList(); + + var results = await Task.WhenAll(removeTasks); + + results.ShouldAllBe(r => r); + + foreach (var name in workerNames) + { + _dynamicWorkerManager.IsRegistered(name).ShouldBeFalse(); + } + } + + [Fact] + public async Task Should_Handle_Concurrent_Add_Remove_Update() + { + var workerName = "concurrent-mixed-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + var tasks = new List + { + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 30000 }, + (_, _) => Task.CompletedTask + ), + _dynamicWorkerManager.UpdateScheduleAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 20000 } + ), + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 10000 }, + (_, _) => Task.CompletedTask + ) + }; + + await Task.WhenAll(tasks); + + // After all concurrent operations, worker should still be in a consistent state + var isRegistered = _dynamicWorkerManager.IsRegistered(workerName); + isRegistered.ShouldBeTrue(); + + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } } From a18cd1c5e9e81da63da1e90cc9d80d3106afed47 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 20 Mar 2026 18:18:16 +0800 Subject: [PATCH 12/12] fix: Refactor background worker handler to use delegate type for improved clarity and consistency --- .../HangfireDynamicBackgroundWorkerManager.cs | 2 +- .../Quartz/QuartzDynamicBackgroundWorkerManager.cs | 2 +- .../TickerQ/TickerQDynamicBackgroundWorkerManager.cs | 3 +-- .../DefaultDynamicBackgroundWorkerManager.cs | 4 ++-- .../DynamicBackgroundWorkerHandler.cs | 6 ++++++ .../DynamicBackgroundWorkerHandlerRegistry.cs | 11 ++++------- .../DynamicBackgroundWorkerManagerExtensions.cs | 3 +-- .../IDynamicBackgroundWorkerHandlerRegistry.cs | 8 ++------ .../IDynamicBackgroundWorkerManager.cs | 3 +-- .../InMemoryDynamicBackgroundWorker.cs | 6 ++---- 10 files changed, 21 insertions(+), 27 deletions(-) create mode 100644 framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs 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 index 5f310827a2..77e7d37a97 100644 --- 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 @@ -31,7 +31,7 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa public virtual Task AddAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); 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 index 7ff3f85a1d..8f409e011d 100644 --- 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 @@ -29,7 +29,7 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana public virtual async Task AddAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); 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 index 5cbeb12faf..b5f1cfcbe4 100644 --- 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 @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -11,7 +10,7 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan public virtual Task AddAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default) { throw new AbpException( diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs index 598bd74014..a3073d0e53 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -30,7 +30,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan public virtual async Task AddAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); @@ -155,7 +155,7 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler) + DynamicBackgroundWorkerHandler handler) { var timer = ServiceProvider.GetRequiredService(); var serviceScopeFactory = ServiceProvider.GetRequiredService(); diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs new file mode 100644 index 0000000000..28f5c933d8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs @@ -0,0 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +public delegate Task DynamicBackgroundWorkerHandler(DynamicBackgroundWorkerExecutionContext context, CancellationToken cancellationToken); \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs index 0f9ea8c022..a30ea3e95a 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs @@ -1,21 +1,18 @@ -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; } + protected ConcurrentDictionary Handlers { get; } public DynamicBackgroundWorkerHandlerRegistry() { - Handlers = new ConcurrentDictionary>(); + Handlers = new ConcurrentDictionary(); } - public virtual void Register(string workerName, Func handler) + public virtual void Register(string workerName, DynamicBackgroundWorkerHandler handler) { Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); Check.NotNull(handler, nameof(handler)); @@ -35,7 +32,7 @@ public class DynamicBackgroundWorkerHandlerRegistry : IDynamicBackgroundWorkerHa return Handlers.ContainsKey(workerName); } - public virtual Func? Get(string workerName) + public virtual DynamicBackgroundWorkerHandler? 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/DynamicBackgroundWorkerManagerExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs index 498a37e28b..5fedbf1360 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; @@ -12,7 +11,7 @@ public static class DynamicBackgroundWorkerManagerExtensions public static Task AddAsync( this IDynamicBackgroundWorkerManager manager, string workerName, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default) { return manager.AddAsync( diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs index eb0db0e6d0..7915f108db 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs @@ -1,16 +1,12 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - namespace Volo.Abp.BackgroundWorkers; public interface IDynamicBackgroundWorkerHandlerRegistry { - void Register(string workerName, Func handler); + void Register(string workerName, DynamicBackgroundWorkerHandler handler); bool Unregister(string workerName); bool IsRegistered(string workerName); - Func? Get(string workerName); + DynamicBackgroundWorkerHandler? Get(string workerName); } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs index 317aa36433..5054f67897 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; @@ -17,7 +16,7 @@ public interface IDynamicBackgroundWorkerManager Task AddAsync( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, CancellationToken cancellationToken = default); /// 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 5001e96ff4..bab468a655 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs @@ -1,8 +1,6 @@ -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers; @@ -11,12 +9,12 @@ public class InMemoryDynamicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase { public string WorkerName { get; } - private readonly Func _handler; + private readonly DynamicBackgroundWorkerHandler _handler; public InMemoryDynamicBackgroundWorker( string workerName, DynamicBackgroundWorkerSchedule schedule, - Func handler, + DynamicBackgroundWorkerHandler handler, AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory)