Introduction

The Job Service is a distributed job scheduling and execution service built on top of the Quartz Scheduler framework. It provides a REST interface for managing scheduled jobs and triggers, allowing you to define when and how frequently jobs should be executed. The service is designed to operate in a clustered environment where multiple instances work together to execute jobs in a load-balanced manner. All job scheduling data and synchronization is managed through a shared database, ensuring consistency across all service instances in the cluster.

Writing jobs

The job implementations must be provided in terms of Java classes that implement the interface org.quartz.Job. All JobService instances in a cluster will compete for executing all pending jobs. Hence, the Java classes that implement the jobs must be available on all JobService instances in the cluster.

When a job implementation must not be executed concurrently, annotate the job class with @DisallowConcurrentExecution. This ensures that the job is never executed by more than one Job Service instance in the cluster at the same time.

The JAR file containing the job implementation classes must be a Spring Boot starter. It must be added to the classpath of the job service by placing it in the directory defined by the parameter loader.path used to start the service. The starter must register the required Spring beans for the jobs. The test-jobs module in the source code repository of the Job Service can be used as an example for such a starter.

The following Maven dependencies are required to implement jobs:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
    <groupId>de.eitco.commons</groupId>
    <artifactId>cmn-job-service-common</artifactId>
</dependency>

Annotation based job registration

The easiest way to register a job is to use annotations. By adding @JobDetails to your job implementation, the JobDetail will be automatically created. You can also use @CronTrigger or @SimpleTrigger to automatically create triggers for the job.

Example for an annotated Job implementation
@JobDetails(name = "AnnotatedTestJob", group = "test", description = "Test job for annotation-based job execution") (1)
@CronTrigger(name = "AnnotatedTestJobTrigger1", group = "test", cronExpression = "0/5 * * * * ?") (2)
@CronTrigger(name = "AnnotatedTestJobTrigger2", group = "test", cronExpression = "0 0 0 * * ?", jobConfigurationKey = "test-jobs.test-job-1.config") (3)
public class AnnotatedTestJob implements Job {

    private static final Logger LOGGER = Logger.getLogger(AnnotatedTestJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {

        LOGGER.info(() -> "Executing AnnotatedTestJob");
    }
}
1 The @JobDetails annotation defines the name, group, and description of the job.
2 The @CronTrigger annotation defines a cron trigger for the job.
3 A second cron trigger with job configuration data loaded from the Job Service’s configuration at the specified key.

To make the JobService aware of the annotated job, you need to register it as a Spring bean:

Example for registering an annotated Job as a bean
@Bean
public Job annotatedTestJob() {
    return new AnnotatedTestJob();
}

Alternatively, if you use classpath scanning, you can simply add @Component to your job class.

Programmatic job registration

If you need more control over the job registration or prefer to separate job implementations from their configuration, you can register jobs programmatically. This approach involves three steps:

  1. Implement the org.quartz.Job interface.

  2. Register a JobDetail as a Spring bean, preferably using JobDetailFactoryBean.

  3. Register a trigger for the job, preferably using CronTriggerFactoryBean or SimpleTriggerFactoryBean.

Implementing the Job

The job implementation is a Java class that implements the org.quartz.Job interface.

Example for a Job implementation
public class SampleJob implements Job {

    // just an arbitrary object to test if Autowired dependency injection works for Jobs
    @Autowired (1)
    private ApplicationContext context;

