Skip to content

Commit 9d914c3

Browse files
committed
Add timezone support to Cron objects
1 parent 281416d commit 9d914c3

File tree

3 files changed

+136
-14
lines changed

3 files changed

+136
-14
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/Cron.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ public class Cron implements ToXContentFragment {
232232

233233
private final String expression;
234234

235+
private TimeZone timeZone;
236+
235237
private transient TreeSet<Integer> seconds;
236238
private transient TreeSet<Integer> minutes;
237239
private transient TreeSet<Integer> hours;
@@ -246,7 +248,20 @@ public class Cron implements ToXContentFragment {
246248
private transient boolean nearestWeekday = false;
247249
private transient int lastdayOffset = 0;
248250

249-
public static final int MAX_YEAR = Calendar.getInstance(UTC, Locale.ROOT).get(Calendar.YEAR) + 100;
251+
// Restricted to 50 years as the tzdb only has correct DST transition information for countries using a lunar calendar
252+
// for the next ~60 years
253+
public static final int MAX_YEAR = Calendar.getInstance(UTC, Locale.ROOT).get(Calendar.YEAR) + 50;
254+
255+
public Cron(String expression, TimeZone timeZone) {
256+
this.timeZone = timeZone;
257+
assert expression != null : "cron expression cannot be null";
258+
this.expression = expression.toUpperCase(Locale.ROOT);
259+
try {
260+
buildExpression(this.expression);
261+
} catch (Exception e) {
262+
throw illegalArgument("invalid cron expression [{}]", e, expression);
263+
}
264+
}
250265

251266
/**
252267
* Constructs a new <CODE>CronExpression</CODE> based on the specified
@@ -259,13 +274,7 @@ public class Cron implements ToXContentFragment {
259274
* <CODE>CronExpression</CODE>
260275
*/
261276
public Cron(String expression) {
262-
assert expression != null : "cron expression cannot be null";
263-
this.expression = expression.toUpperCase(Locale.ROOT);
264-
try {
265-
buildExpression(this.expression);
266-
} catch (Exception e) {
267-
throw illegalArgument("invalid cron expression [{}]", e, expression);
268-
}
277+
this(expression, UTC);
269278
}
270279

271280
/**
@@ -275,7 +284,11 @@ public Cron(String expression) {
275284
* @param cron The existing cron expression to be copied
276285
*/
277286
public Cron(Cron cron) {
278-
this(cron.expression);
287+
this(cron.expression, cron.timeZone);
288+
}
289+
290+
public void setTimeZone(TimeZone timeZone) {
291+
this.timeZone = timeZone;
279292
}
280293

281294
/**
@@ -289,7 +302,7 @@ public Cron(Cron cron) {
289302
public long getNextValidTimeAfter(final long time) {
290303

291304
// Computation is based on Gregorian year only.
292-
Calendar cl = new java.util.GregorianCalendar(UTC, Locale.ROOT);
305+
Calendar cl = new java.util.GregorianCalendar(timeZone, Locale.ROOT);
293306

294307
// move ahead one second, since we're computing the time *after* the
295308
// given time
@@ -397,7 +410,7 @@ public long getNextValidTimeAfter(final long time) {
397410
day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
398411
day -= lastdayOffset;
399412

400-
Calendar tcal = Calendar.getInstance(UTC, Locale.ROOT);
413+
Calendar tcal = Calendar.getInstance(timeZone, Locale.ROOT);
401414
tcal.set(Calendar.SECOND, 0);
402415
tcal.set(Calendar.MINUTE, 0);
403416
tcal.set(Calendar.HOUR_OF_DAY, 0);
@@ -433,7 +446,7 @@ public long getNextValidTimeAfter(final long time) {
433446
t = day;
434447
day = daysOfMonth.first();
435448

436-
Calendar tcal = Calendar.getInstance(UTC, Locale.ROOT);
449+
Calendar tcal = Calendar.getInstance(timeZone, Locale.ROOT);
437450
tcal.set(Calendar.SECOND, 0);
438451
tcal.set(Calendar.MINUTE, 0);
439452
tcal.set(Calendar.HOUR_OF_DAY, 0);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.scheduler;
9+
10+
import org.elasticsearch.test.ESTestCase;
11+
12+
import java.time.Instant;
13+
import java.time.ZoneId;
14+
import java.time.ZoneOffset;
15+
import java.time.ZonedDateTime;
16+
import java.time.zone.ZoneOffsetTransition;
17+
import java.time.zone.ZoneRules;
18+
19+
import static java.util.TimeZone.getTimeZone;
20+
import static org.hamcrest.CoreMatchers.equalTo;
21+
import static org.hamcrest.CoreMatchers.not;
22+
23+
public class CronTimezoneTests extends ESTestCase {
24+
25+
public void testForFixedOffsetCorrectlyCalculateNextRuntime() {
26+
Cron cron = new Cron("0 0 2 * * ?", getTimeZone(ZoneOffset.of("+1")));
27+
long midnightUTC = Instant.parse("2020-01-01T00:00:00Z").toEpochMilli();
28+
long nextValidTimeAfter = cron.getNextValidTimeAfter(midnightUTC);
29+
assertThat(nextValidTimeAfter, equalTo(Instant.parse("2020-01-01T01:00:00Z").toEpochMilli()));
30+
}
31+
32+
public void testForLondonFixedDSTTransitionCheckCorrectSchedule() {
33+
ZoneId londonZone = getTimeZone("Europe/London").toZoneId();
34+
35+
Cron cron = new Cron("0 0 2 * * ?", getTimeZone(londonZone));
36+
ZoneRules londonZoneRules = londonZone.getRules();
37+
Instant springMidnight = Instant.parse("2020-03-01T00:00:00Z");
38+
long timeBeforeDST = springMidnight.toEpochMilli();
39+
40+
assertThat(cron.getNextValidTimeAfter(timeBeforeDST), equalTo(Instant.parse("2020-03-01T02:00:00Z").toEpochMilli()));
41+
42+
ZoneOffsetTransition zoneOffsetTransition = londonZoneRules.nextTransition(springMidnight);
43+
44+
Instant timeAfterDST = zoneOffsetTransition.getDateTimeBefore()
45+
.plusDays(1)
46+
.atZone(ZoneOffset.UTC)
47+
.withHour(0)
48+
.withMinute(0)
49+
.toInstant();
50+
51+
assertThat(cron.getNextValidTimeAfter(timeAfterDST.toEpochMilli()), equalTo(Instant.parse("2020-03-30T01:00:00Z").toEpochMilli()));
52+
}
53+
54+
public void testRandomDSTTransitionCalculateNextTimeCorrectlyRelativeToUTC() {
55+
ZoneId timeZone;
56+
57+
int i = 0;
58+
boolean found;
59+
do {
60+
timeZone = randomZone();
61+
found = getTimeZone(timeZone).useDaylightTime();
62+
i++;
63+
} while (found == false && i <= 500); // Infinite loop prevention
64+
65+
if (found == false) {
66+
fail("Could not find a timezone with DST");
67+
}
68+
69+
logger.info("Testing for timezone {}", timeZone);
70+
71+
ZoneOffsetTransition zoneOffsetTransition = timeZone.getRules().nextTransition(Instant.now());
72+
73+
ZonedDateTime midnightBefore = zoneOffsetTransition.getDateTimeBefore().atZone(timeZone).minusDays(2).withHour(0).withMinute(0);
74+
ZonedDateTime midnightAfter = zoneOffsetTransition.getDateTimeAfter().atZone(timeZone).plusDays(2).withHour(0).withMinute(0);
75+
76+
long epochBefore = midnightBefore.toInstant().toEpochMilli();
77+
long epochAfter = midnightAfter.toInstant().toEpochMilli();
78+
79+
Cron cron = new Cron("0 0 2 * * ?", getTimeZone(timeZone));
80+
81+
long nextScheduleBefore = cron.getNextValidTimeAfter(epochBefore);
82+
long nextScheduleAfter = cron.getNextValidTimeAfter(epochAfter);
83+
84+
assertThat(nextScheduleBefore - epochBefore, equalTo(2 * 60 * 60 * 1000L));
85+
assertThat(nextScheduleAfter - epochAfter, equalTo(2 * 60 * 60 * 1000L));
86+
87+
ZonedDateTime utcMidnightBefore = zoneOffsetTransition.getDateTimeBefore()
88+
.atZone(ZoneOffset.UTC)
89+
.minusDays(2)
90+
.withHour(0)
91+
.withMinute(0);
92+
93+
ZonedDateTime utcMidnightAfter = zoneOffsetTransition.getDateTimeAfter()
94+
.atZone(ZoneOffset.UTC)
95+
.plusDays(2)
96+
.withHour(0)
97+
.withMinute(0);
98+
99+
long utcEpochBefore = utcMidnightBefore.toInstant().toEpochMilli();
100+
long utcEpochAfter = utcMidnightAfter.toInstant().toEpochMilli();
101+
102+
long nextUtcScheduleBefore = cron.getNextValidTimeAfter(utcEpochBefore);
103+
long nextUtcScheduleAfter = cron.getNextValidTimeAfter(utcEpochAfter);
104+
105+
assertThat(nextUtcScheduleBefore - utcEpochBefore, not(equalTo(nextUtcScheduleAfter - utcEpochAfter)));
106+
107+
}
108+
109+
}

x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecyclePolicyTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ public void testNextExecutionTimeSchedule() {
6464
SnapshotLifecyclePolicy p = new SnapshotLifecyclePolicy(
6565
"id",
6666
"name",
67-
"0 1 2 3 4 ? 2099",
67+
"0 1 2 3 4 ? 2049",
6868
"repo",
6969
Collections.emptyMap(),
7070
SnapshotRetentionConfiguration.EMPTY
7171
);
72-
assertThat(p.calculateNextExecution(-1, Clock.systemUTC()), equalTo(4078864860000L));
72+
assertThat(p.calculateNextExecution(-1, Clock.systemUTC()), equalTo(2501028060000L));
7373
}
7474

7575
public void testNextExecutionTimeInterval() {

0 commit comments

Comments
 (0)