Show Navigation

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:

or

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 the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-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:

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:

build.gradle
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:

grails-app/conf/logback.groovy
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:

grails-app/services/demo/HelloWorldJobService.groovy
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:

grails-app/services/demo/HelloWorldJobService.groovy
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:

grails-app/services/demo/EmailService.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class EmailService {

    void send(String user) {
        log.info "Sending email to ${user}"
    }
}

And then the job:

grails-app/services/demo/DailyEmailJobService.groovy
@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:

grails-app/services/demo/todayat/DailyEmailJobService.groovy
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.

grails-app/init/demo/BootStrap.groovy
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

grails-app/services/demo/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

grails-app/services/demo/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:

grails-app/services/demo/GenerateInvoiceJobService.groovy
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.
grails-app/services/demo/GenerateInvoiceService.groovy
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.

4 Help with Grails

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team