Finding Last Fired time using a Cron Expression in Java
Asked Answered
F

11

15

Is there a way in Java to find the "Last Fired Time" from a Cron Expression?

E.g. If now = 25-Apr-2010 10PM, and the cron expression is 0 15 10 ? * * (quartz), it should return 25-Apr-2010 10:15AM.

Note:

  1. I do not care if we use standard cron expressions (like Unix and Quartz) or less popular ones if they can fetch me the correct "Last Fired Time"
  2. Also it is not literally "Last Fire time" as the trigger may not have fired, but logically there should be a way of telling when it (would have) fired last.
Flinn answered 26/4, 2010 at 2:51 Comment(0)
O
11

cron-utils is an opensource Java library to parse, validate, migrate crons that supports the operation you need. To get the previous date from a cron before a given time simply:

//Get date for last execution
DateTime now = DateTime.now();
ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("* * * * * * *"));
DateTime lastExecution = executionTime.lastExecution(now));

Bear in mind that in its current state it is a bit buggy and may not compute correctly for more complex cron expressions.

Omsk answered 11/1, 2016 at 13:29 Comment(2)
hi Joao, This works even if job dont run (example: server offline)?Hoshi
@NunoMarinho Yes, this simply calculates the previous date from a cron expression. The library itself knows nothing of what you're doing with the cron expression. This library does not schedule actions or jobs to be executed it just calculates dates based on cron expressions. I haven't used this library in a long time though, make sure it suits your needs and thoroughly test your application after using it.Ganesha
P
3

First, I am not aware of an existing library that supports this. Quartz might, but the standard Java class libraries certainly don't.

Second, strictly speaking what you are asking for originally asked for is impossible. The best that a library can tell you is that the cron expression would have or should have fired. The only thing that could (theoretically) tell you the last time that a cron expression actually fired is the scheduler instance itself.

Pourparler answered 26/4, 2010 at 4:21 Comment(1)
Hi Stephen, Thanks for your feedback. I have added a little more info in notes of my question to clear my problem.Flinn
B
3

Quartz seems to have some library support for cron expressions.

See the Javadoc for the CronExpression class, which has a method called getTimeBefore. I.e.,

CronExpression cron = new CronExpression("0 15 10 ? * *");
Date today = new Date();
Date previousExecution = cron.getTimeBefore(today);

It might depend on the Quartz version whether this works.

Looking at the latest source (version 2.3.0 at time of writing) this method has not been implemented and always returns null.

