Browse Source

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.
pull/25066/head
SALİH ÖZKARA 3 weeks ago
parent
commit
ebb5fae17a
  1. 4
      docs/en/framework/infrastructure/background-workers/index.md
  2. 4
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
  3. 4
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
  4. 4
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs
  5. 19
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs
  6. 12
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs
  7. 68
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs

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

4
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);

4
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;

4
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;

19
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<IBackgroundWorker> _backgroundWorkers;
private readonly Dictionary<string, InMemoryDynamicBackgroundWorker> _dynamicWorkers;
private readonly ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker> _dynamicWorkers;
protected IServiceProvider ServiceProvider { get; }
protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; }
@ -30,7 +31,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry)
{
_backgroundWorkers = new List<IBackgroundWorker>();
_dynamicWorkers = new Dictionary<string, InMemoryDynamicBackgroundWorker>();
_dynamicWorkers = new ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker>();
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<AbpAsyncTimer>();
@ -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<AbpAsyncTimer>();
var serviceScopeFactory = ServiceProvider.GetRequiredService<IServiceScopeFactory>();

12
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));
}
}
}

68
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<bool>(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<ArgumentException>(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<ArgumentException>(async () =>
{
await _backgroundWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = -1000 },
(_, _) => Task.CompletedTask
);
});
}
}

Loading…
Cancel
Save