Scheduling a job with Spring programmatically (with fixedRate set dynamically)
Asked Answered
M

8

103

Currently I have this :

@Scheduled(fixedRate=5000)
public void getSchedule(){
   System.out.println("in scheduled job");
}

I could change this to use a reference to a property

@Scheduled(fixedRateString="${myRate}")
public void getSchedule(){
   System.out.println("in scheduled job");
}

However I need to use a value obtained programmatically so the schedule can be changed without redeploying the app. What is the best way? I realize using annotations may not be possible...

Mylesmylitta answered 31/1, 2013 at 16:29 Comment(1)
You say "without redeploying the app". Changing a property reference can be done with an app restart without a redeploy (e.g. through updating a system property and then restarting). Is that sufficient, or do you want to be able to change it without a redeploy or a restart?Babbage
W
141

Using a Trigger you can calculate the next execution time on the fly.

Something like this should do the trick (adapted from the Javadoc for @EnableScheduling):

@Configuration
@EnableScheduling
public class MyAppConfig implements SchedulingConfigurer {

    @Autowired
    Environment env;

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
                new Runnable() {
                    @Override public void run() {
                        myBean().getSchedule();
                    }
                },
                new Trigger() {
                    @Override public Date nextExecutionTime(TriggerContext triggerContext) {
                        Calendar nextExecutionTime =  new GregorianCalendar();
                        Date lastActualExecutionTime = triggerContext.lastActualExecutionTime();
                        nextExecutionTime.setTime(lastActualExecutionTime != null ? lastActualExecutionTime : new Date());
                        nextExecutionTime.add(Calendar.MILLISECOND, env.getProperty("myRate", Integer.class)); //you can get the value from wherever you want
                        return nextExecutionTime.getTime();
                    }
                }
        );
    }
}
Wickerwork answered 31/1, 2013 at 18:35 Comment(17)
Looking at your code - are you not running into NullPointerException when the following line is executed nextExecutionTime.setTime(triggerContext.lastActualExecutionTime());. The triggerContext will return null when the application starts.Flavorous
And is there a way to interrupt the current Trigger and change it's value while it's sleeping.Flavorous
I only made sure this would compile, I never ran it.Wickerwork
I used it in my project, works perfectly. Fast Fix: lastActualExecutionTime != null ? lastActualExecutionTime : new Date()Songster
@AlexanderSchwarz: thanks, incorporated the fix in editWickerwork
Is it possible to remove task after exectuion from registry in this solution ?Kelpie
@Kelpie You can return null from nextExecutionTime() to prevent the trigger from continuing to fire, but I'm not sure if that destroys it completely. Alternatively you can schedule using taskRegistrar.getScheduler().schedule(Runnable, Trigger) and use the returned ScheduledFuture to cancel. That might be effectively the same as just returning null in the above, though.Wickerwork
Runnable is never getting executed when I try the above code. the nextexecutiontime returned in my sample code is 5secs after the current time. Does anyone know the possible reason?Arthritis
Thanks for the code, but i modified it, to have it started exactly upon startup: if (lastActualExecutionTime == null) { nextExecutionTime.setTime(new Date()); } else { // Already executed, should be plus'ed nextExecutionTime.setTime(lastActualExecutionTime); nextExecutionTime.add(Calendar.MILLISECOND, rate); }Commodity
@Ach, this works for me, only thing is the previous also works. so after trigger both previous one and new one getting triggered at their respective intervals.Arlyne
I found different solution. You can see my answer: https://mcmap.net/q/211833/-how-to-change-spring-39-s-scheduled-fixeddelay-at-runtimeAuctioneer
What is MyBean class here?Elbring
@AmanNagarkoti It's been awhile but IIRC it's some service class or similar that is responsible for retrieving the schedule from some source, i.e. property, database, etc.Wickerwork
Thanks for the response @ach, So how to configure that source.. form where .. means I need to implement this and how I'll change the schedule at run time...Elbring
@Wickerwork I want to implement this for one specific schedule, though I have multiple schedules running in my application.Elbring
@AmanNagarkoti That is really outside of the scope of this question. You could implement some sort of a cache or registry with an invalidation mechanism.Wickerwork
@AmanNagarkoti Have you understood what the MyBean class is about?Hills
E
27

You can also use Spring Expression Language (SpEL) for this.

Once this value is initialized, you won't be able to update this value.

@Scheduled(fixedRateString = "#{@applicationPropertyService.getApplicationProperty()}")
public void getSchedule(){
   System.out.println("in scheduled job");
}

@Service
public class ApplicationPropertyService {

    public String getApplicationProperty(){
        //get your value here
        return "5000";
    }
}
Evangelin answered 6/12, 2019 at 8:52 Comment(5)
I prefer this than the selected one because of less code and clean.Varian
You can still change the value by making the bean scope as RefreshScope and also by implementing RefreshScopeRefreshedEvent. Sample app here github.com/winster/SpringSchedulerDynamicLace
My question is, if we override the AppProp.Service class and give two different delay timing, the scheduler will run two times?Reviere
No it will not, it will use the overridden property, if you want your scheduler to run multiple times use CRONEvangelin
How it will work if we need to change cron value dynamically. this will not help.Horthy
S
20

To create and manage multiple dynamically scheduled tasks,

Scheduler configuration and bean:

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

@Configuration
@EnableScheduling
public class SchedulingConfigs implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(new Runnable() {
            @Override
            public void run() {
                // Do not put @Scheduled annotation above this method, we don't need it anymore.
                System.out.println("Running Scheduler..." + Calendar.getInstance().getTime());
            }
        }, new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                Calendar nextExecutionTime = new GregorianCalendar();
                Date lastActualExecutionTime = triggerContext.lastActualExecutionTime();
                nextExecutionTime.setTime(lastActualExecutionTime != null ? lastActualExecutionTime : new Date());
                nextExecutionTime.add(Calendar.MILLISECOND, getNewExecutionTime());
                return nextExecutionTime.getTime();
            }
        });
    }

    private int getNewExecutionTime() {
        //Load Your execution time from database or property file
        return 1000;
    }

    @Bean
    public TaskScheduler poolScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
        scheduler.setPoolSize(1);
        scheduler.initialize();
        return scheduler;
    }
}