Benedick answered 27/4, 2010 at 5:34 Comment(2)
Answered in 2010, still returns null :(Ahrendt
today, it still returns null :(Beckiebeckley
D
2
import org.springframework.scheduling.support.CronSequenceGenerator;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

public class DateCalendarUtil {

    public static Date lastRunOn(String cronExpression) {
        final Date nextExecution = fromLocalDateTime(toLocalDate(nextCronDate(cronExpression)).atTime(23, 59, 59));
        return subtract(nextExecution, numberOfDays(nextExecution, nextCronDate(cronExpression, nextExecution)).intValue());
    }

    /**
     * Converts {@link Date} to {@link LocalDate} with default system {@link ZoneId}
     *
     * @param date to be converted to {@link LocalDate}
     * @return converted {@link Date}
     */
    public static LocalDate toLocalDate(Date date) {
        return toLocalDate(date, ZoneId.systemDefault());
    }

    /**
     * Converts {@link Date} to {@link LocalDate} with provided {@link ZoneId}
     * @param date to be converted to {@link LocalDate}
     * @param zoneId with which {@link Date} will be converted
     * @return converted {@link Date}
     */
    public static LocalDate toLocalDate(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).toLocalDate();
    }

    /**
     * Converts {@link Date} to {@link LocalDateTime} with provided {@link ZoneId}
     * @param date to be converted to {@link LocalDateTime} with provided {@link ZoneId}
     * @param zoneId with which {@link Date} will be converted
     * @return converted {@link Date}
     */
    public static LocalDateTime toLocalDateTime(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).toLocalDateTime();
    }

    /**
     * Converts {@link Date} to {@link LocalDateTime} with system default {@link ZoneId}
     *
     * @param date to be converted to {@link LocalDateTime}
     * @return converted {@link Date}
     */
    public static LocalDateTime toLocalDateTime(Date date) {
        return toLocalDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDate} to {@link Date} with default system {@link ZoneId}
     * @param localDate to be converted to {@link Date}
     * @return converted {@link LocalDate}
     */
    public static Date fromLocalDate(LocalDate localDate) {
        return fromLocalDate(localDate, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDate} to {@link Date} with provided {@link ZoneId}
     * @param localDate to be converted to {@link Date}
     * @param zoneId with which {@link LocalDate} converted
     * @return converted {@link LocalDate}
     */
    public static Date fromLocalDate(LocalDate localDate, ZoneId zoneId) {
        return Date.from(localDate.atStartOfDay(zoneId).toInstant());
    }

    /**
     * Converts {@link LocalDateTime} to {@link Date} with default system {@link ZoneId}
     *
     * @param localDateTime to be converted to {@link Date}
     * @return converted {@link LocalDateTime}
     */
    public static Date fromLocalDateTime(LocalDateTime localDateTime) {
        return fromLocalDateTime(localDateTime, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDateTime} to {@link Date} with provided {@link ZoneId}
     *
     * @param localDateTime to be converted to {@link Date}
     * @param zoneId        with which localDateTime converted to {@link Date}
     * @return converted {@link Date}
     */
    private static Date fromLocalDateTime(LocalDateTime localDateTime, ZoneId zoneId) {
        return Date.from(localDateTime.atZone(zoneId).toInstant());
    }

    public static Date yesterday() {
        return yesterday(TimeZone.getDefault());
    }

    public static Date yesterday(TimeZone timezone) {
        return subtract(new Date(), 1, timezone);
    }

    /**
     * Generates start time of give date with system default {@link TimeZone}
     * @param date Date of which start time to be generated
     * @return Date with start time as 00:00:00
     */
    public static Date startTime(Date date) {
        return startTime(date, TimeZone.getDefault());
    }

    /**
     * Generates start time of give date with provided {@link TimeZone}
     * @param date Date of which start time to be generated
     * @param timeZone with which {@link Calendar} created
     * @return Date with start time as 00:00:00
     */
    public static Date startTime(Date date, TimeZone timeZone) {
        Calendar calendar = Calendar.getInstance(timeZone);
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        return calendar.getTime();
    }

    /**
     * Generates end time of give date with system default {@link TimeZone}
     * @param date Date of which end time to be generated
     * @return Date with end time as 23:59:59
     */
    public static Date endTime(Date date) {
        return endTime(date, TimeZone.getDefault());
    }

    /**
     * Generates end time of give date with provided {@link TimeZone}
     * @param date Date of which end time to be generated
     * @param timeZone with which {@link Calendar} created
     * @return Date with end time as 23:59:59
     */
    public static Date endTime(Date date, TimeZone timeZone) {
        Calendar calendar = Calendar.getInstance(timeZone);
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 23);
        calendar.set(Calendar.MINUTE, 59);
        calendar.set(Calendar.SECOND, 59);
        return calendar.getTime();
    }

    /**
     * Calculates number of days between from and to
     * @param from start Date
     * @param to end date
     * @return number of days including last date
     */
    public static Long numberOfDays(Date from, Date to) {
        return TimeUnit.DAYS.convert(to.getTime() - from.getTime(), TimeUnit.MILLISECONDS) + 1;
    }

    /**
     * Gives next {@link Date} from given cron expression
     * @param cronExpression cron expression
     * @return next {@link Date}
     */
    public static Date nextCronDate(String cronExpression) {
        return nextCronDate(cronExpression, new Date());
    }

    public static Date nextCronDate(String cronExpression, Date date) {
        CronSequenceGenerator generator = new CronSequenceGenerator(cronExpression, TimeZone.getTimeZone("IST"));
        return DateCalendarUtil.fromLocalDate(DateCalendarUtil.toLocalDate(generator.next(date)));
    }
}
Disject answered 20/9, 2019 at 9:28 Comment(1)
Code looks great but there is no definition to the subtract function you are using. Please add that.Nimbus
I
1

A solution I have found with Quartz is to go back one time interval for the trigger and calculate what would have been the next firing time. By iterating through all the triggers the most recent time that a trigger should have fired in the past can be determined.


Calculate the interval between each firing:

Date nextFireTime = trigger.getNextFireTime();
Date subsequentFireTime = trigger.getFireTimeAfter(nextFireTime);
long interval = subsequentFireTime.getTime() - nextFireTime.getTime();

Find the next firing time for one time until interval in the past:

Date previousPeriodTime = new Date(System.currentTimeMillis() - interval);
Date previousFireTime = trigger.getFireTimeAfter(previousPeriodTime);

I have found that if you are using a CronTrigger this prevents you asking for a fire time in the past. To work around this I modify the start time, so the above snippet becomes:

Date originalStartTime = trigger.getStartTime(); // save the start time
Date previousPeriodTime = new Date(originalStartTime.getTime() - interval);
trigger.setStartTime(previousPeriodTime);
Date previousFireTime = trigger.getFireTimeAfter(previousPeriodTime);
trigger.setStartTime(originalStartTime); // reset the start time to be nice

Iterate through all of the triggers and find the one that is most recently in the past:

for (String groupName : scheduler.getTriggerGroupNames()) {
    for (String triggerName : scheduler.getTriggerNames(groupName)) {
        Trigger trigger = scheduler.getTrigger(triggerName, groupName);
        // code as detailed above...
        interval = ...
        previousFireTime = ...
    }
}

I'll leave it as an exercise to the reader to refactor this into helper methods or classes. I actually use the above algorithm in a subclassed delegating trigger that I then place in a set sorted by previous firing times.

Iy answered 19/1, 2012 at 16:24 Comment(2)
Difference between the subsequent fires will not be same always.Both
Ahamed is correct. This code is incorrect generally and only correct in a small subset of cases.Hashish
D
1

In case you are using org.quartz, is possible to use context.getPreviousFireTime();

Example:

public class TestJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {

        context.getPreviousFireTime(); 
    }
}

