Skip to content

Commit

Permalink
Add demo of job scheduling using quartz
Browse files Browse the repository at this point in the history
The GreetingController is extended, so it now allows a POST which schedules a job to
run at a set frequency. The job just logs the Greeting to the console

Quartz is a framework which allows Jobs to be scheduled to run asynchronously, at a fixed
frequency or point in time. This is useful if you want to allow the API user to
schedule a task to be performed at a set point in the future

The job schedule is persisted in a postgres database, so they will live beyond the lifespan
of an application run. I.e. the application can be restarted, and the running jobs will
restart. The database schema is created using Flyway

Jira: n/a
  • Loading branch information
Robin Wilkins committed Oct 21, 2020
1 parent 08c2b1b commit f88bc8a
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 17 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ plugins {
id 'org.springframework.boot' version '2.2.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'

id "io.freefair.lombok" version "5.1.0"
}

Expand All @@ -12,9 +11,13 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-quartz")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation('org.flywaydb:flyway-core')
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
runtimeOnly('org.postgresql:postgresql')
}

test {
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/wilkins/showcase/controllers/Greeting.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import lombok.Value;
import lombok.With;

import java.io.Serializable;

@Value(staticConstructor = "of")
@With
public class Greeting {
public class Greeting implements Serializable {

private static final long serialVersionUID = -2464792326773658903L;
// private and final modifiers added by @Value annotation
String salutation;
String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,82 @@
package com.wilkins.showcase.controllers;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.quartz.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.util.StringUtils.isEmpty;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.UUID;

import static org.quartz.TriggerBuilder.newTrigger;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.ResponseEntity.noContent;
import static org.springframework.http.ResponseEntity.notFound;

@Slf4j
@RestController
@RequestMapping(path = "/greeting", produces = APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class GreetingController {

@GetMapping("/greeting")
public Greeting getGreeting(@RequestParam(name = "salutation", required = false) String salutationParam,
@RequestParam(name = "name", required = false) String nameParam) {
private static final String JOB_GROUP = "greeting-jobs";
private final Scheduler scheduler;

log.info("A greeting was requested");
@GetMapping
public Greeting getGreeting(@RequestParam(name = "salutation", required = false, defaultValue = "hello") String salutationParam,
@RequestParam(name = "name", required = false, defaultValue = "world") String nameParam) {

Greeting greeting = Greeting.of("hello", "world");
log.info("A greeting was requested");

if (!isEmpty(salutationParam)) {
greeting = greeting.withSalutation(salutationParam);
}
if (!isEmpty(nameParam)) {
greeting = greeting.withName(nameParam);
}
var greeting = Greeting.of(salutationParam, nameParam);

log.info("Greeting returned: {}", greeting);

return greeting;
}

@PostMapping
public String postGreeting(@RequestParam(name = "salutation", required = false, defaultValue = "hello") String salutationParam,
@RequestParam(name = "name", required = false, defaultValue = "world") String nameParam,
@RequestParam(name = "durationMins", required = false, defaultValue = "2") String durationMins) throws SchedulerException {
var jobDataMap = new JobDataMap();
jobDataMap.put("salutation", salutationParam);
jobDataMap.put("name", nameParam);
jobDataMap.put("greeting", Greeting.of(salutationParam, nameParam));
var id = UUID.randomUUID().toString();
var jobDetail = JobBuilder.newJob()
.ofType(GreetingJob.class)
.withIdentity(id, JOB_GROUP)
.withDescription("Send Greeting Job")
.usingJobData(jobDataMap)
.requestRecovery()
// .storeDurably()
.build();
Date now = new Date();
Date startAt = Timestamp.valueOf(LocalDateTime.from(
now.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime())
.plusMinutes(1));
var trigger = newTrigger()
.forJob(jobDetail)
.withIdentity(jobDetail.getKey().getName(), "greeting-triggers")
.withDescription("Send Greeting Trigger")
.startAt(startAt)
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withRepeatCount(0))
.build();
log.info("scheduling job");
scheduler.scheduleJob(jobDetail, trigger);
return id;
}

@DeleteMapping("/{id}")
public ResponseEntity<?> deleteGreeting(@PathVariable String id) throws SchedulerException {
log.info("deleting job: {}", id);
return scheduler.deleteJob(new JobKey(id, JOB_GROUP)) ? noContent().build() : notFound().build();
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/wilkins/showcase/controllers/GreetingJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.wilkins.showcase.controllers;

import com.wilkins.showcase.services.Greeter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;

@Slf4j
@RequiredArgsConstructor
public class GreetingJob implements Job {

private final Greeter greeter;

@Override
public void execute(JobExecutionContext context) {
JobDataMap jobDataMap = context.getMergedJobDataMap();
log.info("******** {}, {} *************", context.getJobDetail().getKey().getName(),
greeter.greet((Greeting) jobDataMap.get("greeting")));
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/wilkins/showcase/services/Greeter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.wilkins.showcase.services;

import com.wilkins.showcase.controllers.Greeting;
import org.springframework.stereotype.Component;

@Component
public class Greeter {

public String greet(Greeting greeting) {
return greeting.getSalutation() + " " + greeting.getName();
}
}
17 changes: 17 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
spring:
quartz:
job-store-type: jdbc
properties.org.quartz.threadPool.threadCount: 5
properties:
org:
quartz:
jobStore:
driverDelegateClass: "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"
wait-for-jobs-to-complete-on-shutdown: true
jdbc:
initialize-schema: never

datasource:
url: "jdbc:postgresql://localhost:5432/postgres?currentSchema=showcase"
username: "postgres"
password: "Passw0rd"
Loading

0 comments on commit f88bc8a

Please sign in to comment.