Browse Source

feat: Implement dynamic background worker management with Quartz and TickerQ

pull/25066/head
maliming 1 week ago
parent
commit
5babd75f91
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 19
      docs/en/framework/infrastructure/background-workers/index.md
  2. 2
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs
  3. 2
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs
  4. 2
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs
  5. 2
      framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs
  6. 2
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs
  7. 2
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs
  8. 2
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs
  9. 144
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
  10. 27
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs
  11. 168
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs
  12. 135
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
  13. 35
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs
  14. 143
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs
  15. 117
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs
  16. 164
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs
  17. 136
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs
  18. 144
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs
  19. 27
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs
  20. 6
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs
  21. 32
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs
  22. 42
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs
  23. 39
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs
  24. 128
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs

19
docs/en/framework/infrastructure/background-workers/index.md

@ -120,17 +120,17 @@ So, it resolves the given background worker and adds to the `IBackgroundWorkerMa
While we generally add workers in `OnApplicationInitializationAsync`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.
### Add Dynamic Workers at Runtime (Handler in Add)
### Dynamic Workers (Runtime Registration)
You can add a runtime worker without pre-defining a dedicated worker class by passing a handler directly to `AddAsync`.
You can add a runtime worker without pre-defining a dedicated worker class by injecting `IDynamicBackgroundWorkerManager` and passing a handler directly.
```csharp
await backgroundWorkerManager.AddAsync(
await dynamicBackgroundWorkerManager.AddAsync(
"InventorySyncWorker",
new DynamicBackgroundWorkerSchedule
{
Period = 30000 // 30 seconds
// CronExpression = "*/30 * * * * *" // optional (provider dependent)
// CronExpression = "*/30 * * * *" // every 30 minutes, supported by Hangfire, Quartz, TickerQ
},
async (context, cancellationToken) =>
{
@ -144,10 +144,10 @@ You can also **remove** a dynamic worker or **update its schedule** at runtime:
```csharp
// Remove a dynamic worker
var removed = await backgroundWorkerManager.RemoveAsync("InventorySyncWorker");
var removed = await dynamicBackgroundWorkerManager.RemoveAsync("InventorySyncWorker");
// Update the schedule of a dynamic worker
var updated = await backgroundWorkerManager.UpdateScheduleAsync(
var updated = await dynamicBackgroundWorkerManager.UpdateScheduleAsync(
"InventorySyncWorker",
new DynamicBackgroundWorkerSchedule
{
@ -158,10 +158,11 @@ var updated = await backgroundWorkerManager.UpdateScheduleAsync(
Key points:
* `IDynamicBackgroundWorkerManager` is a separate interface from `IBackgroundWorkerManager`, dedicated to runtime (non-type-safe) worker management.
* `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 based on `Period`. **`CronExpression` is only supported by scheduler-backed providers (Hangfire, Quartz, TickerQ).** The default in-memory provider ignores `CronExpression`.
* The `handler` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`.
* At least one of `Period` or `CronExpression` must be set in `DynamicBackgroundWorkerSchedule`.
* **`CronExpression` is only supported by scheduler-backed providers (Hangfire, Quartz, TickerQ).** The default in-memory provider requires `Period` and does not support `CronExpression` alone.
* `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.

2
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobConfiguration.cs

@ -1,4 +1,4 @@
using System;
using System;
namespace Volo.Abp.BackgroundJobs;

2
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs

@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;

2
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using Volo.Abp.DependencyInjection;

2
framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs

@ -1,4 +1,4 @@
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;

2
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;

2
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

2
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

144
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs

@ -9,7 +9,6 @@ using Hangfire.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Hangfire;
@ -21,12 +20,11 @@ namespace Volo.Abp.BackgroundWorkers.Hangfire;
public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISingletonDependency
{
protected AbpHangfireBackgroundJobServer BackgroundJobServer { get; set; } = default!;
protected IServiceProvider ServiceProvider { get; }
public HangfireBackgroundWorkerManager(
IServiceProvider serviceProvider,
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry)
: base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry)
public HangfireBackgroundWorkerManager(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public void Initialize()
@ -139,142 +137,6 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet
}
}
public override Task AddAsync(
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
return AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = DynamicBackgroundWorkerSchedule.DefaultPeriod
},
handler,
cancellationToken
);
}
public override Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
Check.NotNull(handler, nameof(handler));
schedule.Validate();
var cronExpression = schedule.CronExpression;
if (cronExpression.IsNullOrWhiteSpace())
{
var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
cronExpression = GetCron(period);
}
var logger = ServiceProvider.GetRequiredService<ILogger<HangfireBackgroundWorkerManager>>();
var abpHangfireOptions = ServiceProvider.GetRequiredService<IOptions<AbpHangfireOptions>>().Value;
var queueName = abpHangfireOptions.DefaultQueue;
var recurringJobId = $"DynamicWorker:{workerName}";
if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty))
{
logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead.");
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
else
{
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
queueName,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler);
return Task.CompletedTask;
}
public override Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName))
{
return Task.FromResult(false);
}
var recurringJobId = $"DynamicWorker:{workerName}";
RecurringJob.RemoveIfExists(recurringJobId);
DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName);
return Task.FromResult(true);
}
public override Task<bool> UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
schedule.Validate();
if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName))
{
return Task.FromResult(false);
}
var cronExpression = schedule.CronExpression;
if (cronExpression.IsNullOrWhiteSpace())
{
var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
cronExpression = GetCron(period);
}
var abpHangfireOptions = ServiceProvider.GetRequiredService<IOptions<AbpHangfireOptions>>().Value;
var queueName = abpHangfireOptions.DefaultQueue;
var recurringJobId = $"DynamicWorker:{workerName}";
if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty))
{
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
else
{
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
queueName,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
return Task.FromResult(true);
}
private static readonly MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static);
protected virtual string? GetRecurringJobId(IBackgroundWorker worker, Expression<Func<Task>> methodCall)
{

27
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs

@ -1,31 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
public class HangfireDynamicBackgroundWorkerAdapter : ITransientDependency
{
protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
protected IServiceProvider ServiceProvider { get; }
public ILogger<HangfireDynamicBackgroundWorkerAdapter> Logger { get; set; }
public HangfireDynamicBackgroundWorkerAdapter(
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry,
IServiceProvider serviceProvider)
{
DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry;
HandlerRegistry = handlerRegistry;
ServiceProvider = serviceProvider;
Logger = NullLogger<HangfireDynamicBackgroundWorkerAdapter>.Instance;
}
public virtual async Task DoWorkAsync(string workerName, CancellationToken cancellationToken = default)
{
var handler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName);
var handler = HandlerRegistry.Get(workerName);
if (handler == null)
{
Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName);
return;
}
await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken);
try
{
await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken);
}
catch (Exception ex)
{
await ServiceProvider.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
throw;
}
}
}

