Modify scheduler timing dynamically based on the condition used with spring-boot @Scheduled annotation Modify scheduler timing dynamically based on the condition used with spring-boot @Scheduled annotation spring spring

Modify scheduler timing dynamically based on the condition used with spring-boot @Scheduled annotation


Using @Scheduled will only allow to use static schedules. You can use properties to make the schedule configruable like this

@Scheduled(cron = "${yourConfiguration.cronExpression}")// or@Scheduled(fixedDelayString = "${yourConfiguration.fixedDelay}")

However the resulting schedule will be fixed once your spring context is initialized (the application is started).

To get fine grained control over a scheduled execution you need implement a custom Trigger - similar to what you already did. Together with the to be executed task, this trigger can be registered by implementing SchedulingConfigurer in your @Configuration class using ScheduledTaskRegistrar.addTriggerTask:

@Configuration@EnableSchedulingpublic class AppConfig implements SchedulingConfigurer {    @Override    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {        taskRegistrar.setScheduler(taskScheduler());        taskRegistrar.addTriggerTask(() -> myTask().work(), myTrigger());    }    @Bean(destroyMethod="shutdown")    public Executor taskScheduler() {        return Executors.newScheduledThreadPool(42);    }    @Bean    public CustomDynamicSchedule myTrigger() {        new CustomDynamicSchedule();    }    @Bean    public MyTask myTask() {        return new MyTask();    }}

But don't do any registration of a task in the CustomDynamicSchedule just use it to compute the next execution time:

public class CustomDynamicSchedule extends DynamicSchedule implements Trigger {    private long delayInterval;    @Override    public synchronized void increaseDelayInterval(Long delay) {        this.delayInterval += delay;    }    @Override    public synchronized void decreaseDelayInterval(Long delay) {        this.delayInterval += delay;    }    @Override    public synchronized void delay(Long delay) {        this.delayInterval = delay;    }    @Override    public Date nextExecutionTime(TriggerContext triggerContext) {        Date lastTime = triggerContext.lastActualExecutionTime();        return (lastTime == null) ? new Date() : new Date(lastTime.getTime() + delayInterval);    }}

But remember to make CustomDynamicSchedule thread safe as it will be created as singleton by spring and might be accessed by multiple threads in parallel.


Spring's @Scheduled annotation does not provide this support.

This is my preferred implementation of a similar feature using a Queue-based solution which allows flexibility of timing and very robust implementation of this scheduler functionality.

This is the pipeline-enter image description here

  1. Cron Maintainer and Publisher- For every task, there is an affiliated cron and a single-threaded executor service which is responsible for publishing messages to the Queue as per the cron. The task-cron mappings are persisted in the database and are initialized during the startup. Also, we have an API exposed to update cron for a task at runtime.

    We simply shut down the old scheduled executor service and create a one whenever we trigger a change in the cron via our API. Also, we update the same in the database.

  2. Queue- Used for storing messages published by the publisher.
  3. Scheduler- This is where the business logic of the schedulers reside. A listener on the Queue(Kafka, in our case) listens for the incoming messages and invokes the corresponding scheduler task in a new thread whenever it receives a message for the same.

There are various advantages to this approach. This decouples the scheduler from the schedule management task. Now, the scheduler can focus on business logic alone. Also, we can write as many schedulers as we want, all listening to the same queue and acting accordingly.


With annotation you can do it only by aproximation by finding a common denominator and poll it. I will show you later. If you want a real dynamic solution you can not use annotations but you can use programmatic configuration. The positive of this solution is that you can change the execution period even in runtime! Here is an example on how to do that:

  public initializeDynamicScheduledTAsk (ThreadPoolTaskScheduler scheduler,Date start,long executionPeriod) {    scheduler.schedule(      new ScheduledTask(),      new Date(startTime),period    );    }class ScheduledTask implements Runnable{    @Override    public void run() {       // my scheduled logic here     }}

There is a way to cheat and actually do something with annotations. But you can do that only if precision is not important. What does it mean precision. If you know that you want to start it every 5 seconds but 100ms more or less is not important. If you know that you need to start every 5-6-8 or 10 seconds you can configure one job that is executing every second and check within one if statement how long has it passed since the previous execution. It is very lame, but it works :) as long as you don't need up to millisecond precision. Here is an example:

public class SemiDynamicScheduledService {private Long lastExecution;@Value(#{yourDynamicConfiguration})private int executeEveryInMS@Scheduled(fixedDelay=1000)public semiDynamicScheduledMethod() {   if (System.currentTimeMS() - lastExecution>executeEveryInMS) {      lastExecution = System.currentTimeMS();      // put your processing logic here   }}}

I is a bit lame but will do the job for simple cases.