Browse Source

Fix dynamic background job/worker code review issues

- Fix reflection method lookup in DefaultDynamicBackgroundJobManager to match by parameter types
- Add StopAllAsync to IDynamicBackgroundWorkerManager interface and all implementations
- Add semaphore locking to StopAllAsync in DefaultDynamicBackgroundWorkerManager with volatile _isDisposed
- Fix HangfireDynamicBackgroundWorkerManager.GetCron to match existing HangfireBackgroundWorkerManager format
- Fix QuartzDynamicBackgroundWorkerManager.UpdateScheduleAsync to reuse BuildTrigger method
- Fix TickerQDynamicBackgroundWorkerManager.IsRegistered to throw AbpException (consistent with other methods)
- Add GetAllNames and Clear to IDynamicBackgroundWorkerHandlerRegistry
- Call StopAllAsync in AbpBackgroundWorkersModule.OnApplicationShutdownAsync
- Move DynamicBackgroundWorkerManager_Tests to correct namespace/directory
- Fix singleton state pollution in BackgroundJobManager_Tests
- Update docs to warn about handler loss after restart for dynamic jobs and workers
pull/25059/head
maliming 1 week ago
parent
commit
024f16cd99
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 4
      docs/en/framework/infrastructure/background-jobs/index.md
  2. 2
      docs/en/framework/infrastructure/background-workers/index.md
  3. 9
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs
  4. 34
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs
  5. 27
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs
  6. 9
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs
  7. 4
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs
  8. 38
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs
  9. 12
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs
  10. 6
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs
  11. 6
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs
  12. 3
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs
  13. 3
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs

4
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<DynamicBackgroundJobArgs>`. 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.

2
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:

9
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)
{

34
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<IOptions<AbpHangfireOptions>>().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;
}
}

27
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<QuartzDynamicBackgroundWorkerAdapter>()
.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()

9
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;
}
}

4
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs

@ -49,6 +49,10 @@ public class AbpBackgroundWorkersModule : AbpModule
await context.ServiceProvider
.GetRequiredService<IBackgroundWorkerManager>()
.StopAsync(cancellationToken);
await context.ServiceProvider
.GetRequiredService<IDynamicBackgroundWorkerManager>()
.StopAllAsync(cancellationToken);
}
}

38
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<DefaultDynamicBackgroundWorkerManager> Logger { get; set; }
private readonly ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker> _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(

12
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<string> GetAllNames()
{
return Handlers.Keys.ToList().AsReadOnly();
}
public virtual void Clear()
{
Handlers.Clear();
}
}

6
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<string> GetAllNames();
void Clear();
}

6
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.
/// </summary>
bool IsRegistered(string workerName);
/// <summary>
/// Stops all dynamic workers and releases resources.
/// Called during application shutdown.
/// </summary>
Task StopAllAsync(CancellationToken cancellationToken = default);
}

3
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"));
}

3
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs → 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
{
Loading…
Cancel
Save