How to run certain task every day at a particular time using ScheduledExecutorService?

158,116

Solution 1

As with the present java SE 8 release with it's excellent date time API with java.time these kind of calculation can be done more easily instead of using java.util.Calendar and java.util.Date.

Now as a sample example for scheduling a task with your use case:

ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));
ZonedDateTime nextRun = now.withHour(5).withMinute(0).withSecond(0);
if(now.compareTo(nextRun) > 0)
    nextRun = nextRun.plusDays(1);

Duration duration = Duration.between(now, nextRun);
long initialDelay = duration.getSeconds();

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);            
scheduler.scheduleAtFixedRate(new MyRunnableTask(),
    initialDelay,
    TimeUnit.DAYS.toSeconds(1),
    TimeUnit.SECONDS);

The initialDelay is computed to ask the scheduler to delay the execution in TimeUnit.SECONDS. Time difference issues with unit milliseconds and below seems to be negligible for this use case. But you can still make use of duration.toMillis() and TimeUnit.MILLISECONDS for handling the scheduling computaions in milliseconds.

And also TimerTask is better for this or ScheduledExecutorService?

NO: ScheduledExecutorService seemingly better than TimerTask. StackOverflow has already an answer for you.

From @PaddyD,

You still have the issue whereby you need to restart this twice a year if you want it to run at the right local time. scheduleAtFixedRate won't cut it unless you are happy with the same UTC time all year.

As it is true and @PaddyD already has given a workaround(+1 to him), I am providing a working example with Java8 date time API with ScheduledExecutorService. Using daemon thread is dangerous

class MyTaskExecutor
{
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    MyTask myTask;
    volatile boolean isStopIssued;

    public MyTaskExecutor(MyTask myTask$) 
    {
        myTask = myTask$;
        
    }
    
    public void startExecutionAt(int targetHour, int targetMin, int targetSec)
    {
        Runnable taskWrapper = new Runnable(){

            @Override
            public void run() 
            {
                myTask.execute();
                startExecutionAt(targetHour, targetMin, targetSec);
            }
            
        };
        long delay = computeNextDelay(targetHour, targetMin, targetSec);
        executorService.schedule(taskWrapper, delay, TimeUnit.SECONDS);
    }

    private long computeNextDelay(int targetHour, int targetMin, int targetSec) 
    {
        LocalDateTime localNow = LocalDateTime.now();
        ZoneId currentZone = ZoneId.systemDefault();
        ZonedDateTime zonedNow = ZonedDateTime.of(localNow, currentZone);
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec);
        if(zonedNow.compareTo(zonedNextTarget) > 0)
            zonedNextTarget = zonedNextTarget.plusDays(1);
        
        Duration duration = Duration.between(zonedNow, zonedNextTarget);
        return duration.getSeconds();
    }
    
    public void stop()
    {
        executorService.shutdown();
        try {
            executorService.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException ex) {
            Logger.getLogger(MyTaskExecutor.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Note:

  • MyTask is an interface with function execute.
  • While stopping ScheduledExecutorService, Always use awaitTermination after invoking shutdown on it: There's always a likelihood your task is stuck / deadlocking and the user would wait forever.

The previous example I gave with Calender was just an idea which I did mention, I avoided exact time calculation and Daylight saving issues. Updated the solution on per the complain of @PaddyD

Solution 2

In Java 8:

scheduler = Executors.newScheduledThreadPool(1);

//Change here for the hour you want ----------------------------------.at()       
Long midnight=LocalDateTime.now().until(LocalDate.now().plusDays(1).atStartOfDay(), ChronoUnit.MINUTES);
scheduler.scheduleAtFixedRate(this, midnight, 1440, TimeUnit.MINUTES);

Solution 3

If you don't have the luxury of being able to use Java 8, the following will do what you need:

public class DailyRunnerDaemon
{
   private final Runnable dailyTask;
   private final int hour;
   private final int minute;
   private final int second;
   private final String runThreadName;

   public DailyRunnerDaemon(Calendar timeOfDay, Runnable dailyTask, String runThreadName)
   {
      this.dailyTask = dailyTask;
      this.hour = timeOfDay.get(Calendar.HOUR_OF_DAY);
      this.minute = timeOfDay.get(Calendar.MINUTE);
      this.second = timeOfDay.get(Calendar.SECOND);
      this.runThreadName = runThreadName;
   }

   public void start()
   {
      startTimer();
   }

   private void startTimer();
   {
      new Timer(runThreadName, true).schedule(new TimerTask()
      {
         @Override
         public void run()
         {
            dailyTask.run();
            startTimer();
         }
      }, getNextRunTime());
   }


   private Date getNextRunTime()
   {
      Calendar startTime = Calendar.getInstance();
      Calendar now = Calendar.getInstance();
      startTime.set(Calendar.HOUR_OF_DAY, hour);
      startTime.set(Calendar.MINUTE, minute);
      startTime.set(Calendar.SECOND, second);
      startTime.set(Calendar.MILLISECOND, 0);

      if(startTime.before(now) || startTime.equals(now))
      {
         startTime.add(Calendar.DATE, 1);
      }

      return startTime.getTime();
   }
}

It doesn't require any external libs, and will account for daylight savings. Simply pass in the time of day you want to run the task as a Calendar object, and the task as a Runnable. For example:

Calendar timeOfDay = Calendar.getInstance();
timeOfDay.set(Calendar.HOUR_OF_DAY, 5);
timeOfDay.set(Calendar.MINUTE, 0);
timeOfDay.set(Calendar.SECOND, 0);

new DailyRunnerDaemon(timeOfDay, new Runnable()
{
   @Override
   public void run()
   {
      try
      {
        // call whatever your daily task is here
        doHousekeeping();
      }
      catch(Exception e)
      {
        logger.error("An error occurred performing daily housekeeping", e);
      }
   }
}, "daily-housekeeping");

N.B. the timer task runs in a Daemon thread which is not recommended for doing any IO. If you need to use a User thread, you will need to add another method which cancels the timer.

If you have to use a ScheduledExecutorService, simply change the startTimer method to the following:

private void startTimer()
{
   Executors.newSingleThreadExecutor().schedule(new Runnable()
   {
      Thread.currentThread().setName(runThreadName);
      dailyTask.run();
      startTimer();
   }, getNextRunTime().getTime() - System.currentTimeMillis(),
   TimeUnit.MILLISECONDS);
}

I am not sure of the behaviour but you may need a stop method which calls shutdownNow if you go down the ScheduledExecutorService route, otherwise your application may hang when you try to stop it.

Solution 4

Java8:
My upgrage version from top answer:

  1. Fixed situation when Web Application Server doens't want to stop, because of threadpool with idle thread
  2. Without recursion
  3. Run task with your custom local time, in my case, it's Belarus, Minsk


/**
 * Execute {@link AppWork} once per day.
 * <p>
 * Created by aalexeenka on 29.12.2016.
 */
public class OncePerDayAppWorkExecutor {

    private static final Logger LOG = AppLoggerFactory.getScheduleLog(OncePerDayAppWorkExecutor.class);

    private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    private final String name;
    private final AppWork appWork;

    private final int targetHour;
    private final int targetMin;
    private final int targetSec;

    private volatile boolean isBusy = false;
    private volatile ScheduledFuture<?> scheduledTask = null;

    private AtomicInteger completedTasks = new AtomicInteger(0);

    public OncePerDayAppWorkExecutor(
            String name,
            AppWork appWork,
            int targetHour,
            int targetMin,
            int targetSec
    ) {
        this.name = "Executor [" + name + "]";
        this.appWork = appWork;

        this.targetHour = targetHour;
        this.targetMin = targetMin;
        this.targetSec = targetSec;
    }

    public void start() {
        scheduleNextTask(doTaskWork());
    }

    private Runnable doTaskWork() {
        return () -> {
            LOG.info(name + " [" + completedTasks.get() + "] start: " + minskDateTime());
            try {
                isBusy = true;
                appWork.doWork();
                LOG.info(name + " finish work in " + minskDateTime());
            } catch (Exception ex) {
                LOG.error(name + " throw exception in " + minskDateTime(), ex);
            } finally {
                isBusy = false;
            }
            scheduleNextTask(doTaskWork());
            LOG.info(name + " [" + completedTasks.get() + "] finish: " + minskDateTime());
            LOG.info(name + " completed tasks: " + completedTasks.incrementAndGet());
        };
    }

    private void scheduleNextTask(Runnable task) {
        LOG.info(name + " make schedule in " + minskDateTime());
        long delay = computeNextDelay(targetHour, targetMin, targetSec);
        LOG.info(name + " has delay in " + delay);
        scheduledTask = executorService.schedule(task, delay, TimeUnit.SECONDS);
    }

    private static long computeNextDelay(int targetHour, int targetMin, int targetSec) {
        ZonedDateTime zonedNow = minskDateTime();
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec).withNano(0);

        if (zonedNow.compareTo(zonedNextTarget) > 0) {
            zonedNextTarget = zonedNextTarget.plusDays(1);
        }

        Duration duration = Duration.between(zonedNow, zonedNextTarget);
        return duration.getSeconds();
    }

    public static ZonedDateTime minskDateTime() {
        return ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
    }

    public void stop() {
        LOG.info(name + " is stopping.");
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
        executorService.shutdown();
        LOG.info(name + " stopped.");
        try {
            LOG.info(name + " awaitTermination, start: isBusy [ " + isBusy + "]");
            // wait one minute to termination if busy
            if (isBusy) {
                executorService.awaitTermination(1, TimeUnit.MINUTES);
            }
        } catch (InterruptedException ex) {
            LOG.error(name + " awaitTermination exception", ex);
        } finally {
            LOG.info(name + " awaitTermination, finish");
        }
    }

}

Solution 5

Have you considered using something like Quartz Scheduler? This library has a mechanism for scheduling tasks to run at a set period of time every day using a cron like expression (take a look at CronScheduleBuilder).

Some example code (not tested):

public class GetDatabaseJob implements InterruptableJob
{
    public void execute(JobExecutionContext arg0) throws JobExecutionException
    {
        getFromDatabase();
    }
}

public class Example
{
    public static void main(String[] args)
    {
        JobDetails job = JobBuilder.newJob(GetDatabaseJob.class);

        // Schedule to run at 5 AM every day
        ScheduleBuilder scheduleBuilder = 
                CronScheduleBuilder.cronSchedule("0 0 5 * * ?");
        Trigger trigger = TriggerBuilder.newTrigger().
                withSchedule(scheduleBuilder).build();

        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.scheduleJob(job, trigger);

        scheduler.start();
    }
}

There's a bit more work upfront, and you may need to rewrite your job execution code, but it should give you more control over how you want you job to run. Also it would be easier to change the schedule should you need to.

Share:
158,116
AKIWEB
Author by

AKIWEB

Updated on July 02, 2021

Comments

  • AKIWEB
    AKIWEB almost 3 years

    I am trying to run a certain task everyday at 5 AM in the morning. So I decided to use ScheduledExecutorService for this but so far I have seen examples which shows how to run task every few minutes.

    And I am not able to find any example which shows how to run a task every day at a particular time (5 AM) in the morning and also considering the fact of daylight saving time as well -

    Below is my code which will run every 15 minutes -

    public class ScheduledTaskExample {
        private final ScheduledExecutorService scheduler = Executors
            .newScheduledThreadPool(1);
    
        public void startScheduleTask() {
        /**
        * not using the taskHandle returned here, but it can be used to cancel
        * the task, or check if it's done (for recurring tasks, that's not
        * going to be very useful)
        */
        final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate(
            new Runnable() {
                public void run() {
                    try {
                        getDataFromDatabase();
                    }catch(Exception ex) {
                        ex.printStackTrace(); //or loggger would be better
                    }
                }
            }, 0, 15, TimeUnit.MINUTES);
        }
    
        private void getDataFromDatabase() {
            System.out.println("getting data...");
        }
    
        public static void main(String[] args) {
            ScheduledTaskExample ste = new ScheduledTaskExample();
            ste.startScheduleTask();
        }
    }
    

    Is there any way, I can schedule a task to run every day 5 AM in the morning using ScheduledExecutorService considering the fact of daylight saving time as well?

    And also TimerTask is better for this or ScheduledExecutorService?

  • AKIWEB
    AKIWEB over 10 years
    Thanks for suggestion, can you please explain me in detail how intDelayInHour means that I will be running my task at 5 AM in the morning?
  • José Andias
    José Andias over 9 years
    What is the purpose of aDate?
  • PaddyD
    PaddyD over 9 years
    But if you start this at HH:mm the task will be run at 05:mm as opposed to 5am? It also doesn't account for daylight saving time as OP requested. Ok if you start it immediately after the hour, or if you're happy with any time between 5 and 6, or if you don't mind restarting the application in the middle of the night twice a year after the clocks have changed I suppose...
  • PaddyD
    PaddyD over 9 years
    You still have the issue whereby you need to restart this twice a year if you want it to run at the right local time. scheduleAtFixedRate won't cut it unless you are happy with the same UTC time all year.
  • Sage
    Sage over 9 years
    I got your point. +1 and thank you. However, it is better if we don't use Daemon thread(i.e., new Timer(runThreadName, true)).
  • PaddyD
    PaddyD over 9 years
    @Sage no worries. A daemon thread is fine if you're not doing any IO. The use case I wrote this for was just a simple fire-and-forget class for kicking off some threads to perform some daily housekeeping tasks. I suppose if you are performing database reads in the timer task thread as OP's request might indicate, then you shouldn't use a Daemon and will need some kind of stop method which you will have to call to enable your application to terminate. stackoverflow.com/questions/7067578/…
  • philonous
    philonous over 7 years
    For readability I would suggest TimeUnit.DAYS.toMinutes(1) instead of the "magic number" 1440.
  • FReeze FRancis
    FReeze FRancis about 7 years
    @PaddyD Was the last part i.e the one using ScheduledExecutorSerive correct??? The way the anonymous class is created doesnt look correct syntax wise. Also newSingleThreadExecutor() wouldnt have schedule method right??
  • invzbl3
    invzbl3 over 5 years
    Thanks, Victor. In this way need to restart twice a year if I want it to run at the right local time?
  • Victor
    Victor over 5 years
    the fixed rate should not change when local time changes, after created, it becomes about the rate.
  • krizajb
    krizajb over 5 years
    Why does the following example (second) trigger execute n times or till the second passes? Wasn't the code suppose to trigger task once every day?
  • krizajb
    krizajb over 5 years
    How come this triggers my task at 23:59:59?
  • Kelly Denehy
    Kelly Denehy over 5 years
    For readability I would suggest 1 instead of either 1440 or TimeUnit.DAYS.toMinutes(1), and then use the time unit TimeUnit.DAYS. ;-)
  • Naga
    Naga over 5 years
    Maybe this is different question, can we achieve same results with JobScheduler
  • clinux
    clinux over 4 years
    This does not seem to work if the computer hibernates. The initial delay is frozen until the computer wakes up, then the task happens at the wrong time.
  • user2163960
    user2163960 over 4 years
    This example solution still does not work correctly if you are trying to run something (that runs very quickly) right after or at midnight. It has (at least 2) problems - it may trigger before midnight, because the scheduled executor isn't exact, and, when it does trigger, it runs and runs and runs, usually 999 times - once every ms until the next ms happens. Still looking for a proper solution....
  • Mark Jeronimus
    Mark Jeronimus about 4 years
    Please don't use the outdated Date and TimeUnit in 2019
  • Mark Jeronimus
    Mark Jeronimus about 4 years
    It simplifies if you make use of truncatedTo()
  • Mark Jeronimus
    Mark Jeronimus about 4 years
    Best is to run a fast (and inaccurate) timer (I chose every 4 seconds) and fire the event when the current time exceeds nextRun, then update nextRun for the next occurrence. Something like this: private void scheduleNext(){LocalDateTime now=LocalDateTime.now();nextRun=now.truncatedTo(ChronoUnit.D‌​AYS).plusHours(5);if‌​(nextRun.compareTo(n‌​ow)<=0)nextRun=nextR‌​un.plus(1,ChronoUnit‌​.DAYS);}
  • Displee
    Displee over 3 years
    Why would you make a thread pool with only 1 thread inside it?
  • amitwdh
    amitwdh over 3 years
    I have similar requirement. I have to schedule task for a day at given time. Once scheduled task completes, schedule task for next day at given time. This should continue to go on.My question is how to find out scheduled task has completed or not. Once I know scheduled task has completed then only I can schedule for next day.
  • M. Justin
    M. Justin almost 3 years
    @invzbl3 — You're right, since this doesn't account for the 23/25 hour days that occur yearly at the daylight saving time cutover (or whatever amount the daylight time shift may be).
  • Basil Bourque
    Basil Bourque almost 3 years
    This code fails to account to anomalies such as Daylight Savings Time (DST) imposed by politicians on a region’s timekeeping. The LocalDateTime class is exactly the wrong class to be using as it purposely lacks the context of a time zone or offset-from-UTC.
  • Basil Bourque
    Basil Bourque almost 3 years
    If someone is stuck on Java 6 & 7, I would suggest they instead use the back-port of most of the java.time functionality. See the ThreeTen-Backport project.
  • Frédéric Perron
    Frédéric Perron almost 3 years
    Your post is more a comment than an actual answer to the problem.
  • Ankit Jain
    Ankit Jain almost 3 years
    Yes you are correct but I dont have permission to comment on the main answer so I have to comment like this only.
  • Alessandro Parisi
    Alessandro Parisi over 2 years
    This should be the accepted answer to be honest, it's so much more clear and simple compared to the other one