Show Navigation

Grails + @Scheduled

Learn how to use Spring Task Execution and Scheduling to schedule periodic tasks inside your Grails applications

Authors: Ben Rhine

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 native Spring Task Execution and Scheduling to schedule periodic tasks inside a Grails application.

For additional documentation see: @Scheduled TaskScheduler PeriodicTrigger

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-scheduled/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-scheduled/complete

3 Writing Schedules

Lucky for us using Spring Task Execution and Scheduling is an out of the box feature for Grails so there is no need to add any additional dependencies. Lets go ahead and get started.

3.1 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.2 Creating a simple Job

Since scheduling is native functionality. Just execute:

./grailsw create-service HelloWorldJob

This will create the file grails-app/services/demo/HelloWorldJobService.groovy with a empty template method.

The very basic skeleton is:

grails-app/services/demo/HelloWorldJobService.groovy
@Transactional (1)
class HelloWorldJobService {

    def serviceMethod() {
        (2)
    }
}
1 By default services are Transactional
2 The job logic goes here

Let’s modify the previous code and add the following:

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

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.annotation.Scheduled

import java.text.SimpleDateFormat

@Slf4j (1)
@CompileStatic (2)
class HelloWorldJobService {

    boolean lazyInit = false (3)

    @Scheduled(fixedDelay = 10000L) (4)
    void executeEveryTen() {
        log.info "Simple Job every 10 seconds :{}", new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }

    @Scheduled(fixedDelay = 45000L, initialDelay = 5000L) (5)
    void executeEveryFourtyFive() {
        log.info "Simple Job every 45 seconds :{}", new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }
}
1 Although Grails artifacts ( services, controllers ) inject a logger automatically, we annotate the class with @Slf4j to get better IDE support. Depending on the Grails version you are using, @Slf4j annotation will have an impact on aspects such as static compilation, logger name, etc. as described in the Grails Logging Quickcast.
2 Remove @Transactional as this service does not require transactions. Add @CompileStatic for performance.
3 By default Grails' services are lazily initialized. You can disable this and make initialization eager with the lazyInit property. Without forcing the service to be eagerly initialized the jobs will not execute.
4 Create trigger every 10 seconds
5 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:

Simple Job every 45 seconds :14/2/2018 02:06:50 (1)
Simple Job every 10 seconds :14/2/2018 02:06:55 (2)
Simple Job every 10 seconds :14/2/2018 02:07:05 (3)
Simple Job every 10 seconds :14/2/2018 02:07:15
Simple Job every 10 seconds :14/2/2018 02:07:25
Simple Job every 45 seconds :14/2/2018 02:07:35 (4)
Simple Job every 10 seconds :14/2/2018 02:07:35
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
When using @Scheduled, boolean lazyInit = false is required or your schedule will not run.
@Scheduled requires the use of CONSTANT values. Meaning that attempting to generate / build values for fixedDelay, initialDelay, cron, ect. will not work and cause compile errors.

3.3 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
import groovy.util.logging.Slf4j

import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class EmailService {

    void send(String user, String message) {
        log.info "Sending email to ${user} : ${message}"+ ' at ' + new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }
}

And then the job:

grails-app/services/demo/DailyEmailJobService.groovy
@Slf4j
@CompileStatic
class DailyEmailJobService  {

    boolean lazyInit = false (1)

    EmailService emailService (2)

    @Scheduled(cron = "0 30 4 1/1 * ?") (4)
    void execute() {
        emailService.send('[email protected]', 'Test Message') (3)
    }
}
1 Force service to initialize, without forcing the service to be initialized the jobs will not execute
2 Inject the service
3 Call it
4 Trigger the job once a day at 04:30 AM

Cron notation is often confusing so to assist in having clear logic and to make more advanced scheduling easier next we will take a look at job execution using a TaskScheduler.

3.4 Task Scheduler Bean

To get started using more advance scheduling techniques first we need to create and register a TaskScheduler bean.

Create the following configuration file containing a new task scheduler bean

src/main/groovy/demo/SchedulingConfiguration.groovy
package demo

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

@Configuration (1)
class SchedulingConfiguration {

    @Bean
    ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler()
        threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler") (2)
        threadPoolTaskScheduler.initialize()
        threadPoolTaskScheduler
    }
}
1 Make your config discoverable
2 All related threads will be prefixed with ThreadPoolTaskScheduler

If you wish to allow for concurrent execution set the thread pools size ex. threadPoolTaskScheduler.setPoolSize(5)

