Using Quartz to manage Concurrency

Home / Using Quartz to manage Concurrency

A while back, I blogged about using Quartz for scheduling jobs. Recently, I needed to make an API that could trigger jobs, and I thought of Quartz. There is a need/desire in this use case to prevent concurrent execution of jobs when the API is accessed and triggers jobs, and I didn’t really want to stack/queue jobs either. Quartz has some nice mechanisms to achieve exactly what I wanted.


Diving right in, to prevent concurrent execution of a job, we have to add the [DisallowConcurrentExecution] attribute to our job.

In addition to this, since the API (and the jobs) will be running in a clustered environment, our Quartz setup has to indicate this. The relevant properties are shown below.

var properties = new NameValueCollection
{
    ["quartz.scheduler.instanceId"] = "AUTO", // must be unique, so let Quartz assign
    ["quartz.jobStore.clustered"] = "true", // allow for clustering
};

Since the jobs will all be manually triggered from the API, the job itself must be created “durably.” Without telling Quartz to create a job durably, a job with no scheduled triggers won’t be persisted. See my other post for more details on the setup of an IScheduler, but the basic job creation would look like this:

// Store durably
var job = JobBuilder.Create(typeof(MyJob))
    .WithIdentity(new JobKey($"{applicationName}_MyJob")
    .StoreDurably(true)
    .Build();

_scheduler.AddJob(job, true).Wait();

After creating the job, I made a simple Controller endpoint to trigger the job. The API endpoint will use Quartz facilities to determine if the requested job is currently running and/or if it needs to trigger it.

[HttpGet]
[HttpPost]
public IActionResult Post()
{
    var jobKey = new JobKey($"{applicationName}_MyJob");
    var message = string.Empty;
    var isTriggered = _scheduleSvc.TriggerJob(jobKey);

    if (isTriggered)
    {
        status.Message = $"Job {jobKey} was triggered.";
    }
    else
    {
        status.Message = $"Job {jobKey} was not triggered because it does not exist or is already running.";
    }

    _logger.LogDebug(status.Message);


    return new JsonResult({ Status = message});
}

The IScheduleService really does all of the work. We can use the Quartz functionality to check if a job exists, if a job is running, and to trigger the job. For my purposes, I only trigger the job if it exists (yes!) and if it’s not running. I do this, as mentioned previously, since I don’t want a huge queue/backlog of jobs to potentially be created. One and done at any given time! The IScheduler is passed in via .NET Core DI (see other post on how to set that up).

public interface IScheduleService
{
    bool CheckExists(JobKey jobKey);
    bool IsJobRunning(JobKey jobKey);
    bool TriggerJob(JobKey jobKey, bool ifNotRunning = true);
}

public class ScheduleService : IScheduleService
{
    private IScheduler _scheduler;
    private AppSettings _appSettings;

    public ScheduleService(IScheduler scheduler, IOptions<AppSettings> appSettings)
    {
        _scheduler = scheduler;
        _appSettings = appSettings.Value;
    }

    public bool CheckExists(JobKey jobKey)
    {
        return _scheduler.CheckExists(jobKey).Result;
    }

Checking for the job’s trigger states is interesting. We can retrieve all triggers for a specific job and simply check the state of each trigger. I make the assumption that if the trigger has a state, it’s running.

    public bool IsJobRunning(JobKey jobKey)
    {
        // Check if job is running
        var triggers = _scheduler.GetTriggersOfJob(jobKey).Result;
        var isRunning = false;
        foreach (var trigger in triggers)
        {
            var triggerState = _scheduler.GetTriggerState(trigger.Key).Result;
            if (triggerState == TriggerState.Paused || triggerState == TriggerState.Blocked || triggerState == TriggerState.Complete || triggerState == TriggerState.Error)
            {
                isRunning = true;
            }
        }

        return isRunning;
    }

Finally, if the job is exists, it’s not running (unless overridden with ifNotRunning), then the job is triggered.

    public bool TriggerJob(JobKey jobKey, bool ifNotRunning = true)
    {
        if (CheckExists(jobKey))
        {
            if (ifNotRunning == false || !IsJobRunning(jobKey))
            {
                _scheduler.TriggerJob(jobKey);
                return true;
            }
        }

        return false;
    }
}

With all of this configured, the process/user who triggers the API will receive a message indicating precisely what action was performed. The goal of preventing concurrent execution and scheduling of jobs is achieved.

Leave a Reply