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