Browse Source

fix: Update documentation and improve dynamic background worker management for TickerQ and Quartz integration

pull/25066/head
maliming 5 months ago
parent
commit
ad4c47a39c
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 3
      docs/en/framework/infrastructure/background-workers/index.md
  2. 4
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs
  3. 194
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs
  4. 4
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs
  5. 1
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj
  6. 67
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs
  7. 2
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs
  8. 0
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs

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

@ -173,7 +173,8 @@ var updated = await dynamicWorkerManager.UpdateScheduleAsync(
* `workerName` is the runtime identifier of the dynamic worker. If a worker with the same name already exists, it will be **replaced**.
* The `handler` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. It is a good practice to **resolve dependencies** from the `workerContext.ServiceProvider` instead of constructor injection.
* At least one of `Period` or `CronExpression` must be set in `DynamicBackgroundWorkerSchedule`.
* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md), [TickerQ](./tickerq.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone.
* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone.
* **[TickerQ](./tickerq.md) does not support dynamic background workers** because it uses `FrozenDictionary` for function registration, which requires all functions to be registered before the application starts.
* `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/HangfireDynamicBackgroundWorkerManager.cs

@ -116,7 +116,7 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
adapter => adapter.DoWorkAsync(workerName, CancellationToken.None),
cronExpression,
new RecurringJobOptions
{
@ -128,7 +128,7 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa
RecurringJob.AddOrUpdate<HangfireDynamicBackgroundWorkerAdapter>(
recurringJobId,
queueName,
adapter => adapter.DoWorkAsync(workerName, cancellationToken),
adapter => adapter.DoWorkAsync(workerName, CancellationToken.None),
cronExpression,
new RecurringJobOptions
{

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

@ -1,210 +1,44 @@
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 TickerQ.Utilities.Entities;
using TickerQ.Utilities.Enums;
using TickerQ.Utilities.Interfaces.Managers;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
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; }
private readonly ConcurrentDictionary<string, Guid> _cronTickerIds;
public TickerQDynamicBackgroundWorkerManager(
AbpTickerQFunctionProvider abpTickerQFunctionProvider,
AbpTickerQBackgroundWorkersProvider abpTickerQBackgroundWorkersProvider,
ICronTickerManager<CronTickerEntity> cronTickerManager,
IDynamicBackgroundWorkerHandlerRegistry handlerRegistry)
{
AbpTickerQFunctionProvider = abpTickerQFunctionProvider;
AbpTickerQBackgroundWorkersProvider = abpTickerQBackgroundWorkersProvider;
CronTickerManager = cronTickerManager;
HandlerRegistry = handlerRegistry;
Logger = NullLogger<TickerQDynamicBackgroundWorkerManager>.Instance;
_cronTickerIds = new ConcurrentDictionary<string, Guid>();
}
public virtual async Task AddAsync(
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();
// If replacing an existing worker, delete the old cron entry first
if (_cronTickerIds.TryRemove(workerName, out var existingId))
{
await CronTickerManager.DeleteAsync(existingId, cancellationToken);
}
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;
}
try
{
await registeredHandler(
new DynamicBackgroundWorkerExecutionContext(workerName, serviceProvider),
tickerCancellationToken);
}
catch (Exception ex)
{
await serviceProvider.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
throw;
}
}, 0);
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers[functionName] = new AbpTickerQCronBackgroundWorker
{
Function = functionName,
CronExpression = cronExpression,
WorkerType = typeof(TickerQDynamicBackgroundWorkerManager)
};
var result = await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
}, cancellationToken);
if (result.IsSucceeded && result.Result != null)
{
_cronTickerIds[workerName] = result.Result.Id;
}
HandlerRegistry.Register(workerName, handler);
throw new AbpException(
"TickerQ does not support dynamic background worker registration at runtime. " +
"TickerQ uses FrozenDictionary for function registration, which requires all functions to be registered before the application starts. " +
"Please use Hangfire or Quartz provider for dynamic background workers.");
}
public virtual async Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
public virtual Task<bool> RemoveAsync(string workerName, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(workerName, nameof(workerName));
if (!HandlerRegistry.IsRegistered(workerName))
{
return false;
}
var functionName = $"DynamicWorker:{workerName}";
AbpTickerQFunctionProvider.Functions.Remove(functionName);
AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Remove(functionName);
HandlerRegistry.Unregister(workerName);
if (_cronTickerIds.TryRemove(workerName, out var cronTickerId))
{
await CronTickerManager.DeleteAsync(cronTickerId, cancellationToken);
}
return true;
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 async Task<bool> UpdateScheduleAsync(
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 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;
}
// Delete old entry and create new one with updated expression
if (_cronTickerIds.TryRemove(workerName, out var oldCronTickerId))
{
await CronTickerManager.DeleteAsync(oldCronTickerId, cancellationToken);
}
var result = await CronTickerManager.AddAsync(new CronTickerEntity
{
Function = functionName,
Expression = cronExpression
}, cancellationToken);
if (result.IsSucceeded && result.Result != null)
{
_cronTickerIds[workerName] = result.Result.Id;
}
return true;
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 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)
{
Logger.LogWarning(
"TickerQ does not support sub-minute intervals. Period {Period}ms will be rounded up to every minute.",
period);
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.");
return false;
}
}

4
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs

@ -1,6 +1,7 @@
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundJobs.DemoApp.Shared;
using Volo.Abp.BackgroundJobs.Quartz;
using Volo.Abp.BackgroundWorkers.Quartz;
using Volo.Abp.Modularity;
namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz;
@ -8,7 +9,8 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz;
[DependsOn(
typeof(DemoAppSharedModule),
typeof(AbpAutofacModule),
typeof(AbpBackgroundJobsQuartzModule)
typeof(AbpBackgroundJobsQuartzModule),
typeof(AbpBackgroundWorkersQuartzModule)
)]
public class DemoAppQuartzModule : AbpModule
{

1
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj

@ -12,6 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.BackgroundJobs.Quartz\Volo.Abp.BackgroundJobs.Quartz.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.BackgroundWorkers.Quartz\Volo.Abp.BackgroundWorkers.Quartz.csproj" />
<ProjectReference Include="..\Volo.Abp.BackgroundJobs.DemoApp.Shared\Volo.Abp.BackgroundJobs.DemoApp.Shared.csproj" />
</ItemGroup>

67
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs

@ -1,5 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
@ -8,11 +12,70 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared
[DependsOn(typeof(AbpMultiTenancyModule))]
public class DemoAppSharedModule : AbpModule
{
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
public override async Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
{
context.ServiceProvider
.GetRequiredService<SampleJobCreator>()
.CreateJobs();
await DynamicBackgroundWorkerDemoAsync(context);
}
private async Task DynamicBackgroundWorkerDemoAsync(ApplicationInitializationContext context)
{
var dynamicWorkerManager = context.ServiceProvider
.GetService<IDynamicBackgroundWorkerManager>();
if (dynamicWorkerManager == null)
{
return;
}
// AddAsync: Register a dynamic worker with a schedule and handler
await dynamicWorkerManager.AddAsync(
"DemoHeartbeatWorker",
new DynamicBackgroundWorkerSchedule
{
Period = 5000 //5 seconds
},
async (workerContext, cancellationToken) =>
{
Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed.");
await Task.CompletedTask;
}
);
// IsRegistered: Check if a dynamic worker is registered
var isRegistered = dynamicWorkerManager.IsRegistered("DemoHeartbeatWorker");
Console.WriteLine($"DemoHeartbeatWorker is registered: {isRegistered}");
// UpdateScheduleAsync: Update the schedule of an existing dynamic worker
var updated = await dynamicWorkerManager.UpdateScheduleAsync(
"DemoHeartbeatWorker",
new DynamicBackgroundWorkerSchedule
{
Period = 10000 //Change to 10 seconds
}
);
Console.WriteLine($"DemoHeartbeatWorker schedule updated: {updated}");
// RemoveAsync: Remove a dynamic worker
var removed = await dynamicWorkerManager.RemoveAsync("DemoHeartbeatWorker");
Console.WriteLine($"DemoHeartbeatWorker removed: {removed}");
// Re-add the worker to keep it running for demo purposes
await dynamicWorkerManager.AddAsync(
"DemoHeartbeatWorker",
new DynamicBackgroundWorkerSchedule
{
Period = 10000 //10 seconds
},
async (workerContext, cancellationToken) =>
{
Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed.");
await Task.CompletedTask;
}
);
}
}
}

2
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs → modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations
{
[DbContext(typeof(DemoAppDbContext))]
[Migration("20260119064307_Initial")]
[Migration("20260320082618_Initial")]
partial class Initial
{
/// <inheritdoc />

0
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs → modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs

Loading…
Cancel
Save