Scheduler service code:

package io.loadium.resource.service;

import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;

@Service
public class ScheduleTaskService {

    // Task Scheduler
    TaskScheduler scheduler;

    // A map for keeping scheduled tasks
    Map<Integer, ScheduledFuture<?>> jobsMap = new HashMap<>();

    public ScheduleTaskService(TaskScheduler scheduler) {
        this.scheduler = scheduler;
    }


    // Schedule Task to be executed every night at 00 or 12 am
    public void addTaskToScheduler(int id, Runnable task, Date runningDate) {
        ScheduledFuture<?> scheduledTask = scheduler.schedule(task, runningDate);
        jobsMap.put(id, scheduledTask);
    }

    // Remove scheduled task
    public void removeTaskFromScheduler(int id) {
        ScheduledFuture<?> scheduledTask = jobsMap.get(id);
        if (scheduledTask != null) {
            scheduledTask.cancel(true);
            jobsMap.put(id, null);
        }
    }

    // A context refresh event listener
    @EventListener({ContextRefreshedEvent.class})
    void contextRefreshedEvent() {
        // Get all tasks from DB and reschedule them in case of context restarted
    }
}

Sample usage:

// Add a new task with runtime after 10 seconds
scheduleTaskService.addTaskToScheduler(1, () -> System.out.println("my task is running -> 1"), , Date.from(LocalDateTime.now().plusSeconds(10).atZone(ZoneId.systemDefault()).toInstant()));
// Remove scheduled task
scheduleTaskService.removeTaskFromScheduler(1);
Schaffner answered 18/9, 2020 at 9:21 Comment(2)
In short: Just autowire 'TaskScheduler scheduler' and use it.Wylie
IS it possible to explain this, i tried to use a randomizer in the getNewExecutionTime method but it seems it doesnt affect the time when the task is launched.Joyance
C
10

Also you can use this simple approach:

private int refreshTickNumber = 10;
private int tickNumber = 0; 

@Scheduled(fixedDelayString = "${some.rate}")
public void nextStep() {
    if (tickNumber < refreshTickNumber) {
        tickNumber++;
        return;
    }
    else {
        tickNumber = 0;
    }
    // some code
}

