From 9725be0e0fa302e41723ffde93d8925103ef3423 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 11 Dec 2024 14:35:19 +0100 Subject: [PATCH 1/2] Fix HTML encoding for ampersand in XML documentation Replaced '&' with '&' in the XML documentation comment to ensure proper HTML encoding. This change improves compatibility and prevents potential rendering issues in documentation tools. --- src/modules/Elsa.Identity/Features/IdentityFeature.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Identity/Features/IdentityFeature.cs b/src/modules/Elsa.Identity/Features/IdentityFeature.cs index e5ab7a0568..20f5a6de59 100644 --- a/src/modules/Elsa.Identity/Features/IdentityFeature.cs +++ b/src/modules/Elsa.Identity/Features/IdentityFeature.cs @@ -17,7 +17,7 @@ namespace Elsa.Identity.Features; /// -/// Provides identity feature to authenticate & authorize API requests. +/// Provides identity feature to authenticate & authorize API requests. /// [DependsOn(typeof(SystemClockFeature))] [PublicAPI] From 96bf791668b948e25e4c24c1a80649ee31f9fb8e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 11 Dec 2024 14:35:49 +0100 Subject: [PATCH 2/2] Introduce JobKeyProvider for managing Quartz job keys Added JobKeyProvider to centralize job key and group name handling, simplifying Quartz job scheduling. Refactored QuartzWorkflowScheduler to use the new provider, improving maintainability. Updated QuartzSchedulerFeature to register the new provider and a startup task for job registration. --- .../Elsa.Quartz/Contracts/IJobKeyProvider.cs | 9 +++ .../Features/QuartzSchedulerFeature.cs | 15 +++-- .../Elsa.Quartz/Services/JobKeyProvider.cs | 19 ++++++ .../Services/QuartzWorkflowScheduler.cs | 60 ++++++++----------- .../Elsa.Quartz/Tasks/RegisterJobsTask.cs | 35 +++++++++++ 5 files changed, 99 insertions(+), 39 deletions(-) create mode 100644 src/modules/Elsa.Quartz/Contracts/IJobKeyProvider.cs create mode 100644 src/modules/Elsa.Quartz/Services/JobKeyProvider.cs create mode 100644 src/modules/Elsa.Quartz/Tasks/RegisterJobsTask.cs diff --git a/src/modules/Elsa.Quartz/Contracts/IJobKeyProvider.cs b/src/modules/Elsa.Quartz/Contracts/IJobKeyProvider.cs new file mode 100644 index 0000000000..4ca4d9e57e --- /dev/null +++ b/src/modules/Elsa.Quartz/Contracts/IJobKeyProvider.cs @@ -0,0 +1,9 @@ +using Quartz; + +namespace Elsa.Quartz.Contracts; + +internal interface IJobKeyProvider +{ + JobKey GetJobKey() where TJob : IJob; + string GetGroupName(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Quartz/Features/QuartzSchedulerFeature.cs b/src/modules/Elsa.Quartz/Features/QuartzSchedulerFeature.cs index 6600dfca0e..da395d3902 100644 --- a/src/modules/Elsa.Quartz/Features/QuartzSchedulerFeature.cs +++ b/src/modules/Elsa.Quartz/Features/QuartzSchedulerFeature.cs @@ -1,9 +1,11 @@ +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Quartz.Contracts; using Elsa.Quartz.Handlers; -using Elsa.Quartz.Jobs; using Elsa.Quartz.Services; +using Elsa.Quartz.Tasks; using Elsa.Scheduling; using Elsa.Scheduling.Features; using Elsa.Workflows; @@ -34,9 +36,12 @@ public override void Configure() /// public override void Apply() { - Services.AddSingleton(); - Services.AddSingleton(); - Services.AddSingleton(); - Services.AddQuartz(); + Services + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddScoped() + .AddStartupTask() + .AddQuartz(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Quartz/Services/JobKeyProvider.cs b/src/modules/Elsa.Quartz/Services/JobKeyProvider.cs new file mode 100644 index 0000000000..3f10a84786 --- /dev/null +++ b/src/modules/Elsa.Quartz/Services/JobKeyProvider.cs @@ -0,0 +1,19 @@ +using Elsa.Common.Multitenancy; +using Elsa.Quartz.Contracts; +using Quartz; + +namespace Elsa.Quartz.Services; + +internal class JobKeyProvider(ITenantAccessor tenantAccessor) : IJobKeyProvider +{ + public JobKey GetJobKey() where TJob : IJob + { + return new(typeof(TJob).Name, GetGroupName()); + } + + public string GetGroupName() + { + var tenantId = tenantAccessor.Tenant?.Id; + return string.IsNullOrWhiteSpace(tenantId) ? "Default" : tenantId; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Quartz/Services/QuartzWorkflowScheduler.cs b/src/modules/Elsa.Quartz/Services/QuartzWorkflowScheduler.cs index a110b9043b..ac36ccfa35 100644 --- a/src/modules/Elsa.Quartz/Services/QuartzWorkflowScheduler.cs +++ b/src/modules/Elsa.Quartz/Services/QuartzWorkflowScheduler.cs @@ -1,108 +1,103 @@ using Elsa.Common; using Elsa.Common.Multitenancy; using Elsa.Extensions; +using Elsa.Quartz.Contracts; using Elsa.Quartz.Jobs; using Elsa.Scheduling; using Quartz; +using IScheduler = Quartz.IScheduler; namespace Elsa.Quartz.Services; /// /// An implementation of that uses Quartz.NET. /// -public class QuartzWorkflowScheduler(ISchedulerFactory schedulerFactoryFactory, IJsonSerializer jsonSerializer, ITenantAccessor tenantAccessor) : IWorkflowScheduler +internal class QuartzWorkflowScheduler(ISchedulerFactory schedulerFactoryFactory, IJsonSerializer jsonSerializer, ITenantAccessor tenantAccessor, IJobKeyProvider jobKeyProvider) : IWorkflowScheduler { /// public async ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create() - .WithIdentity(GetRunWorkflowJobKey()) - .Build(); + var trigger = TriggerBuilder.Create() + .ForJob(GetRunWorkflowJobKey()) .UsingJobData(CreateJobDataMap(request)) .WithIdentity(GetTriggerKey(taskName)) .StartAt(at) .Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// public async ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create().WithIdentity(GetResumeWorkflowJobKey()).Build(); var trigger = TriggerBuilder.Create() + .ForJob(GetResumeWorkflowJobKey()) .UsingJobData(CreateJobDataMap(request)) .WithIdentity(GetTriggerKey(taskName)) .StartAt(at) .Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create().WithIdentity(GetRunWorkflowJobKey()).Build(); var trigger = TriggerBuilder.Create() .WithIdentity(GetTriggerKey(taskName)) + .ForJob(GetRunWorkflowJobKey()) .UsingJobData(CreateJobDataMap(request)) .StartAt(startAt) .WithSimpleSchedule(schedule => schedule.WithInterval(interval).RepeatForever()) .Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create().WithIdentity(GetResumeWorkflowJobKey()).Build(); var trigger = TriggerBuilder.Create() .WithIdentity(GetTriggerKey(taskName)) + .ForJob(GetResumeWorkflowJobKey()) .UsingJobData(CreateJobDataMap(request)) .StartAt(startAt) .WithSimpleSchedule(schedule => schedule.WithInterval(interval).RepeatForever()) .Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// public async ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create().WithIdentity(GetRunWorkflowJobKey()).Build(); var trigger = TriggerBuilder.Create() .UsingJobData(CreateJobDataMap(request)) + .ForJob(GetRunWorkflowJobKey()) .WithIdentity(GetTriggerKey(taskName)) .WithCronSchedule(cronExpression) .Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// public async ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); - var job = JobBuilder.Create().WithIdentity(GetResumeWorkflowJobKey()).Build(); var trigger = TriggerBuilder.Create() + .ForJob(GetResumeWorkflowJobKey()) .UsingJobData(CreateJobDataMap(request)) .WithIdentity(GetTriggerKey(taskName)) .WithCronSchedule(cronExpression).Build(); - if (!await scheduler.CheckExists(job.Key, cancellationToken)) - await scheduler.ScheduleJob(job, trigger, cancellationToken); + await ScheduleJobAsync(scheduler, trigger, cancellationToken); } /// @@ -112,6 +107,12 @@ public async ValueTask UnscheduleAsync(string taskName, CancellationToken cancel var triggerKey = GetTriggerKey(taskName); await scheduler.UnscheduleJob(triggerKey, cancellationToken); } + + private async Task ScheduleJobAsync(IScheduler scheduler, ITrigger trigger, CancellationToken cancellationToken) + { + if (!await scheduler.CheckExists(trigger.Key, cancellationToken)) + await scheduler.ScheduleJob(trigger, cancellationToken); + } private JobDataMap CreateJobDataMap(ScheduleNewWorkflowInstanceRequest request) { @@ -138,18 +139,9 @@ private JobDataMap CreateJobDataMap(ScheduleExistingWorkflowInstanceRequest requ .AddIfNotEmpty(nameof(ScheduleExistingWorkflowInstanceRequest.ActivityHandle), serializedActivityHandle) .AddIfNotEmpty(nameof(ScheduleExistingWorkflowInstanceRequest.BookmarkId), request.BookmarkId); } - - private JobKey GetRunWorkflowJobKey() => new(nameof(RunWorkflowJob), GetGroupName()); - private JobKey GetResumeWorkflowJobKey() => new(nameof(ResumeWorkflowJob), GetGroupName()); - private string GetGroupName() - { - var tenantId = tenantAccessor.Tenant?.Id; - return string.IsNullOrWhiteSpace(tenantId) ? "Default" : tenantId; - } - - private TriggerKey GetTriggerKey(string taskName) - { - return new TriggerKey(taskName, GetGroupName()); - } + private JobKey GetRunWorkflowJobKey() => jobKeyProvider.GetJobKey(); + private JobKey GetResumeWorkflowJobKey() => jobKeyProvider.GetJobKey(); + private string GetGroupName() => jobKeyProvider.GetGroupName(); + private TriggerKey GetTriggerKey(string taskName) => new(taskName, GetGroupName()); } \ No newline at end of file diff --git a/src/modules/Elsa.Quartz/Tasks/RegisterJobsTask.cs b/src/modules/Elsa.Quartz/Tasks/RegisterJobsTask.cs new file mode 100644 index 0000000000..8f41c72fb0 --- /dev/null +++ b/src/modules/Elsa.Quartz/Tasks/RegisterJobsTask.cs @@ -0,0 +1,35 @@ +using Elsa.Common; +using Elsa.Quartz.Contracts; +using Elsa.Quartz.Jobs; +using JetBrains.Annotations; +using Quartz; + +namespace Elsa.Quartz.Tasks; + +/// +/// Registers the Quartz jobs. +/// +/// +/// +[UsedImplicitly] +internal class RegisterJobsTask(ISchedulerFactory schedulerFactoryFactory, IJobKeyProvider jobKeyProvider) : IStartupTask +{ + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + var scheduler = await schedulerFactoryFactory.GetScheduler(cancellationToken); + await CreateJobAsync(scheduler, cancellationToken); + await CreateJobAsync(scheduler, cancellationToken); + } + + private async Task CreateJobAsync(IScheduler scheduler, CancellationToken cancellationToken) where TJobType : IJob + { + var key = jobKeyProvider.GetJobKey(); + var job = JobBuilder.Create() + .WithIdentity(key) + .StoreDurably() + .Build(); + + if (!await scheduler.CheckExists(job.Key, cancellationToken)) + await scheduler.AddJob(job, false, cancellationToken); + } +} \ No newline at end of file