168
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs

@ -0,0 +1,168 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Hangfire;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
[Dependency(ReplaceServices = true)]
public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
{
protected IServiceProvider ServiceProvider { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
public ILogger<HangfireDynamicBackgroundWorkerManager> Logger { get; set; }
public HangfireDynamicBackgroundWorkerManager(
IServiceProvider serviceProvider,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry)
{
ServiceProvider = serviceProvider;
HandlerRegistry = handlerRegistry;
Logger = NullLogger<HangfireDynamicBackgroundWorkerManager>.Instance;
}
public virtual Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
Check.NotNull(handler, nameof(handler));
schedule.Validate();
var cronExpression = schedule.CronExpression;
if (cronExpression.IsNullOrWhiteSpace())
{
var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
cronExpression = GetCron(period);
}
ScheduleRecurringJob(workerName, cronExpression, cancellationToken);
HandlerRegistry.Register(workerName, handler);
return Task.CompletedTask;
}
public virtual Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!HandlerRegistry.IsRegistered(workerName))
{
return Task.FromResult(false);
}
var recurringJobId = $"DynamicWorker:{workerName}";
RecurringJob.RemoveIfExists(recurringJobId);
HandlerRegistry.Unregister(workerName);
return Task.FromResult(true);
}
public virtual Task<bool> 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 Task.FromResult(false);
}
var cronExpression = schedule.CronExpression;
if (cronExpression.IsNullOrWhiteSpace())
{
var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
cronExpression = GetCron(period);
}
ScheduleRecurringJob(workerName, cronExpression, cancellationToken);
return Task.FromResult(true);
}
public virtual bool IsRegistered(string workerName)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
return HandlerRegistry.IsRegistered(workerName);
}
protected virtual void ScheduleRecurringJob(string workerName, string cronExpression, CancellationToken cancellationToken)
{
var abpHangfireOptions = ServiceProvider.GetRequiredService<IOptions<AbpHangfireOptions>>().Value;
var queueName = abpHangfireOptions.DefaultQueue;
var recurringJobId = $"DynamicWorker:{workerName}";
if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty))
{
Logger.LogWarning(
"Current storage doesn't support specifying queues ({QueueName}) directly for a specific job. Please use the QueueAttribute instead.",
queueName);
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
else
{
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
queueName,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
cronExpression,
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});
}
}
protected virtual string GetCron(int period)
{
var time = TimeSpan.FromMilliseconds(period);
string cron;
if (time.TotalSeconds <= 59)
{
cron = $"*/{time.TotalSeconds} * * * * *";
}
else if (time.TotalMinutes <= 59)
{
cron = $"*/{time.TotalMinutes} * * * *";
}
else if (time.TotalHours <= 23)
{
cron = $"0 */{time.TotalHours} * * *";
}
else if (time.TotalDays <= 31)
{
cron = $"0 0 0 1/{time.TotalDays} * *";
}
else
{
throw new AbpException($"Cannot convert period: {period} to cron expression.");
}
return cron;
}
}

