diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md index 4a67077e0e..e6bfd6255f 100644 --- a/docs/en/framework/infrastructure/background-jobs/index.md +++ b/docs/en/framework/infrastructure/background-jobs/index.md @@ -423,6 +423,10 @@ bool removed = _dynamicJobManager.UnregisterHandler("ProcessOrder"); - **Dynamic handler path**: When the job name matches a registered dynamic handler, the args are wrapped as `DynamicBackgroundJobArgs` (an internal transport type) and enqueued through `IBackgroundJobManager.EnqueueAsync`. When the job executes, the framework looks up the handler by name and invokes it. - All dynamic jobs go through the **standard typed job pipeline**, which means they work with all providers (Default, Hangfire, Quartz, RabbitMQ, TickerQ) without any provider-specific changes. +> **Note:** If the job name matches both a registered typed job configuration and a dynamic handler, **the typed job takes priority** and the dynamic handler is ignored. To avoid confusion, use distinct names for dynamic handlers that do not conflict with existing typed job names. + +> **Important:** Dynamic job handlers are stored **in memory only** and are not persisted across application restarts. When using a persistent provider (Hangfire, Quartz, RabbitMQ, TickerQ), enqueued jobs survive a restart but if no handler is re-registered, the job executor will throw an exception when the job is picked up. To ensure handlers are always available, register them in `OnApplicationInitialization` so they are re-registered on every startup. + ## Integrations Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations. diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index ab5f42b207..8355739e95 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -178,6 +178,8 @@ var updated = await dynamicWorkerManager.UpdateScheduleAsync( * `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. +> **Important:** Dynamic worker handlers are stored **in memory only** and are not persisted across application restarts. When using a persistent scheduler provider (Hangfire or Quartz), the recurring job entries remain in the database after a restart, but the handlers will no longer be registered. Until the handler is re-registered, each scheduled execution will be **skipped with a warning log**. To ensure handlers are always available, register them in `OnApplicationInitializationAsync` so they are re-registered on every startup. + ## 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/DefaultDynamicBackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs index 32bea1391b..7028e0f858 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs @@ -109,9 +109,12 @@ public class DefaultDynamicBackgroundJobManager : IDynamicBackgroundJobManager, { var method = typeof(IBackgroundJobManager) .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(m => m.Name == nameof(IBackgroundJobManager.EnqueueAsync) - && m.IsGenericMethodDefinition - && m.GetParameters().Length == 3); + .FirstOrDefault(m => + m.Name == nameof(IBackgroundJobManager.EnqueueAsync) + && m.IsGenericMethodDefinition + && m.GetParameters() is { Length: 3 } p + && p[1].ParameterType == typeof(BackgroundJobPriority) + && p[2].ParameterType == typeof(TimeSpan?)); if (method == null) { 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 77e7d37a97..f34237fcc0 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 @@ -102,6 +102,12 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa return HandlerRegistry.IsRegistered(workerName); } + public virtual Task StopAllAsync(CancellationToken cancellationToken = default) + { + HandlerRegistry.Clear(); + return Task.CompletedTask; + } + protected virtual void ScheduleRecurringJob(string workerName, string cronExpression, CancellationToken cancellationToken) { var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; @@ -140,31 +146,29 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa protected virtual string GetCron(int period) { var time = TimeSpan.FromMilliseconds(period); + string cron; if (time.TotalSeconds <= 59) { - var seconds = (int)Math.Round(time.TotalSeconds); - return $"*/{seconds} * * * * *"; + cron = $"*/{time.TotalSeconds} * * * * *"; } - - if (time.TotalMinutes <= 59) + else if (time.TotalMinutes <= 59) { - var minutes = (int)Math.Round(time.TotalMinutes); - return $"*/{minutes} * * * *"; + cron = $"*/{time.TotalMinutes} * * * *"; } - - if (time.TotalHours <= 23) + else if (time.TotalHours <= 23) { - var hours = (int)Math.Round(time.TotalHours); - return $"0 */{hours} * * *"; + cron = $"0 */{time.TotalHours} * * *"; } - - if (time.TotalDays <= 31) + else if (time.TotalDays <= 31) + { + cron = $"0 0 0 1/{time.TotalDays} * *"; + } + else { - var days = (int)Math.Round(time.TotalDays); - return $"0 0 */{days} * *"; + throw new AbpException($"Cannot convert period: {period} to cron expression."); } - 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/QuartzDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs index 8f409e011d..4c8b643782 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 @@ -94,22 +94,13 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var jobDetail = JobBuilder.Create() + .WithIdentity(jobKey) + .UsingJobData(DynamicWorkerNameKey, workerName) + .Build(); - 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); + var trigger = BuildTrigger(schedule, jobDetail, triggerKey); + var result = await Scheduler.RescheduleJob(triggerKey, trigger, cancellationToken); return result != null; } @@ -119,6 +110,12 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana return HandlerRegistry.IsRegistered(workerName); } + public virtual Task StopAllAsync(CancellationToken cancellationToken = default) + { + HandlerRegistry.Clear(); + return Task.CompletedTask; + } + protected virtual ITrigger BuildTrigger(DynamicBackgroundWorkerSchedule schedule, IJobDetail jobDetail, TriggerKey triggerKey) { var triggerBuilder = TriggerBuilder.Create() 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 b5f1cfcbe4..45e53fc53d 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 @@ -38,6 +38,13 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan public virtual bool IsRegistered(string workerName) { - return false; + 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 Task StopAllAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs index 3b1b18e8b3..1d944817bf 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs @@ -49,6 +49,10 @@ public class AbpBackgroundWorkersModule : AbpModule await context.ServiceProvider .GetRequiredService() .StopAsync(cancellationToken); + + await context.ServiceProvider + .GetRequiredService() + .StopAllAsync(cancellationToken); } } 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 a3073d0e53..4116baa6c2 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -10,14 +10,14 @@ using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers; -public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency, IDisposable +public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency { protected IServiceProvider ServiceProvider { get; } public ILogger Logger { get; set; } private readonly ConcurrentDictionary _dynamicWorkers; private readonly SemaphoreSlim _semaphore; - private bool _isDisposed; + private volatile bool _isDisposed; public DefaultDynamicBackgroundWorkerManager(IServiceProvider serviceProvider) { @@ -127,29 +127,41 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan return _dynamicWorkers.ContainsKey(workerName); } - public virtual void Dispose() + public virtual async Task StopAllAsync(CancellationToken cancellationToken = default) { if (_isDisposed) { return; } - _isDisposed = true; - - foreach (var kvp in _dynamicWorkers) + await _semaphore.WaitAsync(cancellationToken); + try { - try + if (_isDisposed) { - kvp.Value.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); + return; } - catch (Exception ex) + + _isDisposed = true; + + foreach (var kvp in _dynamicWorkers) { - Logger.LogException(ex); + try + { + await kvp.Value.StopAsync(cancellationToken); + } + catch (Exception ex) + { + Logger.LogException(ex); + } } - } - _dynamicWorkers.Clear(); - _semaphore.Dispose(); + _dynamicWorkers.Clear(); + } + finally + { + _semaphore.Release(); + } } protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker( 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 a30ea3e95a..55e8617915 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using Volo.Abp.DependencyInjection; namespace Volo.Abp.BackgroundWorkers; @@ -37,4 +39,14 @@ public class DynamicBackgroundWorkerHandlerRegistry : IDynamicBackgroundWorkerHa Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); return Handlers.TryGetValue(workerName, out var handler) ? handler : null; } + + public virtual IReadOnlyCollection GetAllNames() + { + return Handlers.Keys.ToList().AsReadOnly(); + } + + public virtual void Clear() + { + Handlers.Clear(); + } } 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 7915f108db..0b7d6757dc 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Volo.Abp.BackgroundWorkers; public interface IDynamicBackgroundWorkerHandlerRegistry @@ -9,4 +11,8 @@ public interface IDynamicBackgroundWorkerHandlerRegistry bool IsRegistered(string workerName); DynamicBackgroundWorkerHandler? Get(string workerName); + + IReadOnlyCollection GetAllNames(); + + void Clear(); } 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 5054f67897..34f4d07ccd 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs @@ -38,4 +38,10 @@ public interface IDynamicBackgroundWorkerManager /// Checks whether a dynamic worker with the given name is registered. /// bool IsRegistered(string workerName); + + /// + /// Stops all dynamic workers and releases resources. + /// Called during application shutdown. + /// + Task StopAllAsync(CancellationToken cancellationToken = default); } diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs index 8183d49c26..29212664ff 100644 --- a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs @@ -81,7 +81,7 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase [Fact] public async Task Should_Execute_Dynamic_Handler_Job() { - _tracker.ExecutedJsonData.IsEmpty.ShouldBeTrue(); + var countBefore = _tracker.ExecutedJsonData.Count; await _backgroundJobExecuter.ExecuteAsync( new JobExecutionContext( @@ -91,6 +91,7 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase ) ); + _tracker.ExecutedJsonData.Count.ShouldBeGreaterThan(countBefore); _tracker.ExecutedJsonData.ShouldContain(d => d.Contains("ORD-001")); } diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs similarity index 99% rename from framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs rename to framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs index be99167d85..a61c99fdc1 100644 --- a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs @@ -5,10 +5,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Shouldly; +using Volo.Abp.BackgroundJobs; using Volo.Abp.BackgroundWorkers; using Xunit; -namespace Volo.Abp.BackgroundJobs; +namespace Volo.Abp.BackgroundWorkers; public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase {