5

I have a method that I wish to run once using Spring and it needs to run on a given java.util.Date (or LocalDateTime alternatively). I am planning to persist all of the dates that the method should execute to a data source. It should run asynchronously.

One way is to check the DB every day for a date and execute the method if the date has passed and hasn't been executed. Is there a better way?

I know that Spring offers a ThreadPoolTaskScheduler and a ThreadPoolTaskExecutor. I am looking at ScheduledFuture schedule(Runnable task, Date startTime) from the TaskScheduler interface. Would I need to create a Runnable Spring managed bean just to call my method? Or is there a simpler annotation that would do this? An example would really help.

(Looked here too.)

Community
  • 1
  • 1
riddle_me_this
  • 8,575
  • 10
  • 55
  • 80

2 Answers2

3

By externalizing the scheduled date (to a database), the typical scheduling practices (i.e. cron based, or fixed scheduling) no longer apply. Given a target Date, you can schedule the task accurately as follows:

Date now = new Date();
Date next = ... get next date from external source ...
long delay = next.getTime() - now.getTime();
scheduler.schedule(Runnable task, delay, TimeUnit.MILLISECONDS);

What remains is to create an efficient approach to dispatching each new task. The following has a TaskDispatcher thread, which schedules each Task based on the next java.util.Date (which you read from a database). There is no need to check daily; this approach is flexible enough to work with any scheduling scenario stored in the database.

To follow is working code to illustrate the approach.

The example Task used; in this case just sleeps for a fixed time. When the task is complete, the TaskDispatcher is signaled through a CountDownLatch.

public class Task implements Runnable {

    private final CountDownLatch completion;
    public Task(CountDownLatch completion) {
        this.completion = completion;
    }

    @Override
    public void run() {
        System.out.println("Doing task");
        try {
            Thread.sleep(60*1000);  // Simulate the job taking 60 seconds
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        completion.countDown();     // Signal that the job is complete
    }

}

The dispatcher is responsible for reading the database for the next scheduled Date, launching a ScheduledFuture runnable, and waiting for the task to complete.

public class TaskDispatcher implements Runnable {

    private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private boolean isInterrupted = false;

    @Override
    public void run() {

        while (!isInterrupted) {

            Date now = new Date();

            System.out.println("Reading database for next date");
            Date next = ... read next data from database ...

            //Date next = new Date();   // Used as test
            //next.setTime(now.getTime()+10*1000); // Used as test

            long delay = next.getTime() - now.getTime();
            System.out.println("Scheduling next task with delay="+delay);

            CountDownLatch latch = new CountDownLatch(1);
            ScheduledFuture<?> countdown = scheduler.schedule(new Task(latch), delay, TimeUnit.MILLISECONDS);

            try {
                System.out.println("Blocking until the current job has completed");
                latch.await();
            } catch (InterruptedException e) {
                System.out.println("Thread has been requested to stop");
                isInterrupted = true;
            }
            if (!isInterrupted)
                System.out.println("Job has completed normally");
        }

        scheduler.shutdown();

    }

}

The TaskDispatcher was started as follows (using Spring Boot) - start the thread as you normally do with Spring:

@Bean
public TaskExecutor taskExecutor() {
    return new SimpleAsyncTaskExecutor(); // Or use another one of your liking
}

@Bean
public CommandLineRunner schedulingRunner(TaskExecutor executor) {
    return new CommandLineRunner() {
        public void run(String... args) throws Exception {
            executor.execute(new TaskDispatcher());
        }
    };
}

Let me know if this approach will work for your use case.

Ian Mc
  • 5,656
  • 4
  • 18
  • 25
0

Take a look at the @Scheduled annotation. It may accomplish what you're looking for.

@Scheduled(cron="*/5 * * * * MON-FRI")
public void scheduledDateWork() {
    Date date = new Date(); //or use DAO call to look up date in database

    executeLogic(date);
}

Cron Expression Examples from another answer:

"0 0 * * * *" = the top of every hour of every day.
"*/10 * * * * *" = every ten seconds.
"0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
"0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30 and 10 o'clock every day.
"0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
"0 0 0 25 12 ?" = every Christmas Day at midnight
Dean Clark
  • 3,770
  • 1
  • 11
  • 26
  • Perhaps my question wasn't clear. I am looking to executeLogic once on a given date. @Scheduled doesn't seem to take a java.util.Date as a parameter. Your suggestion is basically doing what I have written in my second paragraph, only adding another schedule to it. – riddle_me_this May 23 '17 at 23:51
  • You're going to want to make this statefull, so you can determine whether or not an your `executeLogic(Date date)` actually ran or not. From my perspective, the cleanest way to do that would be to have a table with Date and Status columns. Then, I'd periodically (hence the `@Scheduled` method) check for a date that has just passed and needed to be executed. Then I'd update the status after the action had been performed to avoid duplicate executions. – Dean Clark May 24 '17 at 13:39