If you are using context.getTrigger().getPreviousFireTime(), you will have the triggered time of the Job that is running at the moment.

Doble answered 25/11, 2016 at 8:55 Comment(0)
B
0

It's amazing that there is still no quartz method for getting the previous fired time based on a CronExpression...

How to get last fired time ?

If you are manipulating basic CRON like 0 0 0 * * ? (At 00:00:00am every day) you can use João Neves' solution (using com.cronutils.model.time.ExecutionTime).

Otherwise if you are manipulating complex CRON like 0 30 11 ? * MON,THU * it will not works. You'll get random results (I had a Wednesday for this one...).

Edit: I did other tests and it looks working better with the latest version (previous tests were made with version < 3.1.6). Note: You need Java 8 if you want to use version > 3.1.6.

The solution you can use is to store it when your job is triggered.


How to verify that the job has been triggered ?

The solution I found is to use getNextValidTimeAfter from Quartz (CronExpression). This one works fine. You'll ask me what I'm talking about as we are looking for the previous valid time ! You are right, give me one second !

Let's imagine we have a once a month CRON (0 0 16 1 * ? = At 16:00:00pm, on the 1st day, every month) and we want to check everyday that the previous execution worked. You'll have to store the getNextValidTime at each execution and compare this date with today's date. eg (format DD/MM/YYYY):

