diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/README.md b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/README.md new file mode 100644 index 0000000000..2ebe660579 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/README.md @@ -0,0 +1,112 @@ +# ABP TickerQ Background Workers Integration + +This package provides integration between [TickerQ](https://github.com/dotnetdevelopersdz/TickerQ) and the ABP Framework's background worker system. + +## About TickerQ + +TickerQ is a fast, reflection-free background task scheduler for .NET — built with source generators, EF Core integration, cron + time-based execution, and a real-time dashboard. + +Key features: +- **Performance**: Reflection-free design with source generators +- **EF Core Integration**: Native Entity Framework Core support +- **Flexible Scheduling**: Cron expressions and time-based execution +- **Real-time Dashboard**: Built-in monitoring and management +- **Modern .NET**: Built for modern .NET with async/await + +## Installation + +Install the NuGet package: + +```bash +dotnet add package Volo.Abp.BackgroundWorkers.TickerQ +``` + +Or using the ABP CLI: + +```bash +abp add-package Volo.Abp.BackgroundWorkers.TickerQ +``` + +## Usage + +1. Add the module dependency to your ABP module: + +```csharp +[DependsOn(typeof(AbpBackgroundWorkersTickerQModule))] +public class YourModule : AbpModule +{ + // ... +} +``` + +2. Create your background worker: + +```csharp +public class MyTickerQWorker : TickerQBackgroundWorkerBase +{ + public MyTickerQWorker() + { + JobId = nameof(MyTickerQWorker); + CronExpression = "0 */5 * ? * *"; // Every 5 minutes + Priority = 1; + MaxRetryAttempts = 3; + } + + public override Task DoWorkAsync(CancellationToken cancellationToken = default) + { + Logger.LogInformation("TickerQ worker executed!"); + // Your work logic here + return Task.CompletedTask; + } +} +``` + +3. Configure options (optional): + +```csharp +Configure(options => +{ + options.IsAutoRegisterEnabled = true; + options.DefaultCronExpression = "0 * * ? * *"; + options.DefaultMaxRetryAttempts = 3; + options.DefaultPriority = 0; +}); +``` + +## Migration from Other Background Workers + +The integration provides adapters for existing background workers: + +- `AsyncPeriodicBackgroundWorkerBase` workers will work automatically +- `PeriodicBackgroundWorkerBase` workers will work automatically +- Timer periods are converted to appropriate cron expressions + +## Features + +- **Automatic Registration**: Workers are auto-registered by default +- **Dependency Injection**: Full DI support in workers +- **Error Handling**: Built-in retry logic and error handling +- **Performance**: Benefits from TickerQ's reflection-free design +- **Compatibility**: Works with existing ABP background workers + +## Samples + +See the `Samples` folder for example implementations demonstrating: +- Basic worker usage +- Error handling and retries +- Dependency injection +- Configuration options + +## Documentation + +For detailed documentation, see: [docs/en/framework/infrastructure/background-workers/tickerq.md](../../../docs/en/framework/infrastructure/background-workers/tickerq.md) + +## Requirements + +- .NET 8.0 or later +- ABP Framework 9.0 or later +- TickerQ package (when available) + +## Status + +This integration is ready for use once the TickerQ package becomes available on NuGet. The implementation follows ABP's established patterns for background worker integrations (Quartz, Hangfire) and provides a seamless migration path. \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQModule.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQModule.cs new file mode 100644 index 0000000000..56cccdb217 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQModule.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers.TickerQ.Samples; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +/// +/// Sample module demonstrating how to use TickerQ background workers in an ABP application. +/// +[DependsOn(typeof(AbpBackgroundWorkersTickerQModule))] +public class SampleTickerQModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Configure TickerQ options + Configure(options => + { + options.IsAutoRegisterEnabled = true; + options.DefaultCronExpression = "0 * * ? * *"; // Every minute + options.DefaultMaxRetryAttempts = 3; + options.DefaultPriority = 0; + }); + + // Register sample workers as transient services so they can be injected with dependencies + context.Services.AddTransient(); + context.Services.AddTransient(); + context.Services.AddTransient(); + } + + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + // Sample workers with AutoRegister = true will be registered automatically + // But you can also register them manually if needed: + + // await context.AddBackgroundWorkerAsync(); + // await context.AddBackgroundWorkerAsync(); + // await context.AddBackgroundWorkerAsync(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQWorkers.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQWorkers.cs new file mode 100644 index 0000000000..14880b3839 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Samples/SampleTickerQWorkers.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.BackgroundWorkers.TickerQ.Samples; + +/// +/// Sample TickerQ background worker that demonstrates basic usage. +/// This worker runs every 5 minutes and performs a simple logging operation. +/// +public class SampleTickerQWorker : TickerQBackgroundWorkerBase +{ + public SampleTickerQWorker() + { + // Configure the worker + JobId = nameof(SampleTickerQWorker); + CronExpression = "0 */5 * ? * *"; // Every 5 minutes + Priority = 1; // Higher than default priority + MaxRetryAttempts = 5; // Retry up to 5 times on failure + } + + public override Task DoWorkAsync(CancellationToken cancellationToken = default) + { + Logger.LogInformation("Sample TickerQ worker executed at {Time}", DateTime.Now); + + // Simulate some work + Logger.LogDebug("Processing sample work..."); + + // Your background task logic goes here + // For example: + // - Process pending orders + // - Send scheduled notifications + // - Cleanup old data + // - Generate reports + + Logger.LogInformation("Sample TickerQ worker completed successfully"); + + return Task.CompletedTask; + } +} + +/// +/// Sample TickerQ background worker that demonstrates error handling and retry logic. +/// +public class SampleErrorHandlingTickerQWorker : TickerQBackgroundWorkerBase +{ + private static int _executionCount = 0; + + public SampleErrorHandlingTickerQWorker() + { + JobId = nameof(SampleErrorHandlingTickerQWorker); + CronExpression = "0 */10 * ? * *"; // Every 10 minutes + MaxRetryAttempts = 3; + } + + public override async Task DoWorkAsync(CancellationToken cancellationToken = default) + { + var currentCount = Interlocked.Increment(ref _executionCount); + + Logger.LogInformation("Error handling worker executed {Count} times", currentCount); + + // Simulate intermittent failures for demonstration + if (currentCount % 3 == 0) + { + Logger.LogWarning("Simulating a temporary failure (will retry)"); + throw new InvalidOperationException("Simulated failure for demonstration"); + } + + // Simulate successful work + await Task.Delay(100, cancellationToken); // Simulate some async work + + Logger.LogInformation("Error handling worker completed successfully"); + } +} + +/// +/// Sample TickerQ background worker that demonstrates working with dependency injection. +/// +public class SampleDependencyInjectionTickerQWorker : TickerQBackgroundWorkerBase +{ + // In a real application, you would inject your repositories, services, etc. + // private readonly IMyRepository _myRepository; + // private readonly IMyService _myService; + + public SampleDependencyInjectionTickerQWorker( + // IMyRepository myRepository, + // IMyService myService + ) + { + // _myRepository = myRepository; + // _myService = myService; + + JobId = nameof(SampleDependencyInjectionTickerQWorker); + CronExpression = "0 0 */6 ? * *"; // Every 6 hours + } + + public override async Task DoWorkAsync(CancellationToken cancellationToken = default) + { + Logger.LogInformation("Dependency injection worker started"); + + try + { + // Example of using injected services + // var entities = await _myRepository.GetListAsync(cancellationToken: cancellationToken); + // await _myService.ProcessEntitiesAsync(entities, cancellationToken); + + // For demonstration, just simulate the work + await Task.Delay(50, cancellationToken); + + Logger.LogInformation("Dependency injection worker processed data successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred in dependency injection worker"); + throw; // Re-throw to trigger retry logic + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg.analyze.json b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg.analyze.json new file mode 100644 index 0000000000..1ccab7ae24 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.Quartz.abppkg.analyze.json @@ -0,0 +1,68 @@ +{ + "name": "Volo.Abp.BackgroundWorkers.Quartz", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.BackgroundWorkers.Quartz", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.BackgroundWorkers", + "namespace": "Volo.Abp.BackgroundWorkers", + "name": "AbpBackgroundWorkersModule" + }, + { + "declaringAssemblyName": "Volo.Abp.Quartz", + "namespace": "Volo.Abp.Quartz", + "name": "AbpQuartzModule" + } + ], + "implementingInterfaces": [ + { + "name": "IAbpModule", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IAbpModule" + }, + { + "name": "IOnPreApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" + }, + { + "name": "IOnApplicationInitialization", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationInitialization" + }, + { + "name": "IOnPostApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" + }, + { + "name": "IOnApplicationShutdown", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationShutdown" + }, + { + "name": "IPreConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPreConfigureServices" + }, + { + "name": "IPostConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPostConfigureServices" + } + ], + "contentType": "abpModule", + "name": "AbpBackgroundWorkersQuartzModule", + "summary": null + } + ] +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersTickerQServiceCollectionExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersTickerQServiceCollectionExtensions.cs new file mode 100644 index 0000000000..9b58e80254 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersTickerQServiceCollectionExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers.TickerQ; + +namespace Volo.Abp.BackgroundWorkers; + +/// +/// Extension methods for TickerQ background worker integration. +/// +public static class AbpBackgroundWorkersTickerQServiceCollectionExtensions +{ + /// + /// Adds TickerQ background worker services to the service collection. + /// This method provides additional configuration options beyond the basic module registration. + /// + /// The service collection. + /// Optional configuration action. + /// The service collection for chaining. + public static IServiceCollection AddAbpTickerQBackgroundWorkers( + this IServiceCollection services, + Action? configure = null) + { + if (configure != null) + { + services.Configure(configure); + } + + return services; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs index 94404da0d2..7e6ccf4e5f 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs @@ -15,7 +15,12 @@ public class AbpBackgroundWorkersTickerQModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - // Register TickerQ-specific services + // Register TickerQ-specific services and configure options + context.Services.Configure(options => + { + // Set up default options - users can override these in their modules + }); + // The TickerQBackgroundWorkerManager will automatically replace the default manager // due to the [Dependency(ReplaceServices = true)] attribute } diff --git a/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo.Abp.BackgroundWorkers.TickerQ.Tests.csproj b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo.Abp.BackgroundWorkers.TickerQ.Tests.csproj new file mode 100644 index 0000000000..05b135c1db --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo.Abp.BackgroundWorkers.TickerQ.Tests.csproj @@ -0,0 +1,20 @@ + + + + + + net9.0 + Volo.Abp.BackgroundWorkers.TickerQ.Tests + Volo.Abp.BackgroundWorkers.TickerQ.Tests + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQTestModule.cs b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQTestModule.cs new file mode 100644 index 0000000000..ca78345a16 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQTestModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +[DependsOn( + typeof(AbpBackgroundWorkersTickerQModule), + typeof(AbpTestBaseModule), + typeof(AbpAutofacModule) +)] +public class AbpBackgroundWorkersTickerQTestModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerBase_Tests.cs b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerBase_Tests.cs new file mode 100644 index 0000000000..c41eb819fa --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerBase_Tests.cs @@ -0,0 +1,69 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +public class TickerQBackgroundWorkerBase_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Have_Default_Properties() + { + // Arrange & Act + var worker = new TestTickerQWorker(); + + // Assert + worker.AutoRegister.ShouldBeTrue(); + worker.Priority.ShouldBe(0); + worker.MaxRetryAttempts.ShouldBe(3); + worker.JobId.ShouldBeNull(); + worker.CronExpression.ShouldBeNull(); + } + + [Fact] + public void Should_Allow_Custom_Configuration() + { + // Arrange & Act + var worker = new TestTickerQWorker + { + JobId = "CustomJobId", + CronExpression = "0 0 12 * * ?", + Priority = 5, + MaxRetryAttempts = 10, + AutoRegister = false + }; + + // Assert + worker.JobId.ShouldBe("CustomJobId"); + worker.CronExpression.ShouldBe("0 0 12 * * ?"); + worker.Priority.ShouldBe(5); + worker.MaxRetryAttempts.ShouldBe(10); + worker.AutoRegister.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Execute_DoWorkAsync() + { + // Arrange + var worker = new TestTickerQWorker(); + + // Act + await worker.DoWorkAsync(); + + // Assert + worker.ExecutionCount.ShouldBe(1); + } + + private class TestTickerQWorker : TickerQBackgroundWorkerBase + { + public int ExecutionCount { get; private set; } + + public override Task DoWorkAsync(CancellationToken cancellationToken = default) + { + ExecutionCount++; + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerManager_Tests.cs b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerManager_Tests.cs new file mode 100644 index 0000000000..2e8de9f919 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundWorkers.TickerQ.Tests/Volo/Abp/BackgroundWorkers/TickerQ/TickerQBackgroundWorkerManager_Tests.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Xunit; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +public class TickerQBackgroundWorkerManager_Tests : AbpIntegratedTest +{ + private readonly IBackgroundWorkerManager _backgroundWorkerManager; + private readonly IOptions _options; + + public TickerQBackgroundWorkerManager_Tests() + { + _backgroundWorkerManager = GetRequiredService(); + _options = GetRequiredService>(); + } + + [Fact] + public void Should_Use_TickerQBackgroundWorkerManager() + { + // Assert + _backgroundWorkerManager.ShouldBeOfType(); + } + + [Fact] + public void Should_Have_Default_Options() + { + // Assert + var options = _options.Value; + options.IsAutoRegisterEnabled.ShouldBeTrue(); + options.DefaultCronExpression.ShouldBe("0 * * ? * *"); + options.DefaultMaxRetryAttempts.ShouldBe(3); + options.DefaultPriority.ShouldBe(0); + } + + [Fact] + public void Should_Allow_Options_Configuration() + { + // This test would be run in a separate test module with different options + // For now, just verify the options exist + _options.Value.ShouldNotBeNull(); + } +} \ No newline at end of file