135
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs

@ -1,7 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
@ -11,15 +10,9 @@ namespace Volo.Abp.BackgroundWorkers.Quartz;
[Dependency(ReplaceServices = true)]
public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingletonDependency
{
public const string DynamicWorkerNameKey = "AbpDynamicWorkerName";
protected IScheduler Scheduler { get; }
public QuartzBackgroundWorkerManager(
IScheduler scheduler,
IServiceProvider serviceProvider,
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry)
: base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry)
public QuartzBackgroundWorkerManager(IScheduler scheduler)
{
Scheduler = scheduler;
}
@ -103,130 +96,4 @@ public class QuartzBackgroundWorkerManager : BackgroundWorkerManager, ISingleton
await Scheduler.ScheduleJob(quartzWork.JobDetail, quartzWork.Trigger, cancellationToken);
}
}
public override Task AddAsync(
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
return AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = DynamicBackgroundWorkerSchedule.DefaultPeriod
},
handler,
cancellationToken
);
}
public override async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
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.");
}
var jobKey = new JobKey($"DynamicWorker:{workerName}");
var triggerKey = new TriggerKey($"DynamicWorker:{workerName}");
var jobDetail = JobBuilder.Create<QuartzDynamicBackgroundWorkerAdapter>()
.WithIdentity(jobKey)
.UsingJobData(DynamicWorkerNameKey, workerName)
.Build();
var triggerBuilder = TriggerBuilder.Create()
.ForJob(jobDetail)
.WithIdentity(triggerKey);
if (!schedule.CronExpression.IsNullOrWhiteSpace())
{
triggerBuilder.WithCronSchedule(schedule.CronExpression);
}
else
{
triggerBuilder.WithSimpleSchedule(builder =>
builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever());
}
var trigger = triggerBuilder.Build();
if (await Scheduler.CheckExists(jobDetail.Key, cancellationToken))
{
await Scheduler.AddJob(jobDetail, true, true, cancellationToken);
await Scheduler.ResumeJob(jobDetail.Key, cancellationToken);
await Scheduler.RescheduleJob(trigger.Key, trigger, cancellationToken);
}
else
{
await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken);
}
DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler);
}
public override async Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName))
{
return false;
}
var jobKey = new JobKey($"DynamicWorker:{workerName}");
var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken);
if (deleted)
{
DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName);
}
return deleted;
}
public override async Task<bool> UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
schedule.Validate();
if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName))
{
return false;
}
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.");
}
var triggerKey = new TriggerKey($"DynamicWorker:{workerName}");
var triggerBuilder = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(new JobKey($"DynamicWorker:{workerName}"));
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);
return result != null;
}
}

35
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs

@ -1,41 +1,56 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
namespace Volo.Abp.BackgroundWorkers.Quartz;
public class QuartzDynamicBackgroundWorkerAdapter : IJob, ITransientDependency
{
protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
protected IServiceProvider ServiceProvider { get; }
public ILogger<QuartzDynamicBackgroundWorkerAdapter> Logger { get; set; }
public QuartzDynamicBackgroundWorkerAdapter(
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry,
IServiceProvider serviceProvider)
{
DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry;
HandlerRegistry = handlerRegistry;
ServiceProvider = serviceProvider;
Logger = NullLogger<QuartzDynamicBackgroundWorkerAdapter>.Instance;
}
public virtual async Task Execute(IJobExecutionContext context)
{
var workerName = context.MergedJobDataMap.GetString(QuartzBackgroundWorkerManager.DynamicWorkerNameKey);
var workerName = context.MergedJobDataMap.GetString(QuartzDynamicBackgroundWorkerManager.DynamicWorkerNameKey);
if (string.IsNullOrWhiteSpace(workerName))
{
return;
}
var nonNullWorkerName = workerName!;
var handler = DynamicBackgroundWorkerHandlerRegistry.Get(nonNullWorkerName);
var handler = HandlerRegistry.Get(workerName!);
if (handler == null)
{
Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName);
return;
}
await handler(
new DynamicBackgroundWorkerExecutionContext(nonNullWorkerName, ServiceProvider),
context.CancellationToken
);
try
{
await handler(
new DynamicBackgroundWorkerExecutionContext(workerName!, ServiceProvider),
context.CancellationToken);
}
catch (Exception ex)
{
await ServiceProvider.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
throw;
}
}
}