refreshTickNumber is fully configurable at runtime and can be used with @Value annotation.

Cagle answered 5/3, 2017 at 8:52 Comment(3)
Not really helpful, introduces too much of overheadLiddie
Not so much actuallyCagle
This works if, like me, what you want to do is dynamically adjust up and down (in intervals of some.rate) how often the // some code in your scheduled task actually runs. But it doesn't actually answer the question which is about dynamically setting the value of fixedRateString. This way the Task is still triggered every some.rate interval, but the business code in the Task only runs when the Tick Count refreshes. The question and the other answers are about adjusting when the task is triggered to directly control when the business code in the Task runs. Tradeoffs.Endosmosis
A
9

you can manage restarting scheduling using TaskScheduler and ScheduledFuture :

@Configuration
@EnableScheduling
@Component
public class CronConfig implements SchedulingConfigurer , SchedulerObjectInterface{

    @Autowired
    private ScheduledFuture<?> future;

     @Autowired
        private TaskScheduler scheduler;

    @Bean
    public SchedulerController schedulerBean() {
        return new SchedulerController();
    }

    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    } 

        @Override
    public void start() {
        future = scheduler.schedule(new Runnable() {
            @Override
            public void run() {
                //System.out.println(JOB + "  Hello World! " + new Date());
                schedulerBean().schedulerJob();
            }
        }, new Trigger() {
            @Override public Date nextExecutionTime(TriggerContext triggerContext) {
                Calendar nextExecutionTime =  new GregorianCalendar();
                Date lastActualExecutionTime = triggerContext.lastActualExecutionTime(); 
           nextExecutionTime.setTime(convertExpresssiontoDate());//you can get the value from wherever you want
                return nextExecutionTime.getTime();
            }
        });

    }


    @Override
    public void stop() {
        future.cancel(true);

    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // TODO Auto-generated method stub
        start();
    }

}

interface for start stop :

public interface SchedulerObjectInterface {    
    void start();
    void stop();
}

now you can stop and start again (restarting) Scheduling using @Autowired SchedulerObjectInterface

Argentic answered 25/10, 2017 at 6:54 Comment(0)
L
2

Simple Spring Boot example restricted to second, minute, and hourly intervals. Intent of this example is to demonstrate conditional handling of two properties, TimeUnit and interval.

Properties:

snapshot.time-unit=SECONDS
snapshot.interval=5

Scheduled method:

@Scheduled(cron = "*/1 * * * * *")
protected void customSnapshotScheduler()
{
    LocalDateTime now = LocalDateTime.now();
    TimeUnit timeUnit = TimeUnit.valueOf(snapshotProperties.getSnapshot().getTimeUnit());
    int interval = snapshotProperties.getSnapshot().getInterval();

    if (TimeUnit.SECONDS == timeUnit
            && now.getSecond() % interval == 0)
    {
        this.camService.writeSnapshot(webcam.getImage());
    }

    if (TimeUnit.MINUTES == timeUnit
            && now.getMinute() % interval == 0)
    {
        this.camService.writeSnapshot(webcam.getImage());
    }

    if (TimeUnit.HOURS == timeUnit
            && now.getHour() % interval == 0)
    {
        this.camService.writeSnapshot(webcam.getImage());
    }
}
Leigha answered 1/9, 2019 at 14:11 Comment(0)
C
1

See How we are calling "#{@getIntervalTime}" in MySchedularService Class and taking the time interval for next scheduled call from @Bean annotate class

Main Class

package com;