By default ThreadPoolTaskScheduler only has a single thread available for execution, which means tasks will not execute concurrently

Now configure your app to automatically discover the new configuration so your new bean will work with standard Grails dependency injection

grails-app/init/demo/Application.groovy
package demo

import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import groovy.transform.CompileStatic
import org.springframework.context.annotation.ComponentScan

@CompileStatic
@ComponentScan('demo') (1)
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}
1 Enable discovery of additional configuration files

3.5 Runnable Tasks

Once you have created and wired the TaskScheduler you will need to create runnable tasks for it to execute.

Create the following task

grails-app/tasks/demo/EmailTask.groovy
package demo

class EmailTask implements Runnable { (1)

    String email (2)
    String message (3)
    EmailService emailService (4)

    EmailTask(EmailService emailService, String email, String message) { (5)
        this.emailService = emailService
        this.email = email
        this.message = message
    }

    @Override
    void run() { (6)
        emailService.send(email, message)
    }
}
1 Implement Runnable
2 Store incoming email address for later use
3 Store incoming message
4 Store incoming service
5 Task constructor: set email, `message and service
6 Run the task

3.6 Scheduling a Job 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 groovy.time.TimeCategory
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class RegisterService {

        ThreadPoolTaskScheduler threadPoolTaskScheduler (1)
        EmailService emailService (2)

        void register(String email, String message) {
                log.info 'saving {} at {}', email, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
                scheduleFollowupEmail(email, message)
        }

        @CompileDynamic
        Date startAtDate() { (3)
                Date startAt = new Date()
                use(TimeCategory) {
                        startAt = startAt + 1.minute
                }
                startAt
        }

        void scheduleFollowupEmail(String email, String message) {
                threadPoolTaskScheduler.schedule(new EmailTask(emailService, email, message), startAtDate()) (4)
        }
}
1 Dependency injection of our TaskScheduler bean
2 Dependency injection of emailService
3 This method returns a date one minute into the future
4 Schedule the trigger

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.EmailService            : Sending email to [email protected] : Follow up - How was your experience at 23/1/2018 07:56:21
INFO demo.EmailService            : Sending email to [email protected] : Follow up - How was your experience at 23/1/2018 07:56:41

3.7 Simplified Cron with TaskSchedule

In our previous cron example we did mention that many times cron can be confusing and is not intuitive. To alleviate some of that confusion here is the previous cron configuration written using the TaskScheduler

grails-app/services/demo/todayat/DailyMailJobService.groovy
import demo.EmailService
import demo.EmailTask
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Duration

@Slf4j
@CompileStatic
class DailyMailJobService {
    private final int HOUR = 4
    private final int MINUTE = 30
    private final int SECONDS = 0
    private static final long MILLISECONDS_IN_DAY = Duration.ofDays(1).getSeconds() * 1000 (4)

    ThreadPoolTaskScheduler threadPoolTaskScheduler (1)
    EmailService emailService (2)

    void register(String email, String message) {
        scheduleDailyEmail(email, message)
    }

    void scheduleDailyEmail(String email, String message) { (5)
        threadPoolTaskScheduler.scheduleAtFixedRate(new EmailTask(emailService, email, message), dailyDate(), MILLISECONDS_IN_DAY)
    }

    Date dailyDate() { (3)
        Date startAt = new Date(hours: HOUR, minutes: MINUTE, seconds: SECONDS)
        if(startAt.before(new Date())) {
            return (startAt + 1)
        }
        startAt
    }
}
1 Dependency injection of our TaskScheduler bean
2 Dependency injection of emailService
3 Calculate start time
4 Calculate repeat interval
5 Schedule task with start time and repeat interval

Once you have written your schedule using TaskScheduler, register the job in bootstrap.

grails-app/init/demo/BootStrap.groovy
package demo

import demo.todayat.DailyMailJobService
import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

        RegisterService registerService
    DailyMailJobService dailyMailJobService

    def init = { servletContext ->
        final String followUp = 'Follow up - How was your experience'
            registerService.register('[email protected]', followUp)
            sleep(20_000)
            registerService.register('[email protected]', followUp)
        dailyMailJobService.register('[email protected]', 'Daily Reminder')
    }
    def destroy = {
    }
}

3.8 Summary

During this guide, we learned how to configure Jobs using the @Scheduled annotation using fixedDelay, initialDelay, and cron as well as manually configuring jobs with a TaskScheduler and tasks. Moreover, we learned that default configuration for @Scheduled or the TaskScheduler prevents concurrent execution.

For more information review the Spring Task Execution and Scheduling 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