Problems using spring @Scheduled

主宰稳场 提交于 2020-07-20 08:25:02

问题


I have three methods on my project annotated with @Scheduled, being one of them a cron expression and the other two are fixed delay. The annotations look like:

Method 1:

@Scheduled(fixedDelay = 20000)
@Async
protected void checkBrokenEngines() {

Method 2:

@Scheduled(fixedRate = 20000)
@Async
public void checkAvailableTasks() throws Exception {

Method 3:

@Scheduled(cron = "0 0 2 * * ?")
protected void deleteOldData() {

Previously I had a problem that when the checkBrokenEngines and checkAvailableTasks methods were slow to be executed, the next executions did not happen until the previous one ended. Reading the documentation and some topics here from the StackOverflow, I saw that my project had some wrong pool size settings and the methods were not annotated with async. (Async was for the next execution start even if the old one doesn't end, as this does not cause any problems in my application)

Now another problem has come up, which is my question:

When the deleteOldData() method is executed, neither of the other two methods will run until it finishes. After seeing that this method was blocking the execution of the other two, I annotated the method as async and after that, even if this method takes time to be executed, the other two are always invoked correctly within the stipulated time. Why? In my understanding, this should not happen since those methods are noted with async and the pool has enough space to execute them.

PS: deleteOldData() cannot be async. It must start just after the previous execution has been completed.

EDIT 1:

Async executor config:

@Override    
public AsyncTaskExecutor getAsyncExecutor() {
    log.debug("Creating Async Task Executor");
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(50);
    executor.setMaxPoolSize(50);
    executor.setQueueCapacity(10000);        
    return new ExceptionHandlingAsyncTaskExecutor(executor);
}

回答1:


All @Scheduled marked invocations will use the default single thread executor to schedule tasks (async or otherwise).

All @Async tasks are handed off to different aysnc threadpool executor for executions with AOP interceptor.

I think your confusion comes from the fact async methods return immediately but when deleteOldData is run, it is running synchronously as there is only one thread, it blocks the execution of any other scheduled tasks.

Because you have default threadpool (single thread) for the scheduled tasks they are scheduled one after the another.

The other methods annotated with @Async they execute even if it finishes or not. In some cases, I have two methods of executing at the same time. But when deleteOldData is executing, the async methods stop to run until it finishes. This what I'm not understanding, sorry :/ –

This is different from scheduling - This is where your async executor comes into play and they are run concurrently.

You can fix this in one of two ways:

You can use spring.task.scheduling.pool.size=10 in application properties to set the pool size of task scheduler.

Alternatively, use different tasks schedulers. Keep using default scheduler for @Scheduled task and configure something like below for async tasks ( remove scheduled annotation )

There is an enhancement requested to pass task scheduler to the @Scheduled annotation until then you have to schedule tasks manually.

Register a new task scheduler for async invocations and schedule the methods in the post construct stage. Something like

Configuration

@Bean("asyncTaskScheduler")
public TaskScheduler asyncTaskScheduler() {
  return new ThreadPoolTaskScheduler();
}

Services

@Autowired
private TaskScheduler asyncTaskScheduler;

@PostConstruct
void schedule() {
  asyncTaskScheduler.scheduleAtFixedRate(this::checkAvailableTasks, 20000L);
  asyncTaskScheduler.scheduleAtFixedDelay(this::checkBrokenEngines, 20000L);
}

@Async
public void checkBrokenEngines() {...}

@Async
public void checkAvailableTasks() throws Exception {...}



回答2:


Scheduled tasks are processed by the ThreadPoolTaskScheduler, which has a default pool size of 1. Only when they are annotated as @Async, execution is passed into the AsyncTaskExecutor, which for you configured a dedicated executor with a larger pool size.

To configure the ThreadPoolTaskScheduler within a @Configuration class:

@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
    scheduler.setPoolSize(50);
    return scheduler ;
}



回答3:


This happens because @Async task is submitted by default Scheduling executor and its size is 1 by default.

I have modified submit method of the AsyncTaskExecutor executor:

  @Bean
    AsyncConfigurer asyncConfigurer() {
        return new AsyncConfigurer() {
            @Override
            public AsyncTaskExecutor getAsyncExecutor() {
                ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(){
                    @Override
                    public <T> Future<T> submit(Callable<T> task) {
                        System.out.println("async task was started by thread -- "+Thread.currentThread().getName());
                        return super.submit(task);
                    }
                };
                executor.setThreadNamePrefix("custom-async-exec");
                executor.setCorePoolSize(2);
                executor.setQueueCapacity(100);
                executor.initialize();
                return executor;
            }
        };
    } 

And output.

async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1
async task was started by thread -- scheduling-1

So as there is 1 thread into default Shedulers pool scheduling-1 and whenever its busy can't start/submit new @Async tasks. define @Bean ThreadPoolTaskExecutor or add spring.task.scheduling.pool.size=x.

EDIT

Here is simple test for visualization:

@Component
    public static class Jobs{
        @Scheduled(fixedDelay = 1500)
        @Async
        public void job1(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        @Scheduled(fixedDelay = 1500)
        @Async
        public void job2(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        @Scheduled(initialDelay = 10000, fixedDelay = 5000)
        public void blocking(){
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

visualization from visualvm

Red 'arrows' shows the point blocking() job kicks in. and while scheduling-1 Thread blocked on this there is no way to submit job1() and job2() also




回答4:


Just "1 more aspect": Cite: https://www.baeldung.com/spring-async#the-async-annotation

First – let's go over the rules - @Async has two limitations:

  • it must be applied to public methods only

  • self-invocation – calling the async method from within the same class – won't work

side note: <- same rules apply to @Transactional, ... annotation(s).

The reasons are simple – the method needs to be public so that it can be proxied. And self-invocation doesn't work because it bypasses the proxy and calls the underlying method directly.

@Scheduled protected void ... () is ok on the other hand, since (again, thx to baeldung):

The simple rules that need to be followed to annotate a method with @Scheduled are:

  • a method should have void return type
  • a method should not accept any parameters

Please check, whether any issues/misunderstanding exist after applying to @Async rules and considering the other [correct - very nice] answers. (baeldung and @Nonika use public methods!)

The Baeldung @Scheduled article, offers also this:

... Note that scheduled tasks don't run in parallel by default. So even if we used fixedRate, the next task won't be invoked until the previous one is done.

If we want to support parallel behavior in scheduled tasks, we need to add the @Async annotation:

@EnableAsync
public class ScheduledFixedRateExample {
  @Async
  @Scheduled(fixedRate = 1000)
  public void scheduleFixedRateTaskAsync() throws InterruptedException {
    System.out.println(
      "Fixed rate task async - " + System.currentTimeMillis() / 1000);
    Thread.sleep(2000);
  }
}

Now this asynchronous task will be invoked each second, even if the previous task isn't done.

my note: Of course the asynchronous jobs can block each other on some "common resource" (file/object/db/lock/semaphore/etc.) ...which could lead to problems/"full pools"/deadlocks... you should clear it out, before deciding for @Async.

And, since no further word about configuration...i "assume/hope", it works "out of the box"/as documented.

So, still few information cannot be seen from here:

  • self-invocation?
  • "synchron inside"?


来源:https://stackoverflow.com/questions/60617040/problems-using-spring-scheduled

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!