import java.util.Calendar;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SbootSchedularApplication {

    public static void main(String[] args) {
        SpringApplication.run(SbootSchedularApplication.class, args);
    }
    
    @Value("${schedular3Timing}")
    String schedular3Timing;
    
    @Bean
    public String getIntervalTime() 
    {
        long startSchedulerAfterMiliSec = setSchedule(schedular3Timing);

        return ""+startSchedulerAfterMiliSec;
    }
    
    public long setSchedule(String key) 
    {
        int hour = Integer.parseInt(key.substring(0, key.indexOf(":")));
        int min = Integer.parseInt(key.substring(key.indexOf(":") + 1));

        Calendar schedulerCal = Calendar.getInstance();
        schedulerCal.set(Calendar.HOUR, hour);
        schedulerCal.set(Calendar.MINUTE, min);
        schedulerCal.set(Calendar.SECOND, 0);
        
        Calendar localCal = Calendar.getInstance();
        Long currentTimeInMilliSec = localCal.getTime().getTime();
        String currentDayTime = localCal.getTime().toString();

        if (schedulerCal.getTime().getTime() < currentTimeInMilliSec) {         // Means calculating time reference from time 00:00, if current time is 1000 mili-sec and scheduled time is 800 mili-sec -> then that time is already happened, so better add one more day in that same timing.
            schedulerCal.add(Calendar.DATE, 1);         // add 1 day more in the Schedular, if scheduled-MiliSec is less than the current-MiliSec.
        }

        long scheduledTimeInMilliSec = schedulerCal.getTime().getTime();
        String scheduledTime = schedulerCal.getTime().toString();
        System.out.println("** Scheduled start time for the task    : " + scheduledTime + " *** " + scheduledTimeInMilliSec);
        System.out.println("** Current time of the day      : " + currentDayTime + " *** " + currentTimeInMilliSec);

        long startScheduler = scheduledTimeInMilliSec - currentTimeInMilliSec;      // eg: scheduledTime(5pm) - currentTime(3pm) = (2hr)startSchedulerAfter
        return startScheduler;

    }

}


MySchedularService Class : See the JOB-3

package com.service;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class MySchedularService {

    private static final Logger logger = LoggerFactory.getLogger(MySchedularService.class);

//  @Scheduled(fixedRate = 2000, initialDelay = 5000L)
    @Scheduled(fixedRateString = "${schedular1.fixedRateInMS}", initialDelay = 1000L)
    public void job() {
        logger.info("Job1 Run Time : " + new Date());
    }
    
//  @Scheduled(fixedRateString = "${schedular2.fixedRateInMS}", initialDelay = 5000L)
//  public void job2() {
//      logger.info("Job2 Run Time : " + new Date());
//  }

    @Scheduled(fixedRate = 10000 , initialDelayString = "#{@getIntervalTime}")      // we can change the fixedRate = 86400000L miliseconds (i.e, one day interval)    
    public void job3() {
        logger.info("**Job2 Run Time : " + new Date());
    }
    
    

}


Application.properties File

spring.task.scheduling.pool.size=10
schedular1.fixedRateInMS=3000
schedular2.fixedRateInMS=10000
schedular3Timing=01:07

Calycle answered 13/4, 2022 at 20:9 Comment(0)
E
0

i created dynamic tasks using ThreadPoolTaskScheduler from org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler and scheduleWithFixedDelay method. i also added a redisson lock inorder to prevent duplicated jobs in distributed environment here is my code:

public class TaskRunnerService {

    private final ThreadPoolTaskScheduler taskScheduler;
    private final RedissonClient redissonClient;


    public TaskRunnerService(ThreadPoolTaskScheduler taskScheduler, RedissonClient redissonClient) {
        this.taskScheduler = taskScheduler;
        this.redissonClient = redissonClient;
    }

    @PostConstruct
    public void runTasks() {
        List<TaskDTO> taskDTOS = TaskHolder.createTasks();
        for (TaskDTO taskDTO : taskDTOS) {
            RLock lock = this.redissonClient.getFairLock("LoadAndRunScheduleService-" + taskDTO.getId());

            if (lock.tryLock()) {
                try {
                    this.taskScheduler.scheduleWithFixedDelay(() -> {
                        System.out.println(" running task " + taskDTO.getId() + " with delay " + taskDTO.getDelay() + " at " + new Date());
                    }, taskDTO.getDelay() * 1000L);
                }finally {
                    lock.unlock();
                }
            }
        }
    }

}

i created a TaskDTO class to be able to get delay at runtime:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    public class TaskDTO {
    
        private int id;
        private int delay;
    }

and configuration class is:

    @Configuration
    public class AppConfig {
    
        @Bean
        ThreadPoolTaskScheduler taskScheduler(){
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
            scheduler.setPoolSize(2);
            scheduler.initialize();
            return scheduler;
        }
    
    }
Em answered 30/10, 2021 at 7:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.