• 01/01/2019 → job triggered, we store next fire time (let's call it nextFireTime):

CronExpression trigger = new CronExpression("0 0 16 1 * ?");
Date nextFireTime = trigger.getNextValidTimeAfter(new Date());
// = 01/02/2019

• 02/01/2019 → verification of the day: 02/01/2019 < 01/02/2019 OK

...

• 01/02/2019 → let's imagine server is down, the job is not triggered.

• 02/02/2019 → server on, verification of the day: 02/02/2019 > 01/02/2019 KO !

→ We know the previous fire time hasn't worked. You can know do what you want (trigger the job and store the new nextFireTime).


Another option that may interest you, see MISFIRE_INSTRUCTION_FIRE_NOW.

The job is executed immediately after the scheduler discovers misfire situation. This is the smart policy. Example scenario: you have scheduled some system clean up at 2 AM. Unfortunately the application was down due to maintenance by that time and brought back on 3 AM. So the trigger misfired and the scheduler tries to save the situation by running it as soon as it can - at 3 AM.

From (https://dzone.com/articles/quartz-scheduler-misfire)

e.g.:

Trigger trigger = newTrigger().
 withSchedule(
  cronSchedule("0 0 9-17 ? * MON-FRI").
   withMisfireHandlingInstructionFireAndProceed()
 ).
 build();

Official doc: https://www.quartz-scheduler.org/api/2.1.7/org/quartz/CronTrigger.html

Bastien answered 3/10, 2019 at 9:28 Comment(0)
B
0

I am using cron-utils 9.1.6 to get the last execution time based on 'now' and the 'cron expression', and the expected end time adding the expected duration in seconds.

The code's language is Scala.

import java.time.ZonedDateTime
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import com.cronutils.model.CronType.QUARTZ
import java.time.ZoneOffset.UTC

val parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(QUARTZ))
val cronExpression = "0 0 23 ? * *"
val now = ZonedDateTime.now(UTC)
val expectedDurationSec = 1500

println(s"now: $now")
val executionTime = ExecutionTime.forCron(parser.parse(cronExpression))
val lastExecution = executionTime.lastExecution(now)
println(s"lastExecution: ${lastExecution.get()}")
val expectedEnd = lastExecution.get().plusSeconds(expectedDurationSec)
println(s"expectedEnd: $expectedEnd")

the output is:

now: 2022-04-21T01:08:27.499Z
lastExecution: 2022-04-20T23:00Z
expectedEnd: 2022-04-20T23:25Z
Beckiebeckley answered 21/4, 2022 at 1:10 Comment(0)
C
0

There is Spring's org.springframework.scheduling.support.CronSequenceGenerator which computes next() time of cron execution relative to arbitrary time. Assuming that "last fired time" means the earliest time t for which holds next(t) ≥ now, it can be found by simple iteration algorithm:

  1. Starting from given time, walk back to find some moment t where next(t) < now (meaning that t belongs to some previous interval). This can be done by e.g. step doubling, taking some arbitrary initial step size. Assuming that many real-life cron expressions produce roughly equal intervals, reasonable guess for initial step size is next(now) – now + ε, or next(next(now)) – next(now) + ε when now is expected between executions, which is by contract > 0;
  2. Starting from t, walk forward iterating t := next(t) until next(t) ≥ now.

static long previous(String cron, TimeZone timezone, long ms) {
    var generator = new CronSequenceGenerator(cron, timezone);

    // 1. Walk back to find some moment where t.next() < ms
    long t1 = generator.next(new Date(ms)).getTime() - ms;  // initial step
    long t;
    do {
        if (t1 <= 0) {      // overflow or negative initial step
            throw new IllegalArgumentException("Failed to find previous time");
        }
        t = ms - t1 - 1;    // next try for t
        t1 *= 2;            // double step size
    } while (generator.next(new Date(t)).getTime() >= ms);

    // 2. Walk forward through next().next()... chain to find
    // the earliest moment where t1.next() >= ms
    while (t < ms) {
        t1 = t;
        t = generator.next(new Date(t)).getTime();
    }
    return t1;
}

The CronSequenceGenerator can be replaced by CronExpression or whatever having similar to next() function—the code just demonstrates approach to find "previous" using only "next".

Cairistiona answered 26/9, 2023 at 10:32 Comment(0)
P
-1
public class CronMircoUtils {

    /**
     * Use this method to calculate previous valid date for cron
     * @param ce CronExpression object with cron expression
     * @param date Date for find previous valid date
     * @return
     */
    public static Date getPreviousValidDate(CronExpression ce, Date date) {
        try {
            Date nextValidTime = ce.getNextValidTimeAfter(date);
            Date subsequentNextValidTime = ce.getNextValidTimeAfter(nextValidTime);
            long interval = subsequentNextValidTime.getTime() - nextValidTime.getTime();
            return new Date(nextValidTime.getTime() - interval);
        } catch (Exception e) {
            throw new IllegalArgumentException("Unsupported cron or date", e);
        }
    }
}

Source Code in https://github.com/devbhuwan/cron-micro-utils

Photoconductivity answered 6/7, 2015 at 5:46 Comment(1)
This will only work for periodic firings. What happens when there's no subsequentNextValidTime or when firings are irregular (e.g. fire at minutes 10, 12, 11, 17, 37)?Ganesha
T
-1

Its working solution

   pom :
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.0</version>
        </dependency>

   javaCode:
        //Provide your format of date  want
        String format="dd-MMM-YYYY hh:mm:ss";
        //Provide your cron expression you want
        String cronExpression="* * 12 ? * FRI *";
        SimpleDateFormat sdf=new SimpleDateFormat(format);
        CronExpression expression = new CronExpression(cronExpression);
        Date  currentDate=new Date();
        Date nextDate=expression.getNextValidTimeAfter(currentDate);
        long interval = nextDate.getTime()-currentDate.getTime();
        Date previousDate= new Date(currentDate.getTime() - interval);
        System.out.Println(sdf.format(previousDate));

Here im getting the difference between the current time and next fire time and exluding this difference time from current time so that we will get last fired time

Thorstein answered 12/7, 2018 at 11:1 Comment(3)
While this code may answer the question, providing information on how and why it solves the problem improves its long-term value.Jeter
This code has all kinds of crazy assumptions; why would currentDate be exactly in the middle between two executions? Also, this is a good example of how NOT to manipulate dates. What about day time saving and leap seconds?Hyperplane
@JanLarsen I think the assumption here is that current time is exactly some execution time, and cron expression produces exactly equal intervals in millisecondsCairistiona

© 2022 - 2024 — McMap. All rights reserved.