I have a scheduled job configured in an XML file:
<task:scheduled-tasks scheduler="taskScheduler">
<task:scheduled ref="scheduledJobs" method="doSomething" cron="30 * * * * ?"/>
</task:scheduled-tasks>
<bean id="scheduledJobs" class="com.xxx.ScheduledJobs"/>
<task:scheduler id="taskScheduler" pool-size="2" />
Is it possible to query the cron expression (to display to the user) and update it (not necessarily in the XML file but in the bean instance) with a new cron expression?
Or is there a better way of achieving this type of functionality?
I believe the answer is this is not possible using Spring/JDK scheduling but is possible if you use Quartz scheduling instead:
There is another thread covering a similar area:
Rescheduling a CronTriggerBean dynamically with same job details in Spring
You can achieve it in a programmatic way, shortened version:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.support.CronTrigger;
public class ChangelogDeleterTask implements InitializingBean, Runnable {
#Autowired
private TaskScheduler scheduler;
private ScheduledFuture<?> scheduledTask;
private CronTrigger trigger;
private Date nextExecutionTime;
#Override
public void afterPropertiesSet() throws Exception {
schedule("0/5 * * * * *");
}
#Override
public void run() {/*do scheduled job*/}
public synchronized void schedule(String cronExpression) {
scheduledTask.cancel(false);
trigger = new MyCronTrigger(cronExpression);
scheduledTask = scheduler.schedule(this, trigger);
}
String getNextExecutionTime() {
return nextExecutionTime.toString();
}
String getExpression() {
return trigger.getExpression();
}
/**
* Class allows to remember nextExecutionTime.
*/
private class MyCronTrigger extends CronTrigger {
public ChangelogCronTrigger(String expression) {
super(expression);
}
#Override
public Date nextExecutionTime(TriggerContext triggerContext) {
Date date = super.nextExecutionTime(triggerContext);
nextExecutionTime = new Date(date.getTime());
return date;
}
}
}
Related
I like to have an implementation of one #Scheduled job using different configuration properties of .ymlfile.
Means in my yaml file I describe the cron expression as a list:
job:
schedules:
- 10 * * * * *
- 20 * * * * *
I read those values out using Configuration and created a #Bean named scheduled:
#Configuration
#ConfigurationProperties(prefix="job", locations = "classpath:cronjob.yml")
public class CronConfig {
private List<String> schedules;
#Bean
public List<String> schedules() {
return this.schedules;
}
public List<String> getSchedules() {
return schedules;
}
public void setSchedules(List<String> schedules) {
this.schedules = schedules;
}
}
In my Job class I want to start the execution of one method but for both of the schedules in my configuration.
#Scheduled(cron = "#{#schedules}")
public String execute() {
System.out.println(converterService.test());
return "success";
}
With this solution the application creates an error: (more or less clear)
Encountered invalid #Scheduled method 'execute': Cron expression must consist of 6 fields (found 12 in "[10 * * * * *, 20 * * * * *]")
Is there a way to configure the same scheduled job method with multiple declarations of cron expressions?
EDIT 1
After some try I just used a second annotation on the executer method.
#Scheduled(cron = "#{#schedules[0]}")
#Scheduled(cron = "#{#schedules[1]}")
public String execute() {
System.out.println(converterService.test());
return "success";
}
This solution works but is not really dynamic. Is there also a way to make this dynamic?
(edit since I found a way to perform this)
You can actually do this. Below I'm showcasing a working example:
cronjob.yaml
job:
schedules:
- 10 * * * * *
- 20 * * * * *
the actual task to perform MyTask:
package hello;
import org.springframework.stereotype.Component;
#Component
public class MyTask implements Runnable {
#Override
public void run() {
//complicated stuff
}
}
Your CronConfig as is:
package hello;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
#Configuration
#ConfigurationProperties(prefix="job", locations = "classpath:cronjob.yml")
public class CronConfig {
private List<String> schedules;
#Bean
public List<String> schedules() {
return this.schedules;
}
public List<String> getSchedules() {
return schedules;
}
public void setSchedules(List<String> schedules) {
this.schedules = schedules;
}
}
The ScheduledTask bean that is responsible to schedule all crons:
package hello;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
#Component
public class ScheduledTasks {
#Autowired
private TaskScheduler taskScheduler;
#Autowired
private CronConfig cronConfig;
#Autowired
private MyTask myTask;
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
public void scheduleAllCrons() {
cronConfig.getSchedules().forEach( cron -> taskScheduler.schedule(myTask, new CronTrigger(cron)) );
}
}
The context/main class Application:
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
#SpringBootApplication
#EnableScheduling
#EnableAsync
public class Application {
#Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler();
}
public static void main(String[] args) throws Exception {
ApplicationContext ctx = SpringApplication.run(Application.class);
ScheduledTasks scheduledTasks = ctx.getBean(ScheduledTasks.class);
scheduledTasks.scheduleAllCrons();
}
}
application.yaml:
crontab:
submitSubtask: "0 * * * * *"
updateBacktrace: "2/30 * * * * *"
java code:
#EnableScheduling
public class Demo{
#Scheduled(cron = "${crontab.updateBacktrace}")
private void updateBacktrace() {
...
}
#Scheduled(cron = "${crontab.submitSubtask}")
private void submitSubtask() {
...
}
}
it works for springboot 2.3.7.
A trick related to it:
while defining the corn job timing, attribute name should be in lower case
for example: if it is in Camel case, spring did not kick the job :(
application.yml:
common:
scheduler:
feedeErrorLogCleanUp: 0 0/5 * ? * *
WHEREAS BELOW STUFF WORKS
common:
scheduler:
feedeerrorlogcleanUp: 0 0/5 * ? * *
I have a simple scheduled task that was created using the #Scheduled annotation. Something like this -
public void doSomething() {
// My code
}
I also have a blue-green upgrade scenario where this scheduled task may potentially run in both the blue and green clusters. And given this scheduled task modifies the DB, there is a possibility that one of the nodes might overwrite the data from the other node - or worst case race conditions.
I want to pause all the scheduled tasks on the green cluster until it is ready to accept traffic. I already have the code wired to listen to changes to the application state.
I explored a few options -
Simply adding a boolean at the start of the task.
Adding a decorator to the ThreadPoolTaskScheduler.
The problem with these approaches is they skip the task for that particular schedule and not pause it. So, if the task is skipped, say at T, and the application becomes immediately, the application will have to wait T+30mins to refresh data.
Is there a way in springboot to pause schedulers and resume immediately?
It looks like natively spring does not have the ability to pause scheduled tasks. I tend to use Quartz Scheduler when running schedule tasks with spring.
In order to do that we have to do a couple configurations.
First we have to setup our context aware component:
#Component
public final class ApplicationContextHolder extends SpringBeanJobFactory implements ApplicationContextAware {
private static ApplicationContext context;
private transient AutowireCapableBeanFactory beanFactory;
#Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
if (context == null) {
beanFactory = ctx.getAutowireCapableBeanFactory();
context = ctx;
}
}
#Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
/**
* Get the current application context
* #return the current context
*/
public static ApplicationContext getContext() {
return context;
}
}
Then our configuration:
#Configuration
public class QuartzSchedulerConfig {
#Autowired
private ApplicationContext applicationContext;
/**
* Create the job factory bean
* #return Job factory bean
*/
#Bean
public JobFactory jobFactory() {
ApplicationContextHolder jobFactory = new ApplicationContextHolder();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
/**
* Create the Scheduler Factory bean
* #return scheduler factory object
*/
#Bean
public SchedulerFactoryBean schedulerFactory() {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setAutoStartup(true);
factory.setSchedulerName("Scheduler");
factory.setOverwriteExistingJobs(true);
factory.setJobFactory(jobFactory());
return factory;
}
}
Then our actual service:
#Service
public class SchedulerService {
#Autowired
private SchedulerFactoryBean schedulerFactory;
private Scheduler scheduler;
private ScheduledExecutorService executor;
/**
* Initialize the scheduler service
*/
#PostConstruct
private void init() {
scheduler = schedulerFactory.getScheduler();
executor = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
}
...//Other Scheduling tasks can go here
public void pauseJob(String triggerName, String groupName) {
TriggerKey tk = new TriggerKey(triggerName, groupName);
scheduler.pauseJob(tk);
}
}
Quartz Scheduling gives a lot of flexibility when it comes to scheduling tasks
http://www.quartz-scheduler.org/overview/
Customize the task scheduler to wait until it finishes the job and allow spring boot to do the graceful shutdown
#Bean
TaskSchedulerCustomizer taskSchedulerCustomizer() {
return taskScheduler -> {
taskScheduler.setAwaitTerminationSeconds(60);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
};
}
another opinion
Use Shedlock to lock the scheduler for the given amount of time(upgradable time). It also solves the scheduler running on all/multiple instances instead of one instance.
I'm trying to use #Scheduled method to process some regular work (every second). Body of this method can process during more than one second and I see that if this happens next execution is not started. Does Spring support it or I should change it to any other concurrent solution?
I have tried to change Scheduler to ConcurrentTaskScheduler, but looks like it helps only if we have few schedules methods.
#Service
public class MainService {
#Scheduled(cron = "* * * * * *")
public void doSomething() {
//some logic which can takes more than 1 second
}
}
#Configuration
public class SchedulingConfig implements SchedulingConfigurer {
#Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
#Bean
public Executor taskExecutor() {
return new ConcurrentTaskScheduler(
Executors.newScheduledThreadPool(100));
}
}
Once the first execution is taking extra time the second execution will not be started. Otherwise, all works fine. How can I set up parallel execution of one scheduled method?
You can introduce an async component so it does not take 1 second https://www.baeldung.com/spring-async
#Service
public class MainService {
#Autowired
private SomethingService somethingService;
#Scheduled(cron = "* * * * * *")
public void doSomething() {
somethingService.doSomething();
}
}
//Introduce an async component so it does not take 1 second. runs doSomething() in a separate thread
#Component
public class SomethingService {
#Async
public void doSomething() {
//some logic which can takes more than 1 second
}
}
I have e method which is annotated with #scheduled. Its a fairly long running method. I need to run the same method in parallel using a threadpool. Is it possible? The code is:
#Scheduled(fixedRate=100)
public void run() {
Job job = QUEUE.take();
job.run(); //Takes a long time
}
The QUEUE has many jobs and I would like to run them in parallel using Spring's Scheduled annotation.
I think you can change the Job.run method to an Asynchronous methods by use spring's "#Async",or another way you can create yourself threadpool to do the Job.
/**
* Created by roman.luo on 2016/9/14.
*/
#Component
#Scope("prototype")
public class JobDelegate implements Job {
private Job job;
public JobDelegate(Job job) {
this.job = job;
}
#Async
public void run(){
job.run();
}
}
/**
* Created by roman.luo on 2016/9/14.
*/
#Component
public class Sceduled extends ApplicationObjectSupport{
#Scheduled(fixedRate = 100)
public void run(){
Job job = QUEUE.take();
Job jobDelegate = getApplicationContext().getBean(JobDelegate.class,job);
jobDelegate.run();
}
}
remember config the spring xml file:
<task:executor id="myexecutor" pool-size="5" />
<task:annotation-driven executor="myexecutor"/>
I want to be able to run the SAME scheduled job in Spring.
After searching on the Internet, I figured out how to run multiple different jobs at the same time.
I have a #Service annotated class which has only one method, annotated with #Scheduled. I want to have multiple instances of this job running at the same time.
I am not using Quartz or Spring Batch( I have seen a lot of examples with Spring Batch).
The documentation doesn't clearly say if this can be achieved.
Yes, it can be easily achieved, but not with #Scheduled annotation.
How? Let me first explain how Spring works.
Spring from every method annotated with #Scheduled creates a new Runnable object and then schedules it for execution to the TaskScheduler (ThreadPoolTaskScheduler to be precise).
To see the exact code look at ScheduledAnnotationBeanPostProcessor.processScheduled().
So to fulfill your requirement: have multiple instances of the same job, but without using Quartz or Spring Batch we need to abandon #Scheduled annotation and do something a bit different than what the ScheduledAnnotationBeanPostProcessor does by default.
#Configuration
public class SpringConfig {
#Autowired
private TaskScheduler scheduler;
#Autowired
private YourServiceAnnotatedClass service;
#PostConstruct
public void startJobs() {
int numOfJobInstances = 3;
List<ImportantJob> jobs = IntStream.range(0, numOfJobInstances)
.mapToObj(i -> new ImportantJob("job" + i, service))
.collect(Collectors.toList());
jobs.forEach(this::schedule);
}
private void schedule(ImportantJob job) {
scheduler.schedule(job, new CronTrigger("*/5 * * * * *"));
// Above CronTrigger with 5 seconds was used, but feel free to use other variants, e.g.
// scheduler.scheduleAtFixedRate()
// scheduler.scheduleWithFixedDelay()
}
#Bean(destroyMethod = "shutdown")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(3); // Adjust it to your needs
return taskScheduler;
}
}
class ImportantJob implements Runnable {
private final String name;
private final YourServiceAnnotatedClass service;
public ImportantJob(String name, YourServiceAnnotatedClass service) {
this.name = name;
this.service = service;
}
#Override
public void run() {
service.doSth();
}
}
As you can see, although #Scheduled is useful and simple, it is not very flexible. But with some effort you can gain much more control over scheduled tasks.