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