javascheduledexecutorservice

ScheduledExecutorService schedule not triggering


The following code block does not work for me (alert is not being triggered):

public static void main(String[] args) throws InterruptedException, ParseException {        
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);
    TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault());
    Calendar calendar = Calendar.getInstance(timeZone);

    Scanner scanner = new Scanner(System.in);
    System.out.println("The time now is: " + calendar.getTime());
    System.out.println("Enter alert date time: ");
    String dateStr = scanner.nextLine();
    SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
    Date date = sdf.parse(dateStr);
    calendar.setTime(date);
    long alertTimeInMillis = calendar.getTimeInMillis();
    long now = Calendar.getInstance(timeZone).getTimeInMillis();
    System.out.println("Time to alert: " + (alertTimeInMillis - now) + " millis ");

    ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> System.out.println("alert!")
            , alertTimeInMillis, TimeUnit.MILLISECONDS);
    
    while (!scheduledFuture.isDone()) {
        System.out.println("The time now: " + Calendar.getInstance(timeZone).getTime());
        System.out.println("Expected alert time: " + date);
        Thread.sleep(1000);
    }
    scheduledExecutorService.shutdown();
    scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS);
}

While this code block does work:

public static void main(String[] args) throws InterruptedException {

    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);
    LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 10, 12, 1);
    ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() ->
            System.out.println("alert!"),
            LocalDateTime.now().until(localDateTime, ChronoUnit.SECONDS), TimeUnit.SECONDS);

    while (!scheduledFuture.isDone()) {
        Thread.sleep(1000);
    }
    scheduledExecutorService.shutdown();
    scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS);
}

I don't understand the difference, or what exactly is wrong with the first block that I'm missing.


Solution

  • Debugging legacy code

    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);

    No need for that Thread::new. Use empty parens, no arguments needed.

    TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault());

    Weird. You are mixing the legacy TimeZone class with its replacement ZoneId from java.time. Use only java.time; never use the terribly flawed legacy date-time classes.

    ZoneId z = ZoneId.sytemDefault() ;
    

    New conversion methods were added to the old classes for writing code to interoperate with old code not yet updated to java.time. TimeZone.getTimeZone( ZoneId ) is one such conversion method.

    Also, be aware that the JVM’s current default time zone can be changed at any moment by any code in any thread. Consider if you really want your code to spin the wheel on time-zone-roulette at runtime.

    new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

    Your formatting code uses hh in lowercase. This means 12-hour clock. But you neglected to collect an indicator of which half of the day. If the user types a time for 02:00:00, we cannot know if they meant 2 AM or 2 PM. I will change this to HH for 24-hour clock in example code below.

    SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); Date date = sdf.parse(dateStr);

    Be aware that SimpleDateFormat is using a default time zone to parse that date-with-time string. You happen to want the default time zone in your earlier code, so this may work out well. Or this may not work out well if some code has changed your JVM’s current default time zone in the interim.

    ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> System.out.println("alert!") , alertTimeInMillis, TimeUnit.MILLISECONDS);

    👉 This line is the source of your problem. You told the scheduled executor service to wait a number of milliseconds before executing. Your number of executors is the number of milliseconds since the epoch reference of first moment of 1970 in UTC — decades, that is. You told the executor service to wait decades, about 53 years (2023-1970 = 53). Which the executor service will do, faithfully, if you leave your computer running that long.

    Your code:

    long alertTimeInMillis = calendar.getTimeInMillis();
    

    … calls the Calendar#getTimeInMillis method. Javadoc says:

    Returns: the current time as UTC milliseconds from the epoch.

    What you meant to do is what you did do in your System.out.println: Calculate time to elapse between the current moment and that target moment.

    (alertTimeInMillis - now)
    

    Tip: Generally best to log existing values, rather than generating a value to log.

    Modern solution: java.time

    But enough of struggling with the terribly flawed legacy date-time classes. Never use Calendar, Date, or SimpleDateFormat. Always use java.time classes.

    Firstly, if using an executor service, copy the boilerplate code for a graceful shutdown. Find that code on ExecutorService Javadoc. Here is a slightly modified version.

    package work.basil.example.time;
    
    import java.time.Duration;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class WaitToRunDemo
    {
        public static void main ( String[] args )
        {
            WaitToRunDemo app = new WaitToRunDemo();
            app.demo();
        }
    
        private void demo ( )
        {
        }
    
        // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
        // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html 
        void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForTasksToWork , final Duration waitForTasksToComplete )
        {
            executorService.shutdown(); // Disable new tasks from being submitted
            try
            {
                // Wait a while for existing tasks to terminate
                if ( ! executorService.awaitTermination( waitForTasksToWork.toMillis() , TimeUnit.MILLISECONDS ) )
                {
                    executorService.shutdownNow(); // Cancel currently executing tasks
                    // Wait a while for tasks to respond to being cancelled
                    if ( ! executorService.awaitTermination( waitForTasksToComplete.toMillis() , TimeUnit.MILLISECONDS ) )
                    { System.err.println( "ExecutorService did not terminate." ); }
                }
            }
            catch ( InterruptedException ex )
            {
                // (Re-)Cancel if current thread also interrupted
                executorService.shutdownNow();
                // Preserve interrupt status
                Thread.currentThread().interrupt();
            }
        }
    }
    

    Here is complete example code.

    Let's break up your long code into a few separate methods, each with a specific goal.

    To represent a span of time not attached to the timeline, use Duration class.

    Notice how we minimized the use of LocalDateTime. That class cannot represent a moment, a point on the timeline as it lacks the context of a time zone or offset-from-UTC. I cannot imagine a situation where calling LocalDateTime.now is optimal.

    package work.basil.example.time;
    
    import java.time.*;
    import java.time.format.DateTimeFormatter;
    import java.time.temporal.ChronoUnit;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class WaitToRunDemo
    {
        public static void main ( String[] args )
        {
            WaitToRunDemo app = new WaitToRunDemo();
            app.demo();
        }
    
        private void demo ( )
        {
            ZonedDateTime then = this.gatherInput();
            Duration delayUntilTaskRuns = this.calculateTimeToElapse( then );
            System.out.println( "INFO - Now: " + Instant.now() + ". Scheduling task to run after delay of: " + delayUntilTaskRuns.toString() );
    
            this.runTask( delayUntilTaskRuns );
            System.out.println( "Demo done at " + Instant.now() );
        }
    
        private ZonedDateTime gatherInput ( )
        {
            // Put your `Scanner` code here. Tip: Confirm the time zone with the user.
    
            // Simulate the user entering text for the start of the next minute.
            ZoneId zoneId = ZoneId.systemDefault();
            ZonedDateTime now = ZonedDateTime.now( zoneId );
            ZonedDateTime startOfNextMinute = now.truncatedTo( ChronoUnit.MINUTES ).plusMinutes( 1 );
    
            DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd-MM-uuuu HH:mm:ss" );
            String userInput = startOfNextMinute.format( f );
            System.out.println( "DEBUG startOfNextMinute = " + startOfNextMinute );
            System.out.println( "DEBUG userInput = " + userInput );
    
            // Parse user input text into a `LocalDateTime` object to represent date with time-of-day but lacking any time zone or offset-from-UTC. 
            LocalDateTime ldt = LocalDateTime.parse( userInput , f );
            ZonedDateTime zdt = ldt.atZone( zoneId );
    
            return zdt;
        }
    
        private Duration calculateTimeToElapse ( final ZonedDateTime then )
        {
            Instant now = Instant.now();
            Duration delayUntilTaskRuns = Duration.between( now , then.toInstant() );
            if ( delayUntilTaskRuns.isNegative() ) { throw new IllegalStateException( "Specified wait time is negative (in the past)." ); }
            return delayUntilTaskRuns;
        }
    
        private void runTask ( Duration delay )
        {
            ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
            Runnable task = ( ) -> System.out.println( "Done running task at " + Instant.now() );
            ses.schedule( task , delay.toMillis() , TimeUnit.MILLISECONDS );
            this.shutdownAndAwaitTermination( ses , Duration.ofMinutes( 2 ) , Duration.ofMinutes( 1 ) );
        }
    
        // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
        // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html
        void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForTasksToWork , final Duration waitForTasksToComplete )
        {
            executorService.shutdown(); // Disable new tasks from being submitted
            try
            {
                // Wait a while for existing tasks to terminate
                if ( ! executorService.awaitTermination( waitForTasksToWork.toMillis() , TimeUnit.MILLISECONDS ) )
                {
                    executorService.shutdownNow(); // Cancel currently executing tasks
                    // Wait a while for tasks to respond to being cancelled
                    if ( ! executorService.awaitTermination( waitForTasksToComplete.toMillis() , TimeUnit.MILLISECONDS ) )
                    { System.err.println( "ExecutorService did not terminate." ); }
                }
            }
            catch ( InterruptedException ex )
            {
                // (Re-)Cancel if current thread also interrupted
                executorService.shutdownNow();
                // Preserve interrupt status
                Thread.currentThread().interrupt();
            }
        }
    }
    

    Where run:

    DEBUG startOfNextMinute = 2023-01-10T14:30-08:00[America/Los_Angeles]
    DEBUG userInput = 10-01-2023 14:30:00
    INFO - Now: 2023-01-10T22:29:37.091459Z. Scheduling task to run after delay of: PT22.908594S
    Done running task at 2023-01-10T22:30:00.012244Z
    Demo done at 2023-01-10T22:30:00.024242Z
    

    My example code when run was aiming to run the task at 2023-01-10T14:30-08:00[America/Los_Angeles]. That is the exact same moment, same point on the timeline as 2023-01-10T22:30:00Z where the “Z” means an offset from UTC of zero hours-minutes-seconds. The 14:30 in America/Los_Angeles is 8 hours behind UTC, so adding 8 hours brings us to 22:30 in UTC — same moment, different wall-clock time.

    In other words… Alice in Portland Oregon notices her clock on the wall strike 2:30 PM (14:30) as she dials the phone to call Bob in Reykjavík. As Bob answers the call, he notices his own clock on the wall reads 10:30 PM (22:30). Same moment, different wall-clock time.


    By the way, be aware that lines sent to System.out.println across threads may not appear in the console in chronological order. Always include a timestamp such as Instant.now(). If you care about sequence, study those timestamps.