How to have an asynchronous and non-concurrent scheduler in Spring?

Viewed 466

I have in the main class that starts the app:

@SpringBootApplication
@EnableAsync
public class ExperianRequestBotApplication extends RefApplication {

    public ExperianRequestBotApplication() throws RefException {
        super();
    }

    public static void main(String[] args) throws RefException {
        try {
            new ExperianRequestBotApplication().start(args);
        } catch (Exception e) {
            System.out.println(" ------- OFFLINE ------- ");
            System.out.println("La aplicación no esta disponible por :" + e);
        }

    }
}

and a scheduler

@Component
public class ScheduledTaskSincronizarContactos {

    @Autowired
    private ExperianRequestBotService experianRequestBotService;

    private final static Logger LOG = LoggerFactory.getLogger(ScheduledTaskSincronizarContactos.class);

    // Método Shedule encargado de sincronizar los usuarios modificados con Experian 
    @Async
    @Scheduled(cron = "0 */15 * ? * *")
    public void SincronizarContactos() throws Exception {

I want to prevent the scheduler from being launched again if the internal process takes more than 15 minutes to start the task again.

I can't find the way. I have tried implementing @DisallowConcurrentExecution with the quartz library but it is still concurrent.

Any ideas?

5 Answers

By default, all scheduled tasks run on a single thread in Spring boot unless you define a thread pool implementation yourself.

Here is a link for more reference that came in handy for me once: https://crmepham.github.io/spring-boot-multi-thread-scheduling/

I'm surprised to see that your having the issue with the tasks running concurrently in the first place.

How i'm currently doing scheduled tasks whenever i need to, i place @EnableScheduling annotation on the main class, and use fixed rate and initial delay.

Maybe you could use fixed rate and initial delay instead of cron to achieve what you are trying to do.

I've faced a similar issue with concurrency while using spring scheduling. We had a lot of jobs all running on the same service and interfering with each other. We switched to using Quartz Scheduling - felt simpler than the Spring multithreaded scheduler with a bunch of other features that we wanted. This repo was really helpful. https://gitlab.com/johnjvester/jpa-spec-with-quartz/-/blob/master/src/main/java/com/gitlab/johnjvester/jpaspec/config/QuartzConfig.java

Quartz scheduling also has the advantage of being persistent - when it starts up, it will trigger all the missed jobs. One can also alter cron statements programmatically. This might be an overkill for your use case but it is worth a look. :) Also, what Wasif said - use delays to define when the job should be run vs a cron expression and you're guaranteed a win even with Spring!

Remove @Async from the method. @Async causes the method to be called on a separate executor.

The problem is that your job might take longer than the time planned for its run and the time in between runs. One idea would be to store somewhere the last start time and the last end time of the cron job. If the last end time is earlier than the last start time, then at the next run, ignore everything. That is, you need something like this:

@SpringBootApplication
@EnableAsync
public class ExperianRequestBotApplication extends RefApplication {

    public ExperianRequestBotApplication() throws RefException {
        super();
    }

    public static void main(String[] args) throws RefException {
        try {
            if (ExperianRequestBotApplication.shouldRun()) {
                new ExperianRequestBotApplication().start(args);
            }
        } catch (Exception e) {
            System.out.println(" ------- OFFLINE ------- ");
            System.out.println("La aplicación no esta disponible por :" + e);
        }

    }
}

Note that a conditional with a boolean validator is wrapped around the bulk of your job. That shouldRun should check whether the last start date is smaller than the last end date. You will also need to make sure that your job manages these values. It is true that with this approach your job is still being executed every 15 minutes, but, if the last run has not finished yet, then the current run will be extremely quick, it will just check whether the job is still running and if so, then it will avoid doing anything. Otherwise it will properly execute the job. This is simple to implement, protects you from ending up in running the same long job multiple times simultaneously or in sequence and will provide some breathing space for the system to perform other operations.

Thanks all for your answers, finally we've decide to use quartz library. The easiest way, using @DisallowConcurrentExecution we can have async jobs not executing concurrently.

Related