143
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs

@ -0,0 +1,143 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundWorkers.Quartz;
[Dependency(ReplaceServices = true)]
public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
{
public const string DynamicWorkerNameKey = "AbpDynamicWorkerName";
protected IScheduler Scheduler { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
public ILogger<QuartzDynamicBackgroundWorkerManager> Logger { get; set; }
public QuartzDynamicBackgroundWorkerManager(
IScheduler scheduler,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry)
{
Scheduler = scheduler;
HandlerRegistry = handlerRegistry;
Logger = NullLogger<QuartzDynamicBackgroundWorkerManager>.Instance;
}
public virtual async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
Check.NotNull(handler, nameof(handler));
schedule.Validate();
var jobKey = new JobKey($"DynamicWorker:{workerName}");
var triggerKey = new TriggerKey($"DynamicWorker:{workerName}");
var jobDetail = JobBuilder.Create<QuartzDynamicBackgroundWorkerAdapter>()
.WithIdentity(jobKey)
.UsingJobData(DynamicWorkerNameKey, workerName)
.Build();
var trigger = BuildTrigger(schedule, jobDetail, triggerKey);
if (await Scheduler.CheckExists(jobDetail.Key, cancellationToken))
{
await Scheduler.AddJob(jobDetail, true, true, cancellationToken);
await Scheduler.ResumeJob(jobDetail.Key, cancellationToken);
await Scheduler.RescheduleJob(trigger.Key, trigger, cancellationToken);
}
else
{
await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken);
}
HandlerRegistry.Register(workerName, handler);
}
public virtual async Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!HandlerRegistry.IsRegistered(workerName))
{
return false;
}
var jobKey = new JobKey($"DynamicWorker:{workerName}");
var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken);
if (deleted)
{
HandlerRegistry.Unregister(workerName);
}
return deleted;
}
public virtual async Task<bool> 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 triggerKey = new TriggerKey($"DynamicWorker:{workerName}");
var jobKey = new JobKey($"DynamicWorker:{workerName}");
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);
return result != null;
}
public virtual bool IsRegistered(string workerName)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
return HandlerRegistry.IsRegistered(workerName);
}
protected virtual ITrigger BuildTrigger(DynamicBackgroundWorkerSchedule schedule, IJobDetail jobDetail, TriggerKey triggerKey)
{
var triggerBuilder = TriggerBuilder.Create()
.ForJob(jobDetail)
.WithIdentity(triggerKey);
if (!schedule.CronExpression.IsNullOrWhiteSpace())
{
triggerBuilder.WithCronSchedule(schedule.CronExpression);
}
else
{
triggerBuilder.WithSimpleSchedule(builder =>
builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever());
}
return triggerBuilder.Build();
}
}

117
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs

