The UrBlog

John's Ramblings about Software Development

Custom Scheduling in Spring

Spring 3.0 has simplified task scheduling. As part if this, they have deprecated the MethodInvokingTimerTaskFactoryBean and ScheduledTimerTask. Instead you create a scheduler that implements the TaskScheduler interface and uses a Trigger to specify when a task is scheduled to run. The XML and annotations allow you to specify fixedDelay, fixedRate or cron string. These are fixed at run time. This works great for triggers that are fixed at run time, but does not allow you any way to modify these at run time. The TaskScheduler interface provides methods to schedule a task with a trigger, so this gives us an opportunity to pass in a custom trigger that can have it’s trigger interval changed at run time. There are a number of ways to configure this. Here is a simple way I came up with that uses a single bean to schedule the task and change the fixedDelay at run time. This extends the example provided on the Spring blog noted earlier.



First we need a class that takes the scheduler, task and starting delay. For simplicity, it will also implement the Timer interface.

public class DynamicSchedule implements Trigger {

   private TaskScheduler scheduler;
   private Runnable task;
   private ScheduledFuture<?> future;
   private int delay;

   public DynamicSchedule(TaskScheduler scheduler, Runnable task, int delay) {
      this.scheduler = scheduler;
      this.task = task;
      reset(delay);
   }

   public void reset(int delay) {
      if (future != null) {
         System.out.println("Cancelling task...");
         future.cancel(true);
      }
      this.delay = delay;
      System.out.println("Starting task...");
      future = scheduler.schedule(task, this);
   }

   @Override
   public Date nextExecutionTime(TriggerContext triggerContext) {
      Date lastTime = triggerContext.lastActualExecutionTime();
      Date nextExecutionTime = (lastTime == null)
         ? new Date()
         : new Date(lastTime.getTime() + delay);
         System.out.println("DynamicSchedule -- delay: " + delay +
              ", lastActualExecutionTime: " + lastTime +
              "; nextExecutionTime: " + nextExecutionTime);
      return nextExecutionTime;
   }

}

Note the reset method which stops the scheduled task, changes the delay and then restarts the task. If you are changing the delay to a shorter delay, you want to restart with the new delay so it happens immediately. Alternately, you can skip canceling the task and the new delay is picked up on the next execution.

The rest of the code is the same, except for the SchedulerProcessor which has the @Scheduled annotation removed from the process method:

@Component
public class ScheduledProcessor {

   private final AtomicInteger counter = new AtomicInteger();

   @Autowired
   private Worker worker;

   public void process() {
      System.out.println("processing next 10 at " + new Date());
      for (int i = 0; i < 10; i++) {
         worker.work(counter.incrementAndGet());
      }
   }

}

In the XML configuration, we add a name to the scheduler and create the DynamicSchedule. We pass it the scheduler, the process method (wrapped in a MethodInvokingRunnable) and the default delay:

   <context:component-scan base-package="com/test" />

   <task:annotation-driven />

   <task:scheduler id="scheduler" />

   <bean id="dynamicSchedule" class="com.test.DynamicSchedule">
      <constructor-arg ref="scheduler" />
      <constructor-arg>
         <bean class="org.springframework.scheduling.support.MethodInvokingRunnable">
            <property name="targetObject" ref="scheduledProcessor" />
            <property name="targetMethod" value="process" />
         </bean>
      </constructor-arg>
      <constructor-arg value="3000" />
   </bean>

Now we can add a separate process that changes the delay to a random delay to test it out:

@Component
public class ScheduleChanger {

   @Autowired
   private DynamicSchedule dynamicSchedule;

   @Scheduled(fixedDelay=30000)
   public void change() {
      Random rnd = new Random();
      int nextTimeout = rnd.nextInt(30000);
      System.out.println("Changing poll time to: " + nextTimeout);
      dynamicSchedule.reset(nextTimeout);
   }

}

When you run this and view the output, you will see where the dynamic schedule trigger is fired and where the schedule gets changed.

Comments