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?
Advertisement
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"); } }