Schedule periodic tasks inside your Grails applications
Learn how to use Schwartz to schedule periodic tasks inside your Grails applications
Authors: Iván López, Sergio del Amo
Grails Version: 3.3.2
1 Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
Nowadays it is pretty usual to have some kind of cron or scheduled task that needs to run every midnight, every hour, a few times a week,… In the Java world we have been using the Quartz library for ages to do this.
In this guide you will learn how to use the Grails Schwartz plugin to schedule periodic tasks inside a Grails application
2.1 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-schwartz.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/grails-schwartz/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/grails-schwartz/complete
|
3 Writing the Guide
3.1 Installing the plugin
To install the plugin, modify build.gradle
:
buildscript {
repositories {
mavenLocal()
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
classpath "org.grails:grails-gradle-plugin:$grailsVersion"
classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:1.1"
classpath "gradle.plugin.com.energizedwork:idea-gradle-plugins:1.4"
classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.6"
classpath 'com.agileorbit:schwartz:1.0.1' (1)
}
}
1 | Schwartz Plugin |
And also add the following to the dependencies
block:
compile 'com.agileorbit:schwartz:1.0.1'
3.2 Set log level INFO for package demo
During this guide, we are going to use several log statements to show Job execution.
Add at the end of logback.groovy
the next statement:
logger('demo', INFO, ['STDOUT'], false)
The above line configures a logger for package demo
with log level INFO which uses the STDOUT appender and with adaptivity set to false.
3.3 Creating a simple Job
The plugin provides a new Grails command to create a new job. Just execute:
./grailsw create-job HelloWorld
This will create the file grails-app/services/demo/HelloWorldJobService.groovy
with a few examples commented out of
how to trigger the job.
Please note that Schwartz jobs are not Grails artifacts but the plugin uses the convention of naming the job with the
sufix JobService. The job files are placed by default in grails-app/services
directory although there are different
options to create just a POGO.
The very basic skeleton of a Schwartz Job is:
class HelloWorldJobService implements SchwartzJob { (1)
void execute(JobExecutionContext context) throws JobExecutionException {
(2)
}
void buildTriggers() {
(3)
}
}
1 | Implement SchwartzJob |
2 | The job logic goes here |
3 | The different triggers of the job |
Let’s modify the previous code and add the following:
void execute(JobExecutionContext context) throws JobExecutionException {
log.info "{}:{}", context.trigger.key, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date()) (1)
}
void buildTriggers() {
(2)
triggers <<
factory('Simple Job every 10 seconds')
.intervalInSeconds(10)
.build()
(3)
triggers <<
factory('Simple Job every 45 seconds')
.startDelay(5000)
.intervalInSeconds(45)
.build()
}
1 | The name of the job and the date |
2 | Create a trigger every 10 seconds |
3 | Create another trigger every 45 seconds with an initial delay of 5 seconds (5000 millis) |
Now start the application:
./gradlew bootRun
And after a few seconds you will see the following output:
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:24:53 CET 2018 (1)
DEFAULT.Simple Job every 45 seconds -> Fri Jan 19 13:24:58 CET 2018 (2)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:03 CET 2018 (3)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:13 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:23 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:33 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:43 CET 2018
DEFAULT.Simple Job every 45 seconds -> Fri Jan 19 13:25:43 CET 2018 (4)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:53 CET 2018
1 | First execution of 10 seconds job just after the application starts |
2 | The 45 seconds job just starts 5 seconds after the app starts. See startDelay in the trigger configuration. |
3 | Second execution of 10 seconds job just 10 seconds after the first execution |
4 | Second execution of 45 seconds job just 45 seconds after the first execution |
3.4 Business logic in Services
Although the previous example is valid, usually you don’t want to put your business logic in a Job. A better approach is to create an additional service which the JobService
invokes. This approach decouples your business logic from the scheduling logic. Moreover, it facilities testing and maintenance. Let’s see an example:
Create the following service:
package demo
import groovy.transform.CompileStatic
@CompileStatic
class EmailService {
void send(String user) {
log.info "Sending email to ${user}"
}
}
And then the job:
@CompileStatic
@Slf4j
class DailyEmailJobService implements SchwartzJob {
EmailService emailService (1)
void execute(JobExecutionContext context) throws JobExecutionException {
emailService.send('[email protected]') (2)
}
void buildTriggers() {
triggers << factory('Daily email job')
.cronSchedule("0 30 4 1/1 * ? *") (3)
.build()
}
}
1 | Inject the service |
2 | Call it |
3 | Trigger the job once a day at 04:30 AM |
Cron notation is often confusing. To simplify trigger definition, the plugin comes with several builders. The builders support a fluent API, and you end up with readable and intuitive code to define triggers, and IDE autocompletion.
The previous cron configuration can be written as:
import com.agileorbit.schwartz.SchwartzJob
import demo.EmailService
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import static org.quartz.DateBuilder.todayAt
import static org.quartz.DateBuilder.tomorrowAt
@CompileStatic
@Slf4j
class DailyEmailJobService implements SchwartzJob {
final int HOUR = 4
final int MINUTE = 30
final int SECONDS = 0
EmailService emailService (1)
void execute(JobExecutionContext context) throws JobExecutionException {
emailService.send('[email protected]') (2)
}
Date dailyDate() {
Date startAt = todayAt(HOUR, MINUTE, SECONDS)
if (startAt.before(new Date())) {
return tomorrowAt(HOUR, MINUTE, SECONDS)
}
startAt
}
void buildTriggers() {
Date startAt = dailyDate()
triggers << factory('Daily email job')
.startAt(startAt)
.intervalInDays(1)
.build()
}
}
3.5 Scheduling a Job with data manually
Imagine the following scenario. You want to send every user an email 2 hours after they registered into your app. You want to ask him about his experiences during this first interaction with your service.
For this guide, we are going to schedule a Job to trigger after one minute.
Modify BootStrap.groovy
to call twice a new service named RegisterService
.
package demo
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
RegisterService registerService
def init = { servletContext ->
registerService.register('[email protected]')
sleep(20_000)
registerService.register('[email protected]')
}
def destroy = {
}
}
Create RegisterService.groovy
package demo
import com.agileorbit.schwartz.QuartzService
import com.agileorbit.schwartz.builder.BuilderFactory
import groovy.time.TimeCategory
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobDataMap
import org.quartz.JobDetail
import org.quartz.JobKey
import org.quartz.Trigger
import org.quartz.TriggerBuilder
import java.text.SimpleDateFormat
@Slf4j
@CompileStatic
class RegisterService {
QuartzService quartzService
void register(String email) {
log.info 'saving {} at {}', email, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
scheduleFollowupEmail(email)
}
@CompileDynamic
Date startAtDate() { (1)
Date startAt = new Date()
use(TimeCategory) {
startAt = startAt + 1.minute
}
startAt
}
void scheduleFollowupEmail(String email) {
JobDataMap jobDataMap = new JobDataMap()
jobDataMap.put('email', email)
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(JobKey.jobKey(FollowupEmailJobService.simpleName)) (2)
.startAt(startAtDate())
.usingJobData(jobDataMap) (3)
.build()
quartzService.scheduleTrigger(trigger) (4)
}
}
1 | This method returns a date one minute into the future |
2 | We can find the necessary Jobkey with the JobService’s simpleName |
3 | We can pass data into a Job execution. |
4 | Schedule the trigger |
Create FollowupEmailJobService.groovy
package demo
import com.agileorbit.schwartz.SchwartzJob
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobDataMap
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import java.text.SimpleDateFormat
@Slf4j
@CompileStatic
class FollowupEmailJobService implements SchwartzJob {
void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.mergedJobDataMap
String email = jobDataMap.getString('email') (1)
log.info 'Sending followup email to: {} at {}', email, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
}
@Override
void buildTriggers() {
}
}
1 | Extract the job data |
If you execute the above code you will see the Job being executed one minute after we schedule it and with the supplied email address.
INFO demo.RegisterService : saving [email protected] at 23/1/2018 07:55:21
INFO demo.RegisterService : saving [email protected] at 23/1/2018 07:55:41
INFO demo.FollowupEmailJobService : Sending followup email to: [email protected] at 23/1/2018 07:56:21
INFO demo.FollowupEmailJobService : Sending followup email to: [email protected] at 23/1/2018 07:56:41
3.6 Concurrent Job execution
Imagine you develop a Software as a Service application with Grails. Each day at 7:00 AM you want to check if any user’s subscription has expired. If it is finished you want to create an invoice for the new period, save the invoice and update the user’s subscription expiration date in the database.
You want to disable the concurrent execution of such a Job. Using just Quartz you need to annotate the job with
@DisallowConcurrentExecution
. With Schwartz you still can do it, or, if you prefer you can implement the trait StatefulSchwartzJob
instead of SchwartzJob
. StatefulSchwartzJob
extends form SchwartzJob
and adds two annotations @DisallowConcurrentExecution
, @PersistJobDataAfterExecution
.
A sample code could look like:
package demo
import com.agileorbit.schwartz.StatefulSchwartzJob
import grails.gorm.transactions.Transactional
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import static org.quartz.DateBuilder.todayAt
import static org.quartz.DateBuilder.tomorrowAt
class GenerateInvoiceJobService implements StatefulSchwartzJob {
final int HOUR = 7
final int MINUTE = 0
final int SECONDS = 0
GenerateInvoiceService generateInvoiceService
@Transactional (1)
@Override
void execute(JobExecutionContext context) throws JobExecutionException {
generateInvoiceService.generateInvoices()
}
@Override
void buildTriggers() {
Date startAt = dailyDate()
triggers << factory('Daily email job at 7:00 AM')
.startAt(startAt)
.intervalInDays(1)
.build()
}
Date dailyDate() {
Date startAt = todayAt(HOUR, MINUTE, SECONDS)
if (startAt.before(new Date())) {
return tomorrowAt(HOUR, MINUTE, SECONDS)
}
startAt
}
}
1 | Wrap the job’s execution in a transaction. |
package demo
import groovy.transform.CompileStatic
@CompileStatic
class GenerateInvoiceService {
void generateInvoices() {
// Check if users subscription has finished.
// Generate an invoice for a new period and save it in the database
// update user's subscription expiration date
}
}
3.7 Summary
During this guide, we learned how to configure Jobs manually, with cron statements, with the plugin fluid API. Moreover, we learned how to disable concurrent execution or pass Job Data.
Please, refer to the Grails Schwartz plugin’s documentation to learn more.