I am using Quartz to run CRON jobs. I am pretty new with Quartz. The CRON expression is build in the UI using a calendar and some inputs. I am using the selected date in calendar to generate the start date. But, when the user selects date in past, the job doesn't run and throws this error.
based on configured schedule the given trigger will never fire
I am using the MisfireInstruction
to run missed jobs. But, this instruction works only with -1
days.
Does Quartz even support past dates? Is there a way I can do this with Quartz?
using Quartz;
namespace Data.Export.Api.Services;
public class SchedulingService : ISchedulingService
{
private readonly ILogger<SchedulingService> logger;
private readonly ISchedulerFactory schedulerFactory;
public SchedulingService(
ILogger<SchedulingService> logger,
ISchedulerFactory schedulerFactory
)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
}
public async Task ScheduleJobAsync(DataExportTask dataExportTask)
{
if (!CronExpression.IsValidExpression(dataExportTask.CronExpression))
{
throw new ArgumentException("Cron expression is not valid");
}
IScheduler scheduler = await schedulerFactory.GetScheduler();
TriggerKey triggerKey = new(dataExportTask.Id.ToString());
JobKey jobKey = new(dataExportTask.Id.ToString());
ITrigger? existingTrigger = await GetExistingTriggerAsync(scheduler, triggerKey);
IJobDetail? existingJob = await GetExistingJobAsync(scheduler, jobKey);
if (dataExportTask.IsEnabled)
{
IJobDetail job = CreateJobDetail(dataExportTask);
ITrigger trigger = CreateTrigger(dataExportTask);
if (existingTrigger != null && existingJob != null)
{
await RescheduleJobAsync(scheduler, triggerKey, trigger);
}
else
{
await ScheduleJobAsync(scheduler, job, trigger);
}
}
else
{
if (existingTrigger != null)
{
await UnscheduleJobAsync(scheduler, triggerKey);
}
if (existingJob != null)
{
await DeleteJobAsync(scheduler, jobKey);
}
}
}
private static IJobDetail CreateJobDetail(DataExportTask dataExportTask) => JobBuilder.Create<DataExportJob>()
.WithIdentity(CreateJobKey(dataExportTask))
.UsingJobData("TaskId", dataExportTask.Id.ToString())
.UsingJobData("TaskName", dataExportTask.Name)
.Build();
private static JobKey CreateJobKey(DataExportTask dataExportTask) => new(dataExportTask.Id.ToString());
private static ITrigger CreateTrigger(DataExportTask dataExportTask) =>
TriggerBuilder.Create()
.WithIdentity(CreateTriggerKey(dataExportTask))
.WithCronSchedule(dataExportTask.CronExpression, e => e.WithMisfireHandlingInstructionIgnoreMisfires())
.Build();
private static TriggerKey CreateTriggerKey(DataExportTask dataExportTask) => new(dataExportTask.Id.ToString());
private async Task DeleteJobAsync(IScheduler scheduler, JobKey jobKey)
{
try
{
await scheduler.DeleteJob(jobKey);
}
catch (Exception e)
{
logger.LogError("Exception {Message}", e.Message);
logger.LogError("StackTrace {StackTrace}", e.StackTrace);
throw new Exception("Failed to delete job", e);
}
}
private static async Task<IJobDetail?> GetExistingJobAsync(IScheduler scheduler, JobKey jobKey) => await scheduler.GetJobDetail(jobKey);
private static async Task<ITrigger?> GetExistingTriggerAsync(IScheduler scheduler, TriggerKey triggerKey) => await scheduler.GetTrigger(triggerKey);
private async Task RescheduleJobAsync(IScheduler scheduler, TriggerKey triggerKey, ITrigger trigger)
{
try
{
await scheduler.RescheduleJob(triggerKey, trigger);
}
catch (Exception e)
{
logger.LogError("Exception {Message}", e.Message);
logger.LogError("StackTrace {StackTrace}", e.StackTrace);
throw new Exception("Failed to reschedule job", e);
}
}
private async Task ScheduleJobAsync(IScheduler scheduler, IJobDetail job, ITrigger trigger)
{
try
{
await scheduler.ScheduleJob(job, trigger);
}
catch (Exception e)
{
logger.LogError("Exception {Message}", e.Message);
logger.LogError("StackTrace {StackTrace}", e.StackTrace);
throw new Exception("Failed to schedule job", e);
}
}
private async Task UnscheduleJobAsync(IScheduler scheduler, TriggerKey triggerKey)
{
try
{
await scheduler.UnscheduleJob(triggerKey);
}
catch (Exception e)
{
logger.LogError("Exception {Message}", e.Message);
logger.LogError("StackTrace {StackTrace}", e.StackTrace);
throw new Exception("Failed to unschedule job", e);
}
}
}
Does Quartz even support past dates?
I don't think it has a problem with past dates.
It will certainly accept triggers that start in the past and continue into the future (as I have done that many times). It will also accept triggers that start and stop in the past, provided that there is one valid fire time in that range (and yes, I have to admit that I have done that by accident).
But it won't accept triggers that could never have been valid in the past and can never be valid in the future.
In other words, if you have a trigger that runs every Christmas Day (0 0 12 25 12 ? *), quartz will quite happily schedule a job starting on 1st Dec 2022 and ending on 1st Jan 2023 and that it because there is one valid Christmas in that period and it doesn't matter than Jan 2023 is in the past and the trigger has now expired.
However it will not schedule a job with a trigger starting on 1st Dec 2022 and ending on 15th Dec 2022 nor one starting on 1st Dec 2023 and ending on 15th Dec 2023.
Bottom line is when users are selecting dates then you really need to validate the input.
For example, given a ITrigger trigger
, I would test that the following does not return null
((Quartz.Spi.IOperableTrigger)trigger).GetFireTimeAfter(DateTime.UtcNow);
Note this will fail for all of the examples above, even the first one which Quartz.Net will happily accept and ignore, but in my view they are all user errors.