@ -1,11 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using TickerQ.Utilities.Entities;
using TickerQ.Utilities.Enums;
using TickerQ.Utilities.Interfaces.Managers;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.TickerQ;
@ -17,21 +14,15 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl
{
protected AbpTickerQFunctionProvider AbpTickerQFunctionProvider { get; }
protected AbpTickerQBackgroundWorkersProvider AbpTickerQBackgroundWorkersProvider { get; }
protected ICronTickerManager<CronTickerEntity> CronTickerManager { get; }
protected AbpBackgroundWorkersTickerQOptions Options { get; }
public AbpTickerQBackgroundWorkerManager(
AbpTickerQFunctionProvider abpTickerQFunctionProvider,
AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider,
ICronTickerManager<CronTickerEntity> cronTickerManager,
IServiceProvider serviceProvider,
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry,
IOptions<AbpBackgroundWorkersTickerQOptions> options)
: base(serviceProvider, dynamicBackgroundWorkerHandlerRegistry)
{
AbpTickerQFunctionProvider = abpTickerQFunctionProvider;
AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider;
CronTickerManager = cronTickerManager;
Options = options.Value;
}
@ -79,114 +70,6 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl
await base.AddAsync(worker, cancellationToken);
}
public override Task AddAsync(
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
return AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = DynamicBackgroundWorkerSchedule.DefaultPeriod
},
handler,
cancellationToken
);
}
public override async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
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}";
AbpTickerQFunctionProvider.Functions[functionName] =
(string.Empty, TickerTaskPriority.LongRunning, async (tickerCancellationToken, serviceProvider, _) =>
{
var registeredHandler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName);
if (registeredHandler == null)
{
return;
}
await registeredHandler(
new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider),
tickerCancellationToken
);
});
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker
{
Function = functionName,
CronExpression = cronExpression,
WorkerType = typeof(AbpTickerQBackgroundWorkerManager)
};
await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
});
DynamicBackgroundWorkerHandlerRegistry.Register(workerName, handler);
}
public override Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!DynamicBackgroundWorkerHandlerRegistry.IsRegistered(workerName))
{
return Task.FromResult(false);
}
var functionName = $"DynamicWorker:{workerName}";
AbpTickerQFunctionProvider.Functions.Remove(functionName);
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName);
DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName);
return Task.FromResult(true);
}
public override async Task<bool> UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
schedule.Validate();
if (!DynamicBackgroundWorkerHandlerRegistry.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;
}
await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
});
return true;
}
protected virtual string GetCron(int period)
{
var time = TimeSpan.FromMilliseconds(period);

164
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs

@ -0,0 +1,164 @@
using System;
using System.Threading;
using System.Threading.Tasks;
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.TickerQ;
namespace Volo.Abp.BackgroundWorkers.TickerQ;
[Dependency(ReplaceServices = true)]
public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
{
protected AbpTickerQFunctionProvider AbpTickerQFunctionProvider { get; }
protected AbpTickerQBackgroundWorkersProvider AbpTickerQBackgroundWorkersProvider { get; }
protected ICronTickerManager<CronTickerEntity> CronTickerManager { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
public ILogger<TickerQDynamicBackgroundWorkerManager> Logger { get; set; }
public TickerQDynamicBackgroundWorkerManager(
AbpTickerQFunctionProvider abpTickerQFunctionProvider,
AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider,
ICronTickerManager<CronTickerEntity> cronTickerManager,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry)
{
AbpTickerQFunctionProvider = abpTickerQFunctionProvider;
AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider;
CronTickerManager = cronTickerManager;
HandlerRegistry = handlerRegistry;
Logger = NullLogger<TickerQDynamicBackgroundWorkerManager>.Instance;
}
public virtual async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
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}";
AbpTickerQFunctionProvider.Functions[functionName] =
(string.Empty, TickerTaskPriority.LongRunning, async (tickerCancellationToken, serviceProvider, _) =>
{
var registeredHandler = HandlerRegistry.Get(workerName);
if (registeredHandler == null)
{
return;
}
await registeredHandler(
new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider),
tickerCancellationToken);
}, 0);
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker
{
Function = functionName,
CronExpression = cronExpression,
WorkerType = typeof(TickerQDynamicBackgroundWorkerManager)
};
await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
});
HandlerRegistry.Register(workerName, handler);
}
public virtual Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!HandlerRegistry.IsRegistered(workerName))
{
return Task.FromResult(false);
}
var functionName = $"DynamicWorker:{workerName}";
AbpTickerQFunctionProvider.Functions.Remove(functionName);
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName);
HandlerRegistry.Unregister(workerName);
return Task.FromResult(true);
}
public virtual async Task<bool> 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;
}
await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
});
return true;
}
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)
{
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.");
}
}

136
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerManager.cs

