Using the Spring @Schedule Annotation

written by Andrew Wilson on May 30, 2012 with 3 comments

In a previous life I had a requirement that a web application scanned the expiration date of purchased content and sent one of three emails letting the user know that the item would expire soon, was going to expire very soon and that the item has expired. It fired up at early in the morning when the server had the lowest utilization. Later, I had to write a similar feature that would run every couple of minutes. It wasn’t terribly hard to implement the logic, but the scheduler was an external component that required much more work to configure than I would have liked.

The Spring framework provides a really simple API for scheduling tasks. It requires two XML configurations and then an @Scheduled annotation for each of your tasks. I’ll demonstrate the process with an example from VoltDB’s Voter application (you can read more about Voter here and here).  My example executes two tasks, one for writing randomly generated votes for a candidate to a database and another task for reporting on the performance of the database driver. Let’s begin by looking at the configuration file.

<task:annotation-driven scheduler="taskScheduler"/>
<task:scheduler id="taskScheduler" pool-size="2"/>

The above configuration tells Spring that I am using annotations to define scheduled tasks and that I want my scheduler to only have two threads. Spring will scan my application for the @Scheduled annotation and then initialize the tasks and begin executing them.

There are three scheduling methods. The first is “fixedRate” and will execute every N milliseconds after the start of the previous execution. The second is “fixedDelay” and will execute every N milliseconds upon completion of the previous execution. The third method is “cron” and follows the standard cron syntax. The example will use both “fixedDelay” and “fixedRate” and I’ll talk about one important item to consider when designing your scheduled tasks: shutting down your application.

@Service
public class VoteTask {
@Autowired
 VoterRepository voterRepository;
[/sourcecode]

// … More code

[sourcecode language="javascript"]
 @Scheduled(fixedDelay = 10)
 public void createVotes() {
   if (this.voterRepository != null) {
     try {
       if (this.started == false) {
       start();
       waitForCompletion();
       this.started = true;
       }
     vote();
     } catch (Exception e) {
     e.printStackTrace();
     }
   } else {
   logger.error("Voter repository could not be found");
   }
 }

public void vote() throws Exception {
   for (int i = 0; i < 1000000; i++) {
     Vote vote = phoneCallGenerator.receive(2);
     this.voterRepository.vote(new ProcedureCallback() {
       public void clientCallback(ClientResponse arg0)
         throws Exception {
         }
       }, vote);
     }
   }
 }

The code has a lot more than what is shown. For the sake of the example, we are focusing on how to set the task up, what the task does and when it executes.

First, annotate the object as an @Service. Second, create a method that will run your task. In this case, the task is the createVotes() method which will execute 10ms after completion of a previous execution. The task will make a few method calles and then call the vote() method. The vote method calls the VoltDB client repository and adds a vote to a given candidate. It does this within a loop that executes a million times.

Why not just replace the loop with a while(true) instead of the task and the very large loop? The sample application runs within tomcat and using the while(true) will never allow the task to shut down. So even if you execute tomcat’s shutdown script, tomcat will only be able to close the sockets, but not the task itself.

@Service
public class StatisticsTask {
  private static final Logger logger = LoggerFactory .getLogger(StatisticsTask.class);

  @Autowired
  VoterRepository voterRepository;

  ClientStatsContext periodicStatsContext;
  long benchmarkStartTS = -1;

  @Scheduled(fixedRate = 5000)
  public void reportStatistics() {

    if (this.periodicStatsContext == null) {
      this.periodicStatsContext = this.voterRepository
      .createClientStats();
      }

    ClientStats stats = this.periodicStatsContext.fetchAndResetBaseline()
    .getStats();

    if (this.benchmarkStartTS < 1) {
      this.benchmarkStartTS = stats.getEndTimestamp();
      }

    long time = Math
    .round((stats.getEndTimestamp() - benchmarkStartTS) / 1000.0);

    String formattedString = String.format("%02d:%02d:%02d ", time / 3600,
    (time / 60) % 60, time % 60);
    formattedString += String.format("Throughput %d/s, ",
    stats.getTxnThroughput());
    formattedString += String.format("Aborts/Failures %d/%d, ",
    stats.getInvocationAborts(), stats.getInvocationErrors());
    formattedString += String.format("Avg/95%% Latency %d/%dms",
    stats.getAverageLatency(), stats.kPercentileLatency(0.95));
    logger.info(formattedString);
    }
  }

This task class calculates the VoltDB client statistics and displays the throughput, the error rates and the latencies. The log statements will look like the following:

INFO: Server startup in 2781 ms
INFO : org.voltdb.examples.task.StatisticsTask – 00:00:00 Throughput 75743/s, Aborts/Failures 0/0, Avg/95% Latency 28/40ms
INFO : org.voltdb.examples.task.StatisticsTask – 00:00:05 Throughput 96736/s, Aborts/Failures 0/0, Avg/95% Latency 29/35ms
INFO : org.voltdb.examples.task.StatisticsTask – 00:00:10 Throughput 97306/s, Aborts/Failures 0/0, Avg/95% Latency 28/35ms
INFO : org.voltdb.examples.task.StatisticsTask – 00:00:15 Throughput 97430/s, Aborts/Failures 0/0, Avg/95% Latency 28/35ms
INFO : org.voltdb.examples.task.StatisticsTask – 00:00:20 Throughput 97182/s, Aborts/Failures 0/0, Avg/95% Latency 28/35ms

This task runs at a fixed rate because we want to ensure that the statistics are correct for each 5 second interval. In this case, we are seeing that the voter task is executing a bit more than 97K transactions per second against a VoltDB database running on the same host as Tomcat.

Creating tasks is a relatively simple process provided that you configured everything correctly. Again, there are three critical items that cannot be left out. The first are the XML configurations, without them Spring will not scan your application for the @Scheduled annotations and the size of your threadpool will be whatever the defaults happen to be. The second is the @Service annotation or Spring will not examine the class for the @Scheduled annotation. Finally, the @Scheduled annotation using the “fixedRate”, “FixedDelay” or “cron” attributes to invoke your task.