Browse Source

Handle long overflow in dates (#124048)

* Handle long overflow in dates
Stanislav Malyshev 6 months ago
parent
commit
07921a78a6

+ 6 - 0
docs/changelog/124048.yaml

@@ -0,0 +1,6 @@
+pr: 124048
+summary: Handle long overflow in dates
+area: Search
+type: bug
+issues:
+ - 112483

+ 36 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml

@@ -123,3 +123,39 @@ setup:
   - match: { hits.total: 1 }
   - match: { hits.total: 1 }
   - length: { hits.hits: 1 }
   - length: { hits.hits: 1 }
   - match: { hits.hits.0._id: "4" }
   - match: { hits.hits.0._id: "4" }
+
+---
+"test bad dates in range - past":
+  - requires:
+      cluster_features: [ "mapper.range.invalid_date_fix" ]
+      reason: "Fix for invalid date required"
+  - do:
+      catch: /illegal_argument_exception/
+      search:
+        index: dates
+        body:
+          sort: field
+          query:
+            range:
+              date:
+                gte: -522000000
+                lte: 2023
+                format: date_optional_time
+
+---
+"test bad dates in range - future":
+  - requires:
+      cluster_features: [ "mapper.range.invalid_date_fix" ]
+      reason: "Fix for invalid date required"
+  - do:
+      catch: /illegal_argument_exception/
+      search:
+        index: dates
+        body:
+          sort: field
+          query:
+            range:
+              date:
+                gte: 2020
+                lte: 522000000
+                format: date_optional_time

+ 27 - 2
server/src/main/java/org/elasticsearch/common/time/DateUtils.java

@@ -186,7 +186,7 @@ public class DateUtils {
 
 
     /**
     /**
      * convert a java time instant to a long value which is stored in lucene
      * convert a java time instant to a long value which is stored in lucene
-     * the long value resembles the nanoseconds since the epoch
+     * the long value represents the nanoseconds since the epoch
      *
      *
      * @param instant the instant to convert
      * @param instant the instant to convert
      * @return        the nano seconds and seconds as a single long
      * @return        the nano seconds and seconds as a single long
@@ -205,10 +205,35 @@ public class DateUtils {
         return instant.getEpochSecond() * 1_000_000_000 + instant.getNano();
         return instant.getEpochSecond() * 1_000_000_000 + instant.getNano();
     }
     }
 
 
+    /**
+     * Convert a java time instant to a long value which is stored in lucene,
+     * the long value represents the milliseconds since epoch
+     *
+     * @param instant the instant to convert
+     * @return        the total milliseconds as a single long
+     */
+    public static long toLongMillis(Instant instant) {
+        try {
+            return instant.toEpochMilli();
+        } catch (ArithmeticException e) {
+            if (instant.isAfter(Instant.now())) {
+                throw new IllegalArgumentException(
+                    "date[" + instant + "] is too far in the future to be represented in a long milliseconds variable",
+                    e
+                );
+            } else {
+                throw new IllegalArgumentException(
+                    "date[" + instant + "] is too far in the past to be represented in a long milliseconds variable",
+                    e
+                );
+            }
+        }
+    }
+
     /**
     /**
      * Returns an instant that is with valid nanosecond resolution. If
      * Returns an instant that is with valid nanosecond resolution. If
      * the parameter is before the valid nanosecond range then this returns
      * the parameter is before the valid nanosecond range then this returns
-     * the minimum {@linkplain Instant} valid for nanosecond resultion. If
+     * the minimum {@linkplain Instant} valid for nanosecond resolution. If
      * the parameter is after the valid nanosecond range then this returns
      * the parameter is after the valid nanosecond range then this returns
      * the maximum {@linkplain Instant} valid for nanosecond resolution.
      * the maximum {@linkplain Instant} valid for nanosecond resolution.
      * <p>
      * <p>

+ 4 - 1
server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

@@ -36,6 +36,7 @@ import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.common.util.LocaleUtils;
 import org.elasticsearch.common.util.LocaleUtils;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.features.NodeFeature;
 import org.elasticsearch.index.IndexMode;
 import org.elasticsearch.index.IndexMode;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexSortConfig;
 import org.elasticsearch.index.IndexSortConfig;
@@ -81,6 +82,7 @@ import java.util.function.Function;
 import java.util.function.LongSupplier;
 import java.util.function.LongSupplier;
 
 
 import static org.elasticsearch.common.time.DateUtils.toLong;
 import static org.elasticsearch.common.time.DateUtils.toLong;
+import static org.elasticsearch.common.time.DateUtils.toLongMillis;
 
 
 /** A {@link FieldMapper} for dates. */
 /** A {@link FieldMapper} for dates. */
 public final class DateFieldMapper extends FieldMapper {
 public final class DateFieldMapper extends FieldMapper {
@@ -100,12 +102,13 @@ public final class DateFieldMapper extends FieldMapper {
     private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis")
     private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis")
         .withLocale(DEFAULT_LOCALE)
         .withLocale(DEFAULT_LOCALE)
         .toDateMathParser();
         .toDateMathParser();
+    public static final NodeFeature INVALID_DATE_FIX = new NodeFeature("mapper.range.invalid_date_fix");
 
 
     public enum Resolution {
     public enum Resolution {
         MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) {
         MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) {
             @Override
             @Override
             public long convert(Instant instant) {
             public long convert(Instant instant) {
-                return instant.toEpochMilli();
+                return toLongMillis(instant);
             }
             }
 
 
             @Override
             @Override

+ 2 - 1
server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java

@@ -61,7 +61,8 @@ public class MapperFeatures implements FeatureSpecification {
             ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
             ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
             UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE,
             UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE,
             DOC_VALUES_SKIPPER,
             DOC_VALUES_SKIPPER,
-            RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING
+            RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING,
+            DateFieldMapper.INVALID_DATE_FIX
         );
         );
     }
     }
 }
 }

+ 39 - 0
server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java

@@ -27,6 +27,7 @@ import static org.elasticsearch.common.time.DateUtils.clampToNanosRange;
 import static org.elasticsearch.common.time.DateUtils.compareNanosToMillis;
 import static org.elasticsearch.common.time.DateUtils.compareNanosToMillis;
 import static org.elasticsearch.common.time.DateUtils.toInstant;
 import static org.elasticsearch.common.time.DateUtils.toInstant;
 import static org.elasticsearch.common.time.DateUtils.toLong;
 import static org.elasticsearch.common.time.DateUtils.toLong;
+import static org.elasticsearch.common.time.DateUtils.toLongMillis;
 import static org.elasticsearch.common.time.DateUtils.toMilliSeconds;
 import static org.elasticsearch.common.time.DateUtils.toMilliSeconds;
 import static org.elasticsearch.common.time.DateUtils.toNanoSeconds;
 import static org.elasticsearch.common.time.DateUtils.toNanoSeconds;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.containsString;
@@ -93,6 +94,44 @@ public class DateUtilsTests extends ESTestCase {
         assertThat(e.getMessage(), containsString("is after"));
         assertThat(e.getMessage(), containsString("is after"));
     }
     }
 
 
+    public void testInstantToLongMillis() {
+        assertThat(toLongMillis(Instant.EPOCH), is(0L));
+
+        Instant instant = createRandomInstant();
+        long timeSinceEpochInMillis = instant.toEpochMilli();
+        assertThat(toLongMillis(instant), is(timeSinceEpochInMillis));
+
+        Instant maxInstant = Instant.ofEpochSecond(Long.MAX_VALUE / 1000);
+        long maxInstantMillis = maxInstant.toEpochMilli();
+        assertThat(toLongMillis(maxInstant), is(maxInstantMillis));
+
+        Instant minInstant = Instant.ofEpochSecond(Long.MIN_VALUE / 1000);
+        long minInstantMillis = minInstant.toEpochMilli();
+        assertThat(toLongMillis(minInstant), is(minInstantMillis));
+    }
+
+    public void testInstantToLongMillisMin() {
+        /* negative millisecond value of this instant exceeds the maximum value a java long variable can store */
+        Instant tooEarlyInstant = Instant.MIN;
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant));
+        assertThat(e.getMessage(), containsString("too far in the past"));
+
+        Instant tooEarlyInstant2 = Instant.ofEpochSecond(Long.MIN_VALUE / 1000 - 1);
+        e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant2));
+        assertThat(e.getMessage(), containsString("too far in the past"));
+    }
+
+    public void testInstantToLongMillisMax() {
+        /* millisecond value of this instant exceeds the maximum value a java long variable can store */
+        Instant tooLateInstant = Instant.MAX;
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant));
+        assertThat(e.getMessage(), containsString("too far in the future"));
+
+        Instant tooLateInstant2 = Instant.ofEpochSecond(Long.MAX_VALUE / 1000 + 1);
+        e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant2));
+        assertThat(e.getMessage(), containsString("too far in the future"));
+    }
+
     public void testLongToInstant() {
     public void testLongToInstant() {
         assertThat(toInstant(0), is(Instant.EPOCH));
         assertThat(toInstant(0), is(Instant.EPOCH));
         assertThat(toInstant(1), is(Instant.EPOCH.plusNanos(1)));
         assertThat(toInstant(1), is(Instant.EPOCH.plusNanos(1)));

+ 2 - 1
server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java

@@ -152,7 +152,8 @@ public class DateFieldMapperTests extends MapperTestCase {
         return List.of(
         return List.of(
             exampleMalformedValue("2016-03-99").mapping(mappingWithFormat("strict_date_optional_time||epoch_millis"))
             exampleMalformedValue("2016-03-99").mapping(mappingWithFormat("strict_date_optional_time||epoch_millis"))
                 .errorMatches("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"),
                 .errorMatches("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"),
-            exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("long overflow"),
+            exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the past"),
+            exampleMalformedValue("522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the future"),
             exampleMalformedValue("2020").mapping(mappingWithFormat("strict_date"))
             exampleMalformedValue("2020").mapping(mappingWithFormat("strict_date"))
                 .errorMatches("failed to parse date field [2020] with format [strict_date]"),
                 .errorMatches("failed to parse date field [2020] with format [strict_date]"),
             exampleMalformedValue("hello world").mapping(mappingWithFormat("strict_date_optional_time"))
             exampleMalformedValue("hello world").mapping(mappingWithFormat("strict_date_optional_time"))

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java

@@ -42,7 +42,7 @@ public class ToDatetimeTests extends AbstractScalarFunctionTestCase {
             read,
             read,
             TestCaseSupplier.dateCases(),
             TestCaseSupplier.dateCases(),
             DataType.DATETIME,
             DataType.DATETIME,
-            v -> ((Instant) v).toEpochMilli(),
+            v -> DateUtils.toLongMillis((Instant) v),
             emptyList()
             emptyList()
         );
         );
         TestCaseSupplier.unary(
         TestCaseSupplier.unary(

+ 8 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java

@@ -41,7 +41,14 @@ public class ToLongTests extends AbstractScalarFunctionTestCase {
         TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName("Boolean", "bool"), DataType.LONG, b -> b ? 1L : 0L, List.of());
         TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName("Boolean", "bool"), DataType.LONG, b -> b ? 1L : 0L, List.of());
 
 
         // datetimes
         // datetimes
-        TestCaseSupplier.unary(suppliers, read, TestCaseSupplier.dateCases(), DataType.LONG, v -> ((Instant) v).toEpochMilli(), List.of());
+        TestCaseSupplier.unary(
+            suppliers,
+            read,
+            TestCaseSupplier.dateCases(),
+            DataType.LONG,
+            v -> DateUtils.toLongMillis((Instant) v),
+            List.of()
+        );
         TestCaseSupplier.unary(
         TestCaseSupplier.unary(
             suppliers,
             suppliers,
             read,
             read,

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java

@@ -87,7 +87,7 @@ public class ToStringTests extends AbstractScalarFunctionTestCase {
             "ToStringFromDatetimeEvaluator[datetime=" + read + "]",
             "ToStringFromDatetimeEvaluator[datetime=" + read + "]",
             TestCaseSupplier.dateCases(),
             TestCaseSupplier.dateCases(),
             DataType.KEYWORD,
             DataType.KEYWORD,
-            i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(((Instant) i).toEpochMilli())),
+            i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(DateUtils.toLongMillis((Instant) i))),
             List.of()
             List.of()
         );
         );
         TestCaseSupplier.unary(
         TestCaseSupplier.unary(