How to trigger dynamic scheduling jobs in Java and cancel them?

Tags: ,



How can I invoke a job dynamatically and cancel them at ease? Can I trigger a delayed task that runs at a specific moment, and cancel them if the moment has not passed by, just behaving like the alarm clock?

Answer

Quartz is a good scheduling library that has lots of capabilities like run many jobs and simple triggers, cron triggers simultaneously in a single machine or clustered. Also, it can be run on memory or persisting on a database. For more details Scheduling in Spring with Quartz

I have created a basic setup that is focused on scheduling concept. There are there methods to create, list and kill jobs. It is added that Thread.sleep for simulating a long-running job.

Scenario

Create a new job

POST http://localhost:8080/start/foo

Trigger is created. Job name is 'foo-1609322783667'

List triggers by job

GET http://localhost:8080/list/foo

[
    "foo-1609322783667"
]

Kill the running job

DELETE http://localhost:8080/kill/foo

Job is interrupted

Console output:

2020-12-30 13:06:23.671  INFO 920 --- [nio-8080-exec-3] com.example.demo.HomeController          : Job is created. It will be triggered at Wed Dec 30 13:06:28 EET 2020
2020-12-30 13:06:28.681  INFO 920 --- [eduler_Worker-1] com.example.demo.job.FooJob              : Job started DEFAULT.foo-1609322783667
2020-12-30 13:06:51.109  INFO 920 --- [eduler_Worker-1] com.example.demo.job.FooJob              : Job is interrupted DEFAULT.foo-1609322783667
2020-12-30 13:06:51.109  INFO 920 --- [eduler_Worker-1] com.example.demo.job.FooJob              : Job completed DEFAULT.foo-1609322783667

Source Code

pom.xml (if you are using Gradle, you can change definitions on build.gradle)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

application.properties

spring.quartz.job-store-type=memory

JobConfig

@Configuration
public class JobConfig {
    @Bean
    public JobDetailFactoryBean fooJobDetail() {
        JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
        jobDetailFactory.setJobClass(FooJob.class);
        jobDetailFactory.setDurability(true);
        return jobDetailFactory;
    }

    @Bean
    public JobDetailFactoryBean barJobDetail() {
        JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
        jobDetailFactory.setJobClass(BarJob.class);
        jobDetailFactory.setDurability(true);
        return jobDetailFactory;
    }
}

BarJob

@Slf4j
@Service
public class BarJob implements InterruptableJob {
    private Thread thread;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("Job started {}", jobExecutionContext.getTrigger().getKey());
        thread = Thread.currentThread();
        try {
            Thread.sleep(50_000); // wait 50 seconds
        } catch (InterruptedException ex) {
            log.info("Job is interrupted {}", jobExecutionContext.getTrigger().getKey());
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
        log.info("Job completed {}", jobExecutionContext.getTrigger().getKey());
    }

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        thread = Thread.currentThread();
    }
}

FooJob

@Slf4j
@Service
public class FooJob implements InterruptableJob {
    private Thread thread;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("Job started {}", jobExecutionContext.getTrigger().getKey());
        thread = Thread.currentThread();
        try {
            Thread.sleep(100_000); // wait 100 seconds
        } catch (InterruptedException ex) {
            log.info("Job is interrupted {}", jobExecutionContext.getTrigger().getKey());
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
        log.info("Job completed {}", jobExecutionContext.getTrigger().getKey());
    }

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        thread.interrupt();
    }
}

HomeController

@RestController
@Slf4j
public class HomeController {
    @Autowired
    private Scheduler scheduler;

    @Autowired
    @Qualifier("fooJobDetail")
    private JobDetail fooJobDetail;

    @Autowired
    @Qualifier("barJobDetail")
    private JobDetail barJobDetail;

    @PostMapping("/start/{jobName}")
    public ResponseEntity<String> startJob(@PathVariable("jobName") String jobName) throws SchedulerException {
        Optional<JobDetail> jobDetail = parseJob(jobName);
        if (!jobDetail.isPresent()) {
            return ResponseEntity.badRequest().body("Invalid job name");
        }

        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail.get())
                .withIdentity(jobName + "-" + new Date().getTime()) // unique name
                .startAt(Date.from(Instant.now().plusSeconds(5)))   // starts 5 seconds later
                .build();

        Date date = scheduler.scheduleJob(trigger);
        log.info("Job is created. It will be triggered at {}", date);

        return ResponseEntity.ok("Trigger is created. Job name is '" + trigger.getKey().getName() + "'");
    }

    /**
     * Find the job by job name
     */
    private Optional<JobDetail> parseJob(String jobName) {
        if ("foo".equals(jobName)) {
            return Optional.of(fooJobDetail);
        } else if ("bar".equals(jobName)) {
            return Optional.of(barJobDetail);
        }
        return Optional.empty();
    }

    @GetMapping("/list/{jobName}")
    public ResponseEntity<List<String>> listTriggers(@PathVariable("jobName") String jobName) throws SchedulerException {
        Optional<JobDetail> jobDetail = parseJob(jobName);
        if (!jobDetail.isPresent()) {
            return ResponseEntity.badRequest().build();
        }

        List<String> triggers = scheduler.getTriggersOfJob(jobDetail.get().getKey()).stream()
                .map(t -> t.getKey().getName())
                .collect(Collectors.toList());
        return ResponseEntity.ok(triggers);
    }

    @DeleteMapping("/kill/{jobName}")
    public ResponseEntity<String> killTrigger(@PathVariable("jobName") String jobName) throws SchedulerException {
        Optional<JobDetail> jobDetail = parseJob(jobName);
        if (!jobDetail.isPresent()) {
            return ResponseEntity.badRequest().build();
        }
        scheduler.interrupt(jobDetail.get().getKey());
        return ResponseEntity.ok("Job is interrupted");
    }
}


Source: stackoverflow