diff --git a/src/modules/scheduling/Elsa.Scheduling.Quartz/Extensions/JobExecutionExtensions.cs b/src/modules/scheduling/Elsa.Scheduling.Quartz/Extensions/JobExecutionExtensions.cs index c0b3f099..4f9aa497 100644 --- a/src/modules/scheduling/Elsa.Scheduling.Quartz/Extensions/JobExecutionExtensions.cs +++ b/src/modules/scheduling/Elsa.Scheduling.Quartz/Extensions/JobExecutionExtensions.cs @@ -1,4 +1,5 @@ using Elsa.Common.Multitenancy; +using Elsa.Scheduling.Quartz.Jobs; using Quartz; namespace Elsa.Scheduling.Quartz; @@ -18,4 +19,27 @@ internal static class JobExecutionExtensions return await tenantFinder.FindByIdAsync(tenantId, context.CancellationToken); } + + /// + /// Executes delete job if allowed + /// + /// The Quartz job execution context. + /// The Quartz job key. + /// The cancellation token. + public static async Task DeleteJob(this IJobExecutionContext context, JobKey jobKey, CancellationToken cancellationToken = default) + { + if (IsJobAllowedToBeDeleted(jobKey.Name)) + await context.Scheduler.DeleteJob(jobKey, cancellationToken); + } + + /// + /// Checks if the job is allowed to be deleted by name + /// + /// Name of the job to check + /// False if the job is one of the required ones, otherwise true + private static bool IsJobAllowedToBeDeleted(string jobName) + { + return jobName != nameof(ResumeWorkflowJob) + && jobName != nameof(RunWorkflowJob); + } } \ No newline at end of file diff --git a/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/ResumeWorkflowJob.cs b/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/ResumeWorkflowJob.cs index 139fabfd..afb93bd2 100644 --- a/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/ResumeWorkflowJob.cs @@ -57,7 +57,7 @@ public async Task Execute(IJobExecutionContext context) catch (Exception e) { logger.LogError(e, "An error occurred while resuming workflow instance {WorkflowInstanceId}", workflowInstanceId); - await context.Scheduler.DeleteJob(context.JobDetail.Key, cancellationToken); + await context.DeleteJob(context.JobDetail.Key, cancellationToken); } } } diff --git a/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/RunWorkflowJob.cs b/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/RunWorkflowJob.cs index 3db56cf2..60ab3056 100644 --- a/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/RunWorkflowJob.cs +++ b/src/modules/scheduling/Elsa.Scheduling.Quartz/Jobs/RunWorkflowJob.cs @@ -58,7 +58,7 @@ public async Task Execute(IJobExecutionContext context) catch (WorkflowGraphNotFoundException e) { logger.LogWarning(e, "Could not find workflow graph for workflow definition handle {WorkflowDefinitionHandle}", startRequest.WorkflowDefinitionHandle); - await context.Scheduler.DeleteJob(context.JobDetail.Key, cancellationToken); + await context.Scheduler.UnscheduleJob(context.Trigger.Key, cancellationToken); } catch (Exception e) when (transientExceptionDetector.IsTransient(e)) { @@ -68,7 +68,7 @@ public async Task Execute(IJobExecutionContext context) catch (Exception e) { logger.LogError(e, "An error occurred while starting workflow {WorkflowDefinitionHandle} with correlation ID {CorrelationId}", startRequest.WorkflowDefinitionHandle, startRequest.CorrelationId); - await context.Scheduler.DeleteJob(context.JobDetail.Key, cancellationToken); + await context.DeleteJob(context.JobDetail.Key, cancellationToken); } } } diff --git a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Helpers/QuartzJobTestHelper.cs b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Helpers/QuartzJobTestHelper.cs index 346d42be..7828315a 100644 --- a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Helpers/QuartzJobTestHelper.cs +++ b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Helpers/QuartzJobTestHelper.cs @@ -20,10 +20,11 @@ public static class QuartzJobTestHelper /// Creates a mock job execution context with the specified job data. /// public static (IJobExecutionContext Context, Mock Scheduler) CreateJobExecutionContext( - IDictionary jobData) + IDictionary jobData, + string? jobKeyName = null) { var jobDataMap = new JobDataMap(jobData); - var jobKey = new JobKey("test-job"); + var jobKey = new JobKey(jobKeyName ?? "test-job"); var triggerKey = new TriggerKey("test-trigger"); var jobDetail = new Mock(); @@ -130,6 +131,18 @@ public void VerifyRescheduled() => /// public void VerifyDeleted() => scheduler.Verify(s => s.DeleteJob(It.IsAny(), It.IsAny()), Times.Once); + + /// + /// Verifies that the scheduler did not delete a job. + /// + public void VerifyNotDeleted() => + scheduler.Verify(s => s.DeleteJob(It.IsAny(), It.IsAny()), Times.Never); + + /// + /// Verifies that the scheduler unscheduled a job exactly once. + /// + public void VerifyUnscheduled() => + scheduler.Verify(s => s.UnscheduleJob(It.IsAny(), It.IsAny()), Times.Once); } /// diff --git a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/ResumeWorkflowJobTests.cs b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/ResumeWorkflowJobTests.cs index d28d93d1..490ed75e 100644 --- a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/ResumeWorkflowJobTests.cs +++ b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/ResumeWorkflowJobTests.cs @@ -65,17 +65,22 @@ public async Task Execute_TransientException_ReschedulesJob(Type exceptionType, } [Theory] - [InlineData(typeof(InvalidOperationException))] - [InlineData(typeof(ArgumentException))] - public async Task Execute_NonTransientException_DeletesJob(Type exceptionType) + [InlineData(typeof(InvalidOperationException), null)] + [InlineData(typeof(ArgumentException), null)] + [InlineData(typeof(InvalidOperationException), "ResumeWorkflowJob")] + [InlineData(typeof(ArgumentException), "ResumeWorkflowJob")] + public async Task Execute_NonTransientException_DeletesJob(Type exceptionType, string? jobKeyName) { - var (context, scheduler) = CreateJobExecutionContext(); + var (context, scheduler) = CreateJobExecutionContext(jobKeyName: jobKeyName); _transientDetector.SetupIsTransient(false); _workflowRuntime.SetupCreateClientThrows((Exception)Activator.CreateInstance(exceptionType)!); await _job.Execute(context); - scheduler.VerifyDeleted(); + if (string.IsNullOrEmpty(jobKeyName)) + scheduler.VerifyDeleted(); + else + scheduler.VerifyNotDeleted(); } [Fact] @@ -121,7 +126,8 @@ public async Task Execute_WithActivityHandle_DeserializesCorrectly() Assert.Equal("activity-123", capturedRequest.ActivityHandle?.ActivityId); } - private static (IJobExecutionContext, Mock) CreateJobExecutionContext(string? activityHandle = null) + private static (IJobExecutionContext, Mock) CreateJobExecutionContext(string? activityHandle = null, + string? jobKeyName = null) { var jobData = new Dictionary { @@ -132,6 +138,6 @@ private static (IJobExecutionContext, Mock) CreateJobExecutionC if (activityHandle != null) jobData.Add("ActivityHandle", activityHandle); - return QuartzJobTestHelper.CreateJobExecutionContext(jobData); + return QuartzJobTestHelper.CreateJobExecutionContext(jobData, jobKeyName); } } diff --git a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/RunWorkflowJobTests.cs b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/RunWorkflowJobTests.cs index 6ab6a553..9c0562b3 100644 --- a/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/RunWorkflowJobTests.cs +++ b/test/modules/scheduling/Elsa.Scheduling.Quartz.UnitTests/Jobs/RunWorkflowJobTests.cs @@ -65,7 +65,7 @@ public async Task Execute_WorkflowGraphNotFound_DeletesJob() await _job.Execute(context); - scheduler.VerifyDeleted(); + scheduler.VerifyUnscheduled(); } [Theory] @@ -83,17 +83,22 @@ public async Task Execute_TransientException_ReschedulesJob(Type exceptionType, } [Theory] - [InlineData(typeof(InvalidOperationException))] - [InlineData(typeof(ArgumentException))] - public async Task Execute_NonTransientException_DeletesJob(Type exceptionType) + [InlineData(typeof(InvalidOperationException), null)] + [InlineData(typeof(ArgumentException), null)] + [InlineData(typeof(InvalidOperationException), "RunWorkflowJob")] + [InlineData(typeof(ArgumentException), "RunWorkflowJob")] + public async Task Execute_NonTransientException_DeletesJob(Type exceptionType, string? jobKeyName) { - var (context, scheduler) = CreateJobExecutionContext(); + var (context, scheduler) = CreateJobExecutionContext(jobKeyName); _transientDetector.SetupIsTransient(false); _workflowStarter.SetupStartWorkflowThrows((Exception)Activator.CreateInstance(exceptionType)!); await _job.Execute(context); - scheduler.VerifyDeleted(); + if (string.IsNullOrEmpty(jobKeyName)) + scheduler.VerifyDeleted(); + else + scheduler.VerifyNotDeleted(); } [Fact] @@ -111,11 +116,11 @@ public async Task Execute_UsesCorrectWorkflowDefinitionHandle() Assert.Equal("workflow-def-123", capturedRequest.WorkflowDefinitionHandle.DefinitionVersionId); } - private static (IJobExecutionContext, Mock) CreateJobExecutionContext() => + private static (IJobExecutionContext, Mock) CreateJobExecutionContext(string? jobKeyName = null) => QuartzJobTestHelper.CreateJobExecutionContext(new Dictionary { { "DefinitionVersionId", "workflow-def-123" }, { "CorrelationId", "corr-123" }, { "TriggerActivityId", "trigger-123" } - }); + }, jobKeyName); }