    public void execute(JobExecutionContext context) throws JobExecutionException { (2)

        System.out.println("SampleJob");
    }
}
1 Dependency injection will work properly when the job class is instantiated via a JobDetailFactoryBean.
2 Implement your logic within the execute method.
You can pass configuration data from the trigger to the job using the job data map which can be set in the trigger factory bean and then accessed using the JobExecutionContext.

Note that a new instance of the job class is created for each execution using its default constructor.

Registering the JobDetail

To make a job available to the scheduler, it must be registered as a JobDetail. While Quartz requires JobDetail objects, it is highly recommended to use Spring’s JobDetailFactoryBean. This ensures that Spring handles the instantiation of the job class, allowing for dependency injection (e.g., via @Autowired).

The name of the JobDetail will be the name of the Spring bean (unless explicitly specified otherwise), and the default group is DEFAULT.

Example for a JobDetail bean definition
@Bean
public JobDetailFactoryBean alienCheckJobDetail() {
    JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
    jobDetailFactory.setJobClass(SampleJob.class); (1)
    jobDetailFactory.setDescription("Invoke Alien check ...");
    jobDetailFactory.setDurability(true); (2)
    return jobDetailFactory;
}
1 Specify the job implementation class.
2 Setting durability to true ensures the job detail is not deleted when it’s no longer referenced by any trigger.

Registering the Trigger

Once the JobDetail is registered, you can define one or more triggers for it. For programmatic registration, it is recommended to use Spring’s CronTriggerFactoryBean or SimpleTriggerFactoryBean.

Example for a CronTrigger bean definition
@Bean
public CronTriggerFactoryBean alienCheckTrigger(JobDetail alienCheckJobDetail) {
    CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
    factoryBean.setJobDetail(alienCheckJobDetail); (1)
    factoryBean.setCronExpression("0 0 0 * * ?"); (2)
    factoryBean.setDescription("Trigger for Alien check job");
    return factoryBean;
}
1 Reference the JobDetail bean. Spring will automatically inject the JobDetail created by the JobDetailFactoryBean.
2 Define the schedule using a cron expression.

Clustering

The job service is designed to be used in cluster mode: that is several JobService instances care jointly about executing the scheduled jobs and strive to achieve a perfect load balancing between all JobService instances in the cluster. All JobService instances can change the triggers that define which jobs are to be triggered and when these jobs are to be triggered.

The synchronization of the JobService instances is done by using a common database instance and leverages row-locks as synchronization mechanism. Hence all JobService instances must have access to very same database tables.

The JobService is not multi-tenant capable! The JobService will currently fail to start if there are several tenants configured.

Cluster mode is enabled by default via the following default properties in the application.yaml

Default JobService Configuration
spring:
  quartz:
    job-store-type: "jdbc"
    jdbc:
      initialize-schema: "NEVER"
    properties:
      org:
        quartz:
          jobStore:
            driverDelegateClass: "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"
            isClustered: true
            tablePrefix: "ECQZ_"
          scheduler:
            instanceId: "AUTO"
            instanceName: "job-service-scheduler"
            jmx:
              export: false
          threadPool:
            threadCount: 50
            makeThreadsDaemons: true

Disable job execution on individual JobService instances

By default, all JobService instances will jointly take care of executing jobs and strive to achieve a perfect load balancing between all JobService instances in the cluster. The execution of job’s on individual JobService instances can be disabled, such that these instances will not participate in executing jobs. This can be achieved by setting the configuration option spring.quartz.standbyOnlyScheduler.

JobService Configuration
job-service:
  standbyOnlyScheduler: true

Miscellaneous

Immediately triggering a job

A job can be immediately triggered by scheduling a simple trigger with StartTime set to now. A simple trigger will be automatically deleted by the scheduler if all executions have been executed. If you want to check the progress of the job you could define the job to be repeated in the far future.

Example for immediately triggering a job
    public void simpleTriggerTest() throws InterruptedException {
        SchedulerResourceClient client = newClient();

        final JobKeyModel jobKey = new JobKeyModel();
        jobKey.setName("alienCheckJobDetail");
        //jobKey.setGroup("DEFAULT"); (1)

        TriggerKeyModel triggerKey = new TriggerKeyModel();
        triggerKey.setName("simpleTriggerTest");
        //triggerKey.setGroup("DEFAULT"); (1)

        SimpleTriggerModel model = new SimpleTriggerModel();
        model.setKey( triggerKey );
        model.setJobKey( jobKey );
        model.setStartTime( ZonedDateTime.now() );

        //model.setEndTime( ZonedDateTime.now().plusYears(11) ); (2)
        //model.setRepeatInterval(10 * 365 * 24 * 60 * 60 * 1000L ); // 10 years (2)
        //model.setRepeatCount(1); (2)

        client.scheduleSimpleTrigger(model);

        // wait some time (2)
        // Thread.sleep(60 * 1000); // 60 seconds (2)

        // fetch all the triggerDetails and see what has happened to our job... (2)
        //final List<TriggerModel> triggerDetails = client.getTriggerDetails(); (2)
        //... see (2)

        // finally delete the trigger again (2)
        //client.unscheduleJob(triggerKey); (2)
    }
1 The group names need not be specified at all for the default group
2 If you want to check the creation and progress of the job you could define the job to be repeated at least once in the far future. The commented lines suggest how this could implemented…​

Configuration Options

There are three sets of config options that can both be configured via the spring configuration files:

  • spring supported options, see: org.springframework.boot.autoconfigure.quartz.QuartzProperties. In the yaml file these properties are rooted at spring.quartz.

  • standard quartz options, that are defined by the quartz implementation and exist independently form the spring support. See: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/. In the yaml file these properties are rooted at spring.quartz.properties.

  • the additional JobService specific option standbyOnlyScheduler. In the yaml file this property is located at spring.quartz.standbyOnlyScheduler.

Logging of QUARTZ’s activities

QUARTZ has an event based extension interface that allows to get notified about activities in the local scheduler instance and there are two extensions that just log these events to a log file. This kind of logging comes in hand during development and can be enabled via these config options:

JobService Configuration
spring:
  quartz:
    properties:
      org:
        quartz:
          plugin:
            jobHistory:
              class: "org.quartz.plugins.history.LoggingJobHistoryPlugin"
            triggHistory:
              class: "org.quartz.plugins.history.LoggingTriggerHistoryPlugin"

References