@ -1,11 +1,8 @@
using System;
using System.Collections.Concurrent;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers;
@ -19,21 +16,13 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
private bool _isDisposed;
private readonly List<IBackgroundWorker> _backgroundWorkers;
private readonly ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker> _dynamicWorkers;
protected IServiceProvider ServiceProvider { get; }
protected IDynamicBackgroundWorkerHandlerRegistry DynamicBackgroundWorkerHandlerRegistry { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundWorkerManager"/> class.
/// </summary>
public BackgroundWorkerManager(
IServiceProvider serviceProvider,
IDynamicBackgroundWorkerHandlerRegistry dynamicBackgroundWorkerHandlerRegistry)
public BackgroundWorkerManager()
{
_backgroundWorkers = new List<IBackgroundWorker>();
_dynamicWorkers = new ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker>();
ServiceProvider = serviceProvider;
DynamicBackgroundWorkerHandlerRegistry = dynamicBackgroundWorkerHandlerRegistry;
}
public virtual async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default)
@ -46,127 +35,6 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
}
}
public virtual Task AddAsync(
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
return AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = DynamicBackgroundWorkerSchedule.DefaultPeriod
},
handler,
cancellationToken
);
}
public virtual async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
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>();
var serviceScopeFactory = ServiceProvider.GetRequiredService<IServiceScopeFactory>();
var worker = new InMemoryDynamicBackgroundWorker(
workerName,
schedule,
timer,
serviceScopeFactory,
DynamicBackgroundWorkerHandlerRegistry
);
worker.ServiceProvider = ServiceProvider;
worker.LazyServiceProvider = ServiceProvider.GetRequiredService<IAbpLazyServiceProvider>();
_dynamicWorkers[workerName] = worker;
await AddAsync(worker, cancellationToken);
}
public virtual async Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!_dynamicWorkers.TryGetValue(workerName, out var worker))
{
return false;
}
await worker.StopAsync(cancellationToken);
_backgroundWorkers.Remove(worker);
_dynamicWorkers.TryRemove(workerName, out _);
DynamicBackgroundWorkerHandlerRegistry.Unregister(workerName);
return true;
}
public virtual async Task<bool> UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
schedule.Validate();
if (!_dynamicWorkers.TryGetValue(workerName, out var oldWorker))
{
return false;
}
if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression))
{
throw new AbpException("Default background worker manager does not support cron expression without period.");
}
var handler = DynamicBackgroundWorkerHandlerRegistry.Get(workerName);
if (handler == null)
{
return false;
}
await oldWorker.StopAsync(cancellationToken);
_backgroundWorkers.Remove(oldWorker);
_dynamicWorkers.TryRemove(workerName, out _);
var timer = ServiceProvider.GetRequiredService<AbpAsyncTimer>();
var serviceScopeFactory = ServiceProvider.GetRequiredService<IServiceScopeFactory>();
var newWorker = new InMemoryDynamicBackgroundWorker(
workerName,
schedule,
timer,
serviceScopeFactory,
DynamicBackgroundWorkerHandlerRegistry
);
newWorker.ServiceProvider = ServiceProvider;
newWorker.LazyServiceProvider = ServiceProvider.GetRequiredService<IAbpLazyServiceProvider>();
_dynamicWorkers[workerName] = newWorker;
await AddAsync(newWorker, cancellationToken);
return true;
}
public virtual void Dispose()
{
if (_isDisposed)

144
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs

@ -0,0 +1,144 @@
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 Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers;
public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency, IDisposable
{
protected IServiceProvider ServiceProvider { get; }
public ILogger<DefaultDynamicBackgroundWorkerManager> Logger { get; set; }
private readonly ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker> _dynamicWorkers;
private bool _isDisposed;
public DefaultDynamicBackgroundWorkerManager(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Logger = NullLogger<DefaultDynamicBackgroundWorkerManager>.Instance;
_dynamicWorkers = new ConcurrentDictionary<string, InMemoryDynamicBackgroundWorker>();
}
public virtual async Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
Check.NotNull(handler, nameof(handler));
schedule.Validate();
if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression))
{
throw new AbpException(
$"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " +
"Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ).");
}
if (_dynamicWorkers.TryRemove(workerName, out var existingWorker))
{
await existingWorker.StopAsync(cancellationToken);
Logger.LogInformation("Replaced existing dynamic worker: {WorkerName}", workerName);
}
var worker = CreateDynamicWorker(workerName, schedule, handler);
_dynamicWorkers[workerName] = worker;
await worker.StartAsync(cancellationToken);
}
public virtual async Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!_dynamicWorkers.TryRemove(workerName, out var worker))
{
return false;
}
await worker.StopAsync(cancellationToken);
return true;
}
public virtual Task<bool> UpdateScheduleAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
schedule.Validate();
if (schedule.Period == null && !string.IsNullOrWhiteSpace(schedule.CronExpression))
{
throw new AbpException(
$"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " +
"Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ).");
}
if (!_dynamicWorkers.TryGetValue(workerName, out var worker))
{
return Task.FromResult(false);
}
worker.UpdateSchedule(schedule);
return Task.FromResult(true);
}
public virtual bool IsRegistered(string workerName)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
return _dynamicWorkers.ContainsKey(workerName);
}
public virtual void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
foreach (var kvp in _dynamicWorkers)
{
try
{
kvp.Value.StopAsync(CancellationToken.None).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}
_dynamicWorkers.Clear();
}
protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler)
{
var timer = ServiceProvider.GetRequiredService<AbpAsyncTimer>();
var serviceScopeFactory = ServiceProvider.GetRequiredService<IServiceScopeFactory>();
var worker = new InMemoryDynamicBackgroundWorker(
workerName, schedule, handler, timer, serviceScopeFactory);
worker.ServiceProvider = ServiceProvider;
worker.LazyServiceProvider = ServiceProvider.GetRequiredService<IAbpLazyServiceProvider>();
return worker;
}
}

27
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs

