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