9

I have a quartz cron trigger that looks like so:

<bean id="batchProcessCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="batchProcessJobDetail" />
    <property name="cronExpression" value="0 30 2 * * ?" />
</bean>

How should I solve this, if I have several configurations that happen within the 2-3am period? Is there an accepted best practice?

Relevant link: http://www.quartz-scheduler.org/docs/faq.html#FAQ-daylightSavings

Basically it says "Deal with it." But my question is how!

STW
  • 44,917
  • 17
  • 105
  • 161
Jon Bristow
  • 1,675
  • 3
  • 27
  • 42

4 Answers4

5

I solved it using a separate trigger that only fires (an hour early) on the beginning date of DST for the configurations that happen between 2am and 3am Eastern.

Seems kludgey, but it works...

Jon Bristow
  • 1,675
  • 3
  • 27
  • 42
  • 1
    JBristow, there are other ways available that do not require to create a separate trigger. Check Ron's answer or mine. This of course requires to write a bit of code and not only through XML – will824 May 23 '14 at 12:26
  • Unfortunately, I don't know if this meets our needs, since it's been four years and I do not work with that code any more. – Jon Bristow Jun 18 '14 at 22:58
2

We are using the following solution. For this you will also need the joda time library.

public class MyCronExpression extends CronExpression
{
    CronExpression _orgCronExpression;

    public MyCronExpression(String cronExpression) throws ParseException
    {
        super(cronExpression);
        setTimeZone(TimeZone.getTimeZone("UTC"));
        _orgCronExpression = new CronExpression(cronExpression);
    }

    @Override
    public Date getTimeAfter(Date date)
    {

        Date date1 = super.getTimeAfter(new Date(date.getTime()-date.getTimezoneOffset()*60*1000));
        if (TimeZone.getDefault().inDaylightTime( date1 ) && !TimeZone.getDefault().inDaylightTime( date ))
        {
            DateTimeZone dtz = DateTimeZone.getDefault();
            Date dstEnd = new Date(dtz.nextTransition(date.getTime()));
            int dstEndHour = dstEnd.getHours();
            int dstDuration = (dtz.getOffset(date1.getTime()) - dtz.getStandardOffset(date1.getTime()))/(60*60*1000);
            int hour = date1.getHours()+date1.getTimezoneOffset()/60;
            if (hour < dstEndHour && hour >= dstEndHour-dstDuration)
                return dstEnd;
            else
                return _orgCronExpression.getTimeAfter(date);
        }
        else
            return _orgCronExpression.getTimeAfter(date);
    }

}

The class is used as follows:

        CronTriggerImpl trigger = new CronTriggerImpl();
    trigger.setCronExpression(new MyCronExpression("0 15 2 * * ?"));

Here some sample trigger times:

Tue Mar 25 02:15:00 CET 2014
Wed Mar 26 02:15:00 CET 2014
Thu Mar 27 02:15:00 CET 2014
Fri Mar 28 02:15:00 CET 2014
Sat Mar 29 02:15:00 CET 2014
**Sun Mar 30 03:00:00 CEST 2014**
Mon Mar 31 02:15:00 CEST 2014
Tue Apr 01 02:15:00 CEST 2014
Wed Apr 02 02:15:00 CEST 2014

Please post if you find any bugs/issues with this solution.

ron
  • 418
  • 4
  • 12
  • CronExpression class is a final class. How did you extend it? – Ad Infinitum Dec 12 '16 at 09:12
  • final was introduced in release 2.2.0 . either use an older release or patch the current release or if you do not require all features of quartz switch to my project yacron4j where DST handling is implemented and where the issues reported by will824 have been solved. – ron Dec 30 '16 at 08:01
1

I took Ron's very interesting answer and improved the getTimeAfter Method, In order to adjust it to server GMT running and possible differences when scheduling 'Once a year' cron expressions.

@Override
  public Date getTimeAfter(Date date) {

    Date nextDate = super.getTimeAfter(date);
    if(nextDate == null){
      return null;
    }
    DateTime date1 = new DateTime(nextDate);

    if (getTimeZone().inDaylightTime(date1.toDate()) && !getTimeZone().inDaylightTime(date)) {

      DateTimeZone dtz = DateTimeZone.forTimeZone(getTimeZone());
      DateTime dstEndDateTime = new DateTime(new Date(dtz.nextTransition(date.getTime())));

      int dstEndHour = dstEndDateTime.getHourOfDay();
      int dstDuration = (dtz.getOffset(date1.getMillis()) - dtz.getStandardOffset(date1.getMillis())) / (60 * 60 * 1000);
      int hour = date1.getHourOfDay();

      // Verifies if the scheduled hour is within a phantom hour (dissapears upon DST change)
      if (hour < dstEndHour && hour >= dstEndHour-dstDuration){       
        // Verify if the date is  a skip, otherwise it is a date in the future (like threads that run once a year)
        if(dstEndDateTime.getDayOfYear() == date1.minusDays(1).getDayOfYear()){
          return dstEndDateTime.toDate();
        }else{
          return nextDate;
        }

      }else{
        return nextDate;
      }

    } else{
      return nextDate;
    }

  }

Please note my server runs in GMT mode, therefore I do not use some of the offset conversions present in Ron's answer.

Also I discovered a Quartz bug, in which if you use the following configuration, it will fail because it is not capable of processing the cron expression correctly:

SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");

String cron = "0 15 2 8 3 ? 2015";
FailsafeCronExpression cronExpression = new FailsafeCronExpression(cron);
cronExpression.setTimeZone(DateTimeZone.forID("America/Vancouver"));

DateTime nextDate = new DateTime(cronExpression.getTimeAfter(sdf.parse("12/11/2014 10:15:00")));

This actually seems to happen because DST change takes place during 9th of March 2am for Vancouver and seems the Quartz internal implementation of the super.getTimeAfter(date) method will always send null.

I hope this information is useful.

will824
  • 2,203
  • 4
  • 27
  • 29
  • Will, thanks for the feedback. I have been able to solve the issue with America/Vancouver. I have integrated the solution into a new project: http://sourceforge.net/projects/yacron4j – ron May 03 '15 at 19:31
1

I'm aware this question is quite old, but it still seems valid. I believe i've found a way to solve this problem, i'l leave it here in case someone else sumbles upon it and finds it handy

With spring 5.3 comes improved scheduling, rewritten using java.time API. It also supports quartz-specific extensions to cron expressions. Example computation code:

public Instant calculateNextExecution(String cronExpression, Instant lastExecutionInstant, ZoneId executionZoneId) {
    LocalDateTime lastExecutionDateTimeInExecutionZone lastExecutionInstant.atZone(executionZoneId)
        .toLocalDateTime();
    
    LocalDateTime nextExecutionDateInExecutionZone = CronExpression.parse(cronExpression).next(lastExecutionDateTimeInExecutionZone);
    // skipped checking and handling nonexistant next execution
    ZoneOffsetTransition transition = executionZoneId.getRules().getTransition(nextExecutionDateInExecutionZone);
    if (transition == null) {
        // next execution didn't occur during time transition
        return nextExecutionDateInExecutionZone.atZone(executionZoneId)
            .toInstant();
    } else {
        // next execution occured during time transition, one might check if transition was a gap or overlap and do sth with it
        return doSthWithIt(transition, nextExecutionDateInExecutionZone);
    }
}

Relevant spring class is org.springframework.scheduling.support.CronExpression. Detailed description https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions

@update: Spring scheduler doesn't support years in cron expressions :( so it might not work in you scenario

jgolda
  • 11
  • 2