@ -0,0 +1,27 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers;
public static class DynamicBackgroundWorkerManagerExtensions
{
/// <summary>
/// Adds a dynamic worker with the default schedule (<see cref="DynamicBackgroundWorkerSchedule.DefaultPeriod"/>).
/// </summary>
public static Task AddAsync(
this IDynamicBackgroundWorkerManager manager,
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default)
{
return manager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = DynamicBackgroundWorkerSchedule.DefaultPeriod
},
handler,
cancellationToken);
}
}

6
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs

@ -18,5 +18,11 @@ public class DynamicBackgroundWorkerSchedule
$"Period must be greater than 0 when provided. Given value: {Period.Value}.",
nameof(Period));
}
if (Period == null && string.IsNullOrWhiteSpace(CronExpression))
{
throw new ArgumentException(
"At least one of 'Period' or 'CronExpression' must be set.");
}
}
}

32
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IBackgroundWorkerManager.cs

@ -1,5 +1,4 @@
using System;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Threading;
@ -18,33 +17,4 @@ public interface IBackgroundWorkerManager : IRunnable
/// </param>
/// <param name="cancellationToken"></param>
Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a dynamic worker by name and handler.
/// </summary>
Task AddAsync(
string workerName,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default);
/// <summary>
/// Adds a dynamic worker by name, schedule and handler.
/// </summary>
Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes a previously added dynamic worker by name.
/// Returns true if the worker was found and removed; false otherwise.
/// </summary>
Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default);
/// <summary>
/// Updates the schedule of a previously added dynamic worker.
/// Returns true if the worker was found and updated; false otherwise.
/// </summary>
Task<bool> UpdateScheduleAsync(string workerName, DynamicBackgroundWorkerSchedule schedule, CancellationToken cancellationToken = default);
}

42
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs

@ -0,0 +1,42 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers;
/// <summary>
/// Manages dynamic background workers that are registered at runtime
/// without requiring a strongly-typed worker class.
/// </summary>
public interface IDynamicBackgroundWorkerManager
{
/// <summary>
/// Adds a dynamic worker by name, schedule and handler.
/// If a worker with the same name already exists, it will be replaced.
/// </summary>
Task AddAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes a previously added dynamic worker by name.
/// Returns true if the worker was found and removed; false otherwise.
/// </summary>
Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default);
/// <summary>
/// Updates the schedule of a previously added dynamic worker.
/// Returns true if the worker was found and updated; false otherwise.
/// </summary>
Task<bool> UpdateScheduleAsync(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks whether a dynamic worker with the given name is registered.
/// </summary>
bool IsRegistered(string workerName);
}

39
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs

@ -1,3 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -7,34 +9,45 @@ namespace Volo.Abp.BackgroundWorkers;
public class InMemoryDynamicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase
{
protected string WorkerName { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }
public string WorkerName { get; }
private readonly Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> _handler;
public InMemoryDynamicBackgroundWorker(
string workerName,
DynamicBackgroundWorkerSchedule schedule,
Func<DynamicBackgroundWorkerExecutionContext, CancellationToken, Task> handler,
AbpAsyncTimer timer,
IServiceScopeFactory serviceScopeFactory,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry)
IServiceScopeFactory serviceScopeFactory)
: base(timer, serviceScopeFactory)
{
WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
Check.NotNull(schedule, nameof(schedule));
HandlerRegistry = Check.NotNull(handlerRegistry, nameof(handlerRegistry));
_handler = Check.NotNull(handler, nameof(handler));
Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
CronExpression = schedule.CronExpression;
}
public virtual void UpdateSchedule(DynamicBackgroundWorkerSchedule schedule)
{
Check.NotNull(schedule, nameof(schedule));
Timer.Stop();
Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod;
CronExpression = schedule.CronExpression;
Timer.Start(StartCancellationToken);
}
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
{
var handler = HandlerRegistry.Get(WorkerName);
if (handler == null)
{
Logger.LogWarning("No dynamic background worker handler registered for: {WorkerName}", WorkerName);
return;
}
await handler(new DynamicBackgroundWorkerExecutionContext(WorkerName, workerContext.ServiceProvider), workerContext.CancellationToken);
await _handler(
new DynamicBackgroundWorkerExecutionContext(WorkerName, workerContext.ServiceProvider),
workerContext.CancellationToken);
}
public override string ToString()
{
return $"DynamicWorker:{WorkerName}";
}
}

128
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicBackgroundWorkerManager_Tests.cs

