-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Prevent stack overflow in rounding #80450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1132,50 +1132,69 @@ public long beforeOverlap(long localMillis, Overlap overlap) { | |
| */ | ||
| private class JavaTimeRounding extends TimeIntervalPreparedRounding { | ||
| @Override | ||
| public long round(long utcMillis) { | ||
| final Instant utcInstant = Instant.ofEpochMilli(utcMillis); | ||
| final LocalDateTime rawLocalDateTime = LocalDateTime.ofInstant(utcInstant, timeZone); | ||
|
|
||
| // a millisecond value with the same local time, in UTC, as `utcMillis` has in `timeZone` | ||
| final long localMillis = utcMillis + timeZone.getRules().getOffset(utcInstant).getTotalSeconds() * 1000; | ||
| assert localMillis == rawLocalDateTime.toInstant(ZoneOffset.UTC).toEpochMilli(); | ||
|
|
||
| final long roundedMillis = roundKey(localMillis, interval) * interval; | ||
| final LocalDateTime roundedLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(roundedMillis), ZoneOffset.UTC); | ||
|
|
||
| // Now work out what roundedLocalDateTime actually means | ||
| final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(roundedLocalDateTime); | ||
| if (currentOffsets.isEmpty() == false) { | ||
| // There is at least one instant with the desired local time. In general the desired result is | ||
| // the latest rounded time that's no later than the input time, but this could involve rounding across | ||
| // a timezone transition, which may yield the wrong result | ||
| final ZoneOffsetTransition previousTransition = timeZone.getRules().previousTransition(utcInstant.plusMillis(1)); | ||
| for (int offsetIndex = currentOffsets.size() - 1; 0 <= offsetIndex; offsetIndex--) { | ||
| final OffsetDateTime offsetTime = roundedLocalDateTime.atOffset(currentOffsets.get(offsetIndex)); | ||
| final Instant offsetInstant = offsetTime.toInstant(); | ||
| if (previousTransition != null && offsetInstant.isBefore(previousTransition.getInstant())) { | ||
| /* | ||
| * Rounding down across the transition can yield the | ||
| * wrong result. It's best to return to the transition | ||
| * time and round that down. | ||
| */ | ||
| return round(previousTransition.getInstant().toEpochMilli() - 1); | ||
| public long round(long originalUtcMillis) { | ||
| long utcMillis = originalUtcMillis; | ||
| int attempts = 0; | ||
| /* | ||
| * We give up after 5000 attempts and throw an exception. The | ||
| * most attempts I could get running locally are 500 - for | ||
| * Asia/Tehran with an 80,000 day range. You just can't declare | ||
| * ranges much larger than that in ES right now. | ||
| */ | ||
| attempt: while (attempts < 5000) { | ||
| final Instant utcInstant = Instant.ofEpochMilli(utcMillis); | ||
| final LocalDateTime rawLocalDateTime = LocalDateTime.ofInstant(utcInstant, timeZone); | ||
|
|
||
| // a millisecond value with the same local time, in UTC, as `utcMillis` has in `timeZone` | ||
| final long localMillis = utcMillis + timeZone.getRules().getOffset(utcInstant).getTotalSeconds() * 1000; | ||
| assert localMillis == rawLocalDateTime.toInstant(ZoneOffset.UTC).toEpochMilli(); | ||
|
|
||
| final long roundedMillis = roundKey(localMillis, interval) * interval; | ||
| final LocalDateTime roundedLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(roundedMillis), ZoneOffset.UTC); | ||
|
|
||
| // Now work out what roundedLocalDateTime actually means | ||
| final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(roundedLocalDateTime); | ||
| if (currentOffsets.isEmpty() == false) { | ||
| // There is at least one instant with the desired local time. In general the desired result is | ||
| // the latest rounded time that's no later than the input time, but this could involve rounding across | ||
| // a timezone transition, which may yield the wrong result | ||
| final ZoneOffsetTransition previousTransition = timeZone.getRules().previousTransition(utcInstant.plusMillis(1)); | ||
| for (int offsetIndex = currentOffsets.size() - 1; 0 <= offsetIndex; offsetIndex--) { | ||
| final OffsetDateTime offsetTime = roundedLocalDateTime.atOffset(currentOffsets.get(offsetIndex)); | ||
| final Instant offsetInstant = offsetTime.toInstant(); | ||
| if (previousTransition != null && offsetInstant.isBefore(previousTransition.getInstant())) { | ||
| /* | ||
| * Rounding down across the transition can yield the | ||
| * wrong result. It's best to return to the transition | ||
| * time and round that down. | ||
| */ | ||
| attempts++; | ||
| utcMillis = previousTransition.getInstant().toEpochMilli() - 1; | ||
| continue attempt; | ||
| } | ||
|
|
||
| if (utcInstant.isBefore(offsetTime.toInstant()) == false) { | ||
| return offsetInstant.toEpochMilli(); | ||
| } | ||
| } | ||
|
|
||
| if (utcInstant.isBefore(offsetTime.toInstant()) == false) { | ||
| return offsetInstant.toEpochMilli(); | ||
| } | ||
| final OffsetDateTime offsetTime = roundedLocalDateTime.atOffset(currentOffsets.get(0)); | ||
| final Instant offsetInstant = offsetTime.toInstant(); | ||
| throw new IllegalArgumentException( | ||
|
||
| this + " failed to round " + utcMillis + " down: " + offsetInstant + " is the earliest possible" | ||
| ); | ||
| } else { | ||
| // The desired time isn't valid because within a gap, so just return the start of the gap | ||
| ZoneOffsetTransition zoneOffsetTransition = timeZone.getRules().getTransition(roundedLocalDateTime); | ||
| return zoneOffsetTransition.getInstant().toEpochMilli(); | ||
| } | ||
|
|
||
| final OffsetDateTime offsetTime = roundedLocalDateTime.atOffset(currentOffsets.get(0)); | ||
| final Instant offsetInstant = offsetTime.toInstant(); | ||
| assert false : this + " failed to round " + utcMillis + " down: " + offsetInstant + " is the earliest possible"; | ||
| return offsetInstant.toEpochMilli(); // TODO or throw something? | ||
| } else { | ||
| // The desired time isn't valid because within a gap, so just return the start of the gap | ||
| ZoneOffsetTransition zoneOffsetTransition = timeZone.getRules().getTransition(roundedLocalDateTime); | ||
| return zoneOffsetTransition.getInstant().toEpochMilli(); | ||
| } | ||
| throw new IllegalArgumentException( | ||
| this | ||
| + " failed to round " | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we please log the timezone and the interval in the exception message if this happens? This way if we have missed a case that we are not aware of we'll at least be able to debug it. Perhaps a logger warning too?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll make sure it gets in there. I think folks intended |
||
| + utcMillis | ||
| + " down: transitioned backwards through too many daylight savings time transitions" | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1010,6 +1010,12 @@ public void testIntervalTwoTransitions() { | |
| assertThat(rounding.round(time("1982-11-10T02:51:22.662Z")), isDate(time("1982-03-23T05:00:00Z"), tz)); | ||
| } | ||
|
|
||
| public void testHugeTimeInterval() { | ||
| ZoneId tz = ZoneId.of("Asia/Tehran"); | ||
| Rounding rounding = Rounding.builder(TimeValue.timeValueDays(80000)).timeZone(tz).build(); | ||
| assertThat(rounding.round(time("2078-11-10T02:51:22.662Z")), isDate(time("1970-01-01T00:00:00+03:30"), tz)); | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't write a test locally that'd fail without going to pretty intense lengths. So I just used the worse case I could find. |
||
|
|
||
| public void testFixedIntervalRoundingSize() { | ||
| Rounding unitRounding = Rounding.builder(TimeValue.timeValueHours(10)).build(); | ||
| Rounding.Prepared prepared = unitRounding.prepare(time("2010-01-01T00:00:00.000Z"), time("2020-01-01T00:00:00.000Z")); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can make this constant a field that can be modified with the builder which defaults to 5000? That way we can write a unit test that ensures we correctly throw the exception in case the loop limit is reached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