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

JobService Configuration
  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"

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 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
spring:
  quartz:
    standbyOnlyScheduler: true

REST Interface

The REST interface allows the following:

  • Editing the triggers that define which jobs shall be executed at what time.

  • Retrieving information about:

    • available/known job types that could be scheduled

    • the properties of the configured triggers

    • when the triggers did fire last and when they will fire next

There is no way to add new job implementations via the REST interface.

Once the service is started you can access the API documentation located at: http://localhost:50001/

Providing job implementations

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.

Example for an Job implementation
public class SampleJob implements Job {

    // just an arbitrary object to test if Autowired dependency injection works for Job's
    @Autowired (1)
    private Tenants tenants;

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

        System.out.println( "SampleJob - Tenant List:" );
        tenants.forEach( (tenant) ->{
            System.out.println( tenant );
        });
    }
}
1 Dependency injection will work properly when Job class is instantiated using JobDetail-Beans
2 Code your logic into the execute method

The Java classes that implement the interface org.quartz.Job are instantiated using a constructor without parameters. A new java object is created for each execution.

These job implementations can not be referenced directly from job triggers, but need to be "registered" with the scheduler. These registrations result in JobDetail-objects/JobDetailFactoryBean-beans that have names and can be referenced from job triggers by that name. To make use of spring based dependency injection in Job classes the JobDetail-objects ultimately required by the QUARTZ implementation must not be instantiated manually. One should make use of JobDetailFactoryBean-beans to let spring create the JobDetail-objects. The registration of JobDetailFactoryBean-beans is done via spring-boot auto-configuration. The name of the JobDetail-objects created via JobDetailFactoryBean-beans is the name of the bean! So you can reference the JobDetail by the name of the bean (and the group- name which defaults to DEFAULT if not specified) when creating triggers for the JobDetail. The job implementation classes will usually be bundled in a jar file containing a META-INF/spring.factories manifest file that specifies a configuration Bean, which defines JobDetailFactoryBean-Beans that define the jobs that can be executed. These JobDetailFactoryBean-beans do reference the Java classes that implement the interface org.quartz.Job and do specify additional properties/configuration parameters.

META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=de.eitco.commons.job.service.test.testjobs.TestJobRegistrationConfiguration
Example for a JobDetail-Bean
    @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 The Job implementation class to use
2 Usually set to true, such that the job detail is not deleted, when it is no longer referenced by any trigger.

Database

Database table creation

The data base tables are created by the JobService at startup via the liquibase script: job-service\core\src\main\resources\liquibase-changeLog.xml The script defines a custom table prefix for all the QUARTZ tables to prevent table-collisions if some other library or service uses the same database schema. Otherwise the change log simply includes the standard liquibase script provided by the QUARTZ implementation: /org/quartz/impl/jdbcjobstore/liquibase.quartz.init.xml

The QUARTZ implementation can take care of creating the database tables itself, but that solution caused problems in cluster mode.

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