15

Is there a way I could run a job only once using Quartz in Java? I understand it does not make sense to use Quartz in this case. But, the thing is, I have multiple jobs and they are run multiple times. So, I am using Quartz.

Is this even possible?

Kirby
  • 15,127
  • 10
  • 89
  • 104
Kaddy
  • 1,226
  • 7
  • 21
  • 34

7 Answers7

20

You should use SimpleTrigger that fires at specific time and without repeating. TriggerUtils has many handy methods for creating these kind of things.

Marko Lahma
  • 6,586
  • 25
  • 29
  • 3
    all the handy methods are gone in version 2.0. Any idea where they are now? – Jens Schauder Apr 11 '11 at 12:42
  • 1
    @JensSchauder looks like these all moved into the various children of ScheduleBuilder for example CronScheduleBuilder. The way set things up is a bit confusing. As you have to use the TiggerBuilder to build a trigger, and pass it a schedule with withSchedule method, where the schedule is itself a trigger. – ams Jul 21 '13 at 06:46
  • 1
    Won't jobs using a SimpleTrigger run a single time, each time the application is started? It will not continuously poll, which is good, however, it will be run every time the app is run resulting in additional work to ensure that non idempotent actions are not run again. – Andrew Cotton Feb 21 '19 at 20:57
  • You can trigger job just once, like scheduler.TriggerJob. If you configure such job to be run on application start, of course it will run. It all depends on configuration. – Marko Lahma Feb 26 '19 at 07:03
10

Yes, it's possible!

JobKey jobKey = new JobKey("testJob");
JobDetail job = newJob(TestJob.class)
            .withIdentity(jobKey)
            .storeDurably()
            .build();
scheduler.addJob(job, true);
scheduler.triggerJob(jobKey); //trigger a job inmediately
Rodrigo Villalba Zayas
  • 5,326
  • 2
  • 23
  • 36
6

In quartz > 2.0, you can get the scheduler to unschedule any job after work is done:

@Override
protected void execute(JobExecutionContext context)
            throws JobExecutionException {
    ...
    // process execution
    ...
    context.getScheduler().unscheduleJob(triggerKey);
    ...
}

where triggerKey is the ID of the job to run only once. After this, the job wouldn't be called anymore.

jelies
  • 9,110
  • 5
  • 50
  • 65
5

Here is an example of how to run a TestJob class immediately with Quartz 2.x:

public JobKey runJob(String jobName)
{
    // if you don't call startAt() then the current time (immediately) is assumed.
    Trigger runOnceTrigger = TriggerBuilder.newTrigger().build();
    JobKey jobKey = new JobKey(jobName);
    JobDetail job = JobBuilder.newJob(TestJob.class).withIdentity(jobKey).build();
    scheduler.scheduleJob(job, runOnceTrigger);
    return jobKey;
}

see also Quartz Enterprise Job Scheduler TutorialsSimpleTriggers

splash
  • 13,037
  • 1
  • 44
  • 67
3

I'm not sure how much similar is Quartz in Mono and Java but this seems working in .Net

TriggerBuilder.Create ()
        .StartNow ()
        .Build (); 
Puchacz
  • 1,987
  • 2
  • 24
  • 38
3

I had to ask myself if it made sense to try to configure a job and add checks if it had been run already as suggested in Marko Lahma's answer (since scheduling a job to run once results in it being run once, every time we start the app). I found examples of CommandLineRunner apps which didn't quite work for me, mostly because we already had an ApplicationRunner which was used for other jobs which use Quartz scheduling / cron. I wasn't happy with having Quartz initialize this job using a SimpleTrigger, so I had to find something else.

Using some ideas from the following articles:

I was able to piece together a working implementation which allows me to do the following:

  • run existing jobs via Quartz, on a timer
  • run new job, one time programmatically (single use Quartz job using the SimpleTrigger didn't satisfy my requirements, since it would be run once on every application load)

I came up with the following CommandLineRunner class:

public class BatchCommandLineRunner implements CommandLineRunner {

@Autowired
private Scheduler scheduler;

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

public void run(final String... args) throws SchedulerException {

    LOGGER.info("BatchCommandLineRunner: running with args -> " + Arrays.toString(args));

    for (final String jobName : args) {

        final JobKey jobKey = findJobKey(jobName);
        if (jobKey != null) {

            LOGGER.info("Triggering job for: " + jobName);
            scheduler.triggerJob(jobKey);

        } else {

            LOGGER.info("No job found for jobName: " + jobName);
        }

    }
}

private JobKey findJobKey(final String jobNameToFind) throws SchedulerException {

    for (final JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals("DEFAULT"))) {

        final String jobName = jobKey.getName();

        if (jobName.equals(jobNameToFind)) {

            return jobKey;
        }
    }
    return null;
}
}

In one of my configuration classes I added a CommandLineRunner bean which calls the custom CommandLineRunner I created:

@Configuration
public class BatchConfiguration {

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

    @Bean
    public BatchCommandLineRunner batchCommandLineRunner() {

        return new BatchCommandLineRunner();
    }

    @Bean
    public CommandLineRunner runCommandLineArgs(final ApplicationArguments applicationArguments) throws Exception {

        final List<String> jobNames = applicationArguments.getOptionValues("jobName");

        LOGGER.info("runCommandLineArgs: running the following jobs -> " + ArrayUtils.toString(jobNames));

        batchCommandLineRunner().run(jobNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY));

        return null;
    }
}

Later, I am able to initiate these jobs via the CLI without affecting my current Quartz scheduled jobs, and as long as no one runs the command via CLI multiple times, it will never be run again. I have to do some juggling of types since I accept ApplicationArguments, and then convert them into String[].

Finally, I am able to call it like this:

java -jar <your_application>.jar --jobName=<QuartzRegisteredJobDetailFactoryBean>

The result is that the job is initialized only when I call it, and it is excluded from my CronTriggerFactoryBean triggers which I used for my other jobs.

There are several assumptions being made here, so I'll try to summarize:

  • the job must be registered as a JobDetailFactoryBean (e.g.: scheduler.setJobDetails(...))
  • everything is essentially the same as a job with CronTriggerFactoryBean, excepting the lacking scheduler.setTriggers(...) call
  • Spring knows to execute the CommandLineRunner classes after the application has booted
  • I hardcoded the parameter being passed into the application to "jobName"
  • I assumed a group name of "DEFAULT" for all jobs; if you want to use differing groups this would need to be adjusted when fetching JobKey, which is used to actually run the job
  • there is nothing which prevents this job from being run multiple times via CLI, but it was triggered on every application load using SimpleTrigger approach, so this is better for me; if this is not acceptable, perhaps using StepListener and ExitStatus, etc. can prevent it from being executed twice
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Andrew Cotton
  • 415
  • 3
  • 12
1

Another solution: There is a method .withRepeatCount(0) in SimpleSchedulerBuilder:

public final int TEN_SECONDS = 10;
Trigger trigger = newTrigger()
    .withIdentity("myJob", "myJobGroup")
    .startAt(new Date(System.currentMillis()+TEN_SECONDS*1000)
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
      .withRepeatCount(0)
      .withIntervalInMinutes(1))
    .build();
Robert
  • 1,579
  • 1
  • 21
  • 36