@ -9,21 +9,19 @@ namespace Volo.Abp.BackgroundJobs;
public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
{
private readonly IBackgroundWorkerManager _backgroundWorkerManager;
private readonly IDynamicBackgroundWorkerHandlerRegistry _handlerRegistry;
private readonly IDynamicBackgroundWorkerManager _dynamicWorkerManager;
public DynamicBackgroundWorkerManager_Tests()
{
_backgroundWorkerManager = GetRequiredService<IBackgroundWorkerManager>();
_handlerRegistry = GetRequiredService<IDynamicBackgroundWorkerHandlerRegistry>();
_dynamicWorkerManager = GetRequiredService<IDynamicBackgroundWorkerManager>();
}
[Fact]
public async Task Should_Register_Dynamic_Handler_When_Added()
public async Task Should_Register_Dynamic_Worker()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
@ -32,7 +30,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
(_, _) => Task.CompletedTask
);
_handlerRegistry.IsRegistered(workerName).ShouldBeTrue();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
}
[Fact]
@ -41,7 +39,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
var workerName = "dynamic-worker-" + Guid.NewGuid();
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
@ -63,12 +61,25 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
(await tcs.Task).ShouldBeTrue();
}
[Fact]
public async Task Should_Add_Dynamic_Worker_With_Default_Schedule()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await _dynamicWorkerManager.AddAsync(
workerName,
(_, _) => Task.CompletedTask
);
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
}
[Fact]
public async Task Should_Remove_Dynamic_Worker()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
@ -77,17 +88,17 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
(_, _) => Task.CompletedTask
);
_handlerRegistry.IsRegistered(workerName).ShouldBeTrue();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
var result = await _backgroundWorkerManager.RemoveAsync(workerName);
var result = await _dynamicWorkerManager.RemoveAsync(workerName);
result.ShouldBeTrue();
_handlerRegistry.IsRegistered(workerName).ShouldBeFalse();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse();
}
[Fact]
public async Task Should_Return_False_When_Removing_NonExistent_Worker()
{
var result = await _backgroundWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid());
var result = await _dynamicWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid());
result.ShouldBeFalse();
}
@ -97,7 +108,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
var workerName = "dynamic-worker-" + Guid.NewGuid();
var executionCount = 0;
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
@ -110,7 +121,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
}
);
var result = await _backgroundWorkerManager.UpdateScheduleAsync(
var result = await _dynamicWorkerManager.UpdateScheduleAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
@ -119,7 +130,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
);
result.ShouldBeTrue();
_handlerRegistry.IsRegistered(workerName).ShouldBeTrue();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
var timeout = TimeSpan.FromSeconds(5);
var startTime = DateTime.UtcNow;
@ -134,7 +145,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
[Fact]
public async Task Should_Return_False_When_Updating_NonExistent_Worker()
{
var result = await _backgroundWorkerManager.UpdateScheduleAsync(
var result = await _dynamicWorkerManager.UpdateScheduleAsync(
"non-existent-worker-" + Guid.NewGuid(),
new DynamicBackgroundWorkerSchedule { Period = 1000 }
);
@ -146,20 +157,15 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
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(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 60000 },
(_, _) =>
{
firstHandlerCalled = true;
return Task.CompletedTask;
}
(_, _) => Task.CompletedTask
);
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 50 },
(_, _) =>
@ -173,11 +179,11 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
completedTask.ShouldBe(secondHandlerTcs.Task);
(await secondHandlerTcs.Task).ShouldBeTrue();
_handlerRegistry.IsRegistered(workerName).ShouldBeTrue();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
var removed = await _backgroundWorkerManager.RemoveAsync(workerName);
var removed = await _dynamicWorkerManager.RemoveAsync(workerName);
removed.ShouldBeTrue();
_handlerRegistry.IsRegistered(workerName).ShouldBeFalse();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse();
}
[Fact]
@ -187,7 +193,7 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 0 },
(_, _) => Task.CompletedTask
@ -202,11 +208,73 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await _backgroundWorkerManager.AddAsync(
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = -1000 },
(_, _) => Task.CompletedTask
);
});
}
[Fact]
public async Task Should_Throw_When_No_Period_And_No_CronExpression()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule(),
(_, _) => Task.CompletedTask
);
});
}
[Fact]
public async Task Should_Continue_Running_After_Handler_Throws_Exception()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
var callCount = 0;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 50 },
(_, _) =>
{
var count = Interlocked.Increment(ref callCount);
if (count == 1)
{
throw new InvalidOperationException("Simulated failure");
}
tcs.TrySetResult(true);
return Task.CompletedTask;
}
);
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(5000));
completedTask.ShouldBe(tcs.Task);
callCount.ShouldBeGreaterThan(1);
}
[Fact]
public async Task Should_Not_Be_Registered_After_Remove()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse();
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 1000 },
(_, _) => Task.CompletedTask
);
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue();
await _dynamicWorkerManager.RemoveAsync(workerName);
_dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse();
}
}

Loading…
Cancel
Save