Spring Scheduler — Dynamically changing the schedule
I have not seen in-process schedulers used widely. There is usually a dedicated application that is built solely for the purpose of scheduling and maintaining job runs. That way, there is only one application that focuses on scheduling, holding the job runs, monitoring and alerting after x failures, enabling/disabling a job etc. All that you have to do, is to configure jobs at this dedicated job scheduler to perform certain actions at your application at certain frequency. But in the absence of such dedicated setup, you would probably want to do the scheduling in process.
In this post, I will walk you over what I learnt trying to build one.
I needed a fixed delay schedule for a sync. It was easily doable with @Scheduled
and @EnabledScheduling
annotation in Spring boot
@Component
public class SyncScheduler {
@Autowired
private SyncDao syncDao;
public String getFixedDelay() {
return syncDao.getSchedule();
} @Scheduled(fixedDelayString =
"#{@syncScheduler.getFixedDelay()}")
public void run() {
// sync logic here.
}
}
But soon the requirement was to change this schedule dynamically. Can’t hide behind annotations anymore 🤷♀️ But actually it was way easier than I imagined.
Here is the code.
To summarize,
- I am scheduling task when the application comes up using a
SchedulingConfigurer
- I am using a
Trigger
— which is how after every run, the scheduler know when to kick start the next run. It gives aTriggerContext
to know when the last run executed/completed. I am particular interested inlastActualExecutionTime()
— when the last run started executing so that I can implement a fixed delay.
i.e. I have configured sync with 10 mins.
Say T1 executed at 10:00 and took 5 mins, using this trigger I would say next trigger should be at 10:00 + 10 = 10:10
Say T1 executed at 10:00 and took 12 mins, next trigger should have been at 10:10 but that time has passed already! So it will trigger right away.
This was perfect for my usecase.
- Then I get a handle to the
ScheduledFuture
so I can cancel whenever I need to change the schedule; or know when the next schedule will kick start
ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
So far I have done what that little @Scheduled
annotation was doing for me out of the box. Now its time to manipulate the schedule.
From the ScheduledFuture
obtained, I know when the next run is going to kick start or if there is one in progress already — using getDelay()
If there is one already in-progress, the updated schedule will apply after it is done and asking for the next schedule at Trigger.
I extended this a little to add a threshold too. i.e. just like how we don’t interrupt the one in-progress because the schedule will be updated anyway when it is done, we can also say, “the next run is coming up within X seconds so don’t bother cancelling this future and rescheduling one again. Instead, just let the next run pick up the updated schedule”
Problem is when your initial schedule was 1 hour and now you want to change it to, say, 5 mins.
If your last run was at 10:00, sure you wouldn’t want to wait till 11:00 for the change to take effect. That’s why we cancel the future and re-configure the schedule again.
public synchronized void updateSchedule(
Integer syncScheduleInSeconds)
{
LOGGER.info(“Sync schedule is updated in DB”);
this.scheduleInSeconds = syncScheduleInSeconds; long delayInSeconds= this.scheduledFuture
.getDelay(TimeUnit.SECONDS); if (delayInSeconds < 0) {
LOGGER.info(“Sync run is already in process. New schedule
will take effect after the current run”);
} else if (delayInSeconds <
CANCEL_SCHEDULED_TASK_DELAY_THRESHOLD_IN_SECONDS){
LOGGER.info(“Next sync is less than {} seconds away. after
the next run, schedule will automatically be
adjusted.”,
CANCEL_SCHEDULED_TASK_DELAY_THRESHOLD_IN_SECONDS);
} else {
LOGGER.info(“Next sync is more than {} seconds away.
scheduledFuture.delay() is {}. Hence cancelling the
schedule and rescheduling.”,
CANCEL_SCHEDULED_TASK_DELAY_THRESHOLD_IN_SECONDS,
delayInSeconds); this.scheduledFuture.cancel(false);
LOGGER.info(“Reconfiguring sync for {} with new schedule
{}”, jobName, syncScheduleInSeconds);
configureTasks(this.scheduledTaskRegistrar);
}
}
There is one catch though. These cancelled futures can pile up in the internal queue maintained in Executor and cause memory leaks.
Make sure you set
setRemoveOnCancelPolicy(true)
at TaskScheduler.
Runnable code is here