Skip to content

Commit ffd5c1b

Browse files
committed
Scripting: Conditionally use java time api in scripting (#31441)
This commit adds a boolean system property, `es.scripting.use_java_time`, which controls the concrete return type used by doc values within scripts. The return type of accessing doc values for a date field is changed to Object, essentially duck typing the type to allow co-existence during the transition from joda time to java time.
1 parent 578c446 commit ffd5c1b

File tree

15 files changed

+149
-108
lines changed

15 files changed

+149
-108
lines changed

buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,11 +778,16 @@ class BuildPlugin implements Plugin<Project> {
778778
systemProperty property.getKey(), property.getValue()
779779
}
780780
}
781+
782+
// TODO: remove this once joda time is removed from scriptin in 7.0
783+
systemProperty 'es.scripting.use_java_time', 'true'
784+
781785
// Set the system keystore/truststore password if we're running tests in a FIPS-140 JVM
782786
if (project.inFipsJvm) {
783787
systemProperty 'javax.net.ssl.trustStorePassword', 'password'
784788
systemProperty 'javax.net.ssl.keyStorePassword', 'password'
785789
}
790+
786791
boolean assertionsEnabled = Boolean.parseBoolean(System.getProperty('tests.asserts', 'true'))
787792
enableSystemAssertions assertionsEnabled
788793
enableAssertions assertionsEnabled

docs/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ integTestCluster {
3737
extraConfigFile 'hunspell/en_US/en_US.dic', '../server/src/test/resources/indices/analyze/conf_dir/hunspell/en_US/en_US.dic'
3838
// Whitelist reindexing from the local node so we can test it.
3939
setting 'reindex.remote.whitelist', '127.0.0.1:*'
40+
41+
// TODO: remove this for 7.0, this exists to allow the doc examples in 6.x to continue using the defaults
42+
systemProperty 'es.scripting.use_java_time', 'false'
4043
}
4144

4245
// remove when https://github.com/elastic/elasticsearch/issues/31305 is fixed

docs/painless/painless-getting-started.asciidoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ POST hockey/player/1/_update
211211
==== Dates
212212

213213
Date fields are exposed as
214-
`ReadableDateTime`
214+
`ReadableDateTime` or
215215
so they support methods like
216216
`getYear`,
217217
and `getDayOfWeek`.
@@ -233,6 +233,11 @@ GET hockey/_search
233233
}
234234
----------------------------------------------------------------
235235
// CONSOLE
236+
// TEST[warning:The joda time api for doc values is deprecated. Use -Des.scripting.use_java_time=true to use the java time api for date field doc values]
237+
238+
NOTE: Date fields are changing in 7.0 to be exposed as `ZonedDateTime`
239+
from Java 8's time API. To switch to this functionality early,
240+
add `-Des.scripting.use_java_time=true` to `jvm.options`.
236241

237242
[float]
238243
[[modules-scripting-painless-regex]]

docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ POST /sales/_search?size=0
425425
--------------------------------------------------
426426
// CONSOLE
427427
// TEST[setup:sales]
428+
// TEST[warning:The joda time api for doc values is deprecated. Use -Des.scripting.use_java_time=true to use the java time api for date field doc values]
428429

429430
Response:
430431

modules/lang-painless/build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717
* under the License.
1818
*/
1919

20-
21-
2220
esplugin {
2321
description 'An easy, safe and fast scripting language for Elasticsearch'
2422
classname 'org.elasticsearch.painless.PainlessPlugin'
2523
}
2624

2725
integTestCluster {
2826
module project.project(':modules:mapper-extras')
27+
systemProperty 'es.scripting.use_java_time', 'true'
2928
}
3029

3130
dependencies {

modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Longs {
7979
}
8080

8181
class org.elasticsearch.index.fielddata.ScriptDocValues$Dates {
82-
org.joda.time.ReadableDateTime get(int)
83-
org.joda.time.ReadableDateTime getValue()
82+
Object get(int)
83+
Object getValue()
8484
List getValues()
8585
org.joda.time.ReadableDateTime getDate()
8686
List getDates()

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/20_scriptfield.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ setup:
108108
script_fields:
109109
bar:
110110
script:
111-
source: "doc.date.value.dayOfWeek"
111+
source: "doc.date.value.dayOfWeek.value"
112112

113113
- match: { hits.hits.0.fields.bar.0: 7}
114114

@@ -123,7 +123,7 @@ setup:
123123
source: >
124124
StringBuilder b = new StringBuilder();
125125
for (def date : doc.dates) {
126-
b.append(" ").append(date.getDayOfWeek());
126+
b.append(" ").append(date.getDayOfWeek().value);
127127
}
128128
return b.toString().trim()
129129

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ setup:
9595
field:
9696
script:
9797
source: "doc.date.get(0)"
98-
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
98+
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
9999

100100
- do:
101101
search:
@@ -104,7 +104,7 @@ setup:
104104
field:
105105
script:
106106
source: "doc.date.value"
107-
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
107+
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
108108

109109
- do:
110110
warnings:

server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
package org.elasticsearch.index.fielddata;
2121

22-
2322
import org.apache.lucene.index.SortedNumericDocValues;
2423
import org.apache.lucene.util.ArrayUtil;
2524
import org.apache.lucene.util.BytesRef;
@@ -38,13 +37,17 @@
3837
import java.io.IOException;
3938
import java.security.AccessController;
4039
import java.security.PrivilegedAction;
40+
import java.time.Instant;
41+
import java.time.ZoneOffset;
42+
import java.time.ZonedDateTime;
4143
import java.util.AbstractList;
4244
import java.util.Arrays;
4345
import java.util.Comparator;
4446
import java.util.List;
4547
import java.util.function.Consumer;
4648
import java.util.function.UnaryOperator;
4749

50+
import static org.elasticsearch.common.Booleans.parseBoolean;
4851

4952
/**
5053
* Script level doc values, the assumption is that any implementation will
@@ -56,6 +59,7 @@
5659
* values form multiple documents.
5760
*/
5861
public abstract class ScriptDocValues<T> extends AbstractList<T> {
62+
5963
/**
6064
* Set the current doc ID.
6165
*/
@@ -165,20 +169,20 @@ public long getValue() {
165169
}
166170

167171
@Deprecated
168-
public ReadableDateTime getDate() throws IOException {
172+
public Object getDate() throws IOException {
169173
deprecated("getDate on numeric fields is deprecated. Use a date field to get dates.");
170174
if (dates == null) {
171-
dates = new Dates(in);
175+
dates = new Dates(in, deprecationCallback, false);
172176
dates.setNextDocId(docId);
173177
}
174178
return dates.getValue();
175179
}
176180

177181
@Deprecated
178-
public List<ReadableDateTime> getDates() throws IOException {
182+
public List<Object> getDates() throws IOException {
179183
deprecated("getDates on numeric fields is deprecated. Use a date field to get dates.");
180184
if (dates == null) {
181-
dates = new Dates(in);
185+
dates = new Dates(in, deprecationCallback, false);
182186
dates.setNextDocId(docId);
183187
}
184188
return dates;
@@ -211,45 +215,57 @@ public Void run() {
211215
}
212216
}
213217

214-
public static final class Dates extends ScriptDocValues<ReadableDateTime> {
215-
protected static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Dates.class));
218+
public static final class Dates extends ScriptDocValues<Object> {
219+
220+
/** Whether scripts should expose dates as java time objects instead of joda time. */
221+
private static final boolean USE_JAVA_TIME = parseBoolean(System.getProperty("es.scripting.use_java_time"), false);
216222

217223
private static final ReadableDateTime EPOCH = new DateTime(0, DateTimeZone.UTC);
218224

225+
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Dates.class));
226+
219227
private final SortedNumericDocValues in;
228+
220229
/**
221-
* Callback for deprecated fields. In production this should always point to
222-
* {@link #deprecationLogger} but tests will override it so they can test that
223-
* we use the required permissions when calling it.
230+
* Method call to add deprecation message. Normally this is
231+
* {@link #deprecationLogger} but tests override.
224232
*/
225233
private final Consumer<String> deprecationCallback;
234+
235+
/**
236+
* Whether java time or joda time should be used. This is normally {@link #USE_JAVA_TIME} but tests override it.
237+
*/
238+
private final boolean useJavaTime;
239+
226240
/**
227-
* Values wrapped in {@link MutableDateTime}. Null by default an allocated on first usage so we allocate a reasonably size. We keep
228-
* this array so we don't have allocate new {@link MutableDateTime}s on every usage. Instead we reuse them for every document.
241+
* Values wrapped in a date time object. The concrete type depends on the system property {@code es.scripting.use_java_time}.
242+
* When that system property is {@code false}, the date time objects are of type {@link MutableDateTime}. When the system
243+
* property is {@code true}, the date time objects are of type {@link java.time.ZonedDateTime}.
229244
*/
230-
private MutableDateTime[] dates;
245+
private Object[] dates;
231246
private int count;
232247

233248
/**
234249
* Standard constructor.
235250
*/
236251
public Dates(SortedNumericDocValues in) {
237-
this(in, deprecationLogger::deprecated);
252+
this(in, message -> deprecationLogger.deprecatedAndMaybeLog("scripting_joda_time_deprecation", message), USE_JAVA_TIME);
238253
}
239254

240255
/**
241-
* Constructor for testing deprecation logging.
256+
* Constructor for testing with a deprecation callback.
242257
*/
243-
Dates(SortedNumericDocValues in, Consumer<String> deprecationCallback) {
258+
Dates(SortedNumericDocValues in, Consumer<String> deprecationCallback, boolean useJavaTime) {
244259
this.in = in;
245260
this.deprecationCallback = deprecationCallback;
261+
this.useJavaTime = useJavaTime;
246262
}
247263

248264
/**
249265
* Fetch the first field value or 0 millis after epoch if there are no
250266
* in.
251267
*/
252-
public ReadableDateTime getValue() {
268+
public Object getValue() {
253269
if (count == 0) {
254270
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
255271
throw new IllegalStateException("A document doesn't have a value for a field! " +
@@ -264,7 +280,7 @@ public ReadableDateTime getValue() {
264280
* Fetch the first value. Added for backwards compatibility with 5.x when date fields were {@link Longs}.
265281
*/
266282
@Deprecated
267-
public ReadableDateTime getDate() {
283+
public Object getDate() {
268284
deprecated("getDate is no longer necessary on date fields as the value is now a date.");
269285
return getValue();
270286
}
@@ -273,13 +289,13 @@ public ReadableDateTime getDate() {
273289
* Fetch all the values. Added for backwards compatibility with 5.x when date fields were {@link Longs}.
274290
*/
275291
@Deprecated
276-
public List<ReadableDateTime> getDates() {
292+
public List<Object> getDates() {
277293
deprecated("getDates is no longer necessary on date fields as the values are now dates.");
278294
return this;
279295
}
280296

281297
@Override
282-
public ReadableDateTime get(int index) {
298+
public Object get(int index) {
283299
if (index >= count) {
284300
throw new IndexOutOfBoundsException(
285301
"attempted to fetch the [" + index + "] date when there are only ["
@@ -310,29 +326,24 @@ void refreshArray() throws IOException {
310326
if (count == 0) {
311327
return;
312328
}
313-
if (dates == null) {
314-
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
315-
dates = new MutableDateTime[count];
316-
for (int i = 0; i < dates.length; i++) {
317-
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
329+
if (useJavaTime) {
330+
if (dates == null || count > dates.length) {
331+
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
332+
dates = new ZonedDateTime[count];
318333
}
319-
return;
320-
}
321-
if (count > dates.length) {
322-
// Happens when we move to a new document and it has more dates than any documents before it.
323-
MutableDateTime[] backup = dates;
324-
dates = new MutableDateTime[count];
325-
System.arraycopy(backup, 0, dates, 0, backup.length);
326-
for (int i = 0; i < backup.length; i++) {
327-
dates[i].setMillis(in.nextValue());
334+
for (int i = 0; i < count; ++i) {
335+
dates[i] = ZonedDateTime.ofInstant(Instant.ofEpochMilli(in.nextValue()), ZoneOffset.UTC);
328336
}
329-
for (int i = backup.length; i < dates.length; i++) {
337+
} else {
338+
deprecated("The joda time api for doc values is deprecated. Use -Des.scripting.use_java_time=true" +
339+
" to use the java time api for date field doc values");
340+
if (dates == null || count > dates.length) {
341+
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
342+
dates = new MutableDateTime[count];
343+
}
344+
for (int i = 0; i < count; i++) {
330345
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
331346
}
332-
return;
333-
}
334-
for (int i = 0; i < count; i++) {
335-
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
336347
}
337348
}
338349

server/src/main/java/org/elasticsearch/script/ScriptModule.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919

2020
package org.elasticsearch.script;
2121

22+
import org.elasticsearch.common.Booleans;
23+
import org.elasticsearch.common.logging.DeprecationLogger;
24+
import org.elasticsearch.common.logging.Loggers;
25+
import org.elasticsearch.common.settings.ClusterSettings;
26+
import org.elasticsearch.common.settings.Settings;
27+
import org.elasticsearch.plugins.ScriptPlugin;
28+
import org.elasticsearch.search.aggregations.pipeline.movfn.MovingFunctionScript;
29+
2230
import java.util.Collections;
2331
import java.util.HashMap;
2432
import java.util.List;
@@ -27,14 +35,6 @@
2735
import java.util.stream.Collectors;
2836
import java.util.stream.Stream;
2937

30-
import org.elasticsearch.common.settings.ClusterSettings;
31-
import org.elasticsearch.common.settings.Settings;
32-
import org.elasticsearch.plugins.ScriptPlugin;
33-
import org.elasticsearch.search.aggregations.pipeline.movfn.MovingFunctionScript;
34-
import org.elasticsearch.common.Booleans;
35-
import org.elasticsearch.common.logging.DeprecationLogger;
36-
import org.elasticsearch.common.logging.Loggers;
37-
3838
/**
3939
* Manages building {@link ScriptService}.
4040
*/

server/src/main/java/org/elasticsearch/search/aggregations/support/values/ScriptDoubleValues.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import java.io.IOException;
2929
import java.lang.reflect.Array;
30+
import java.time.ZonedDateTime;
3031
import java.util.Collection;
3132

3233
/**
@@ -54,6 +55,9 @@ public boolean advanceExact(int target) throws IOException {
5455
} else if (value instanceof ReadableInstant) {
5556
resize(1);
5657
values[0] = ((ReadableInstant) value).getMillis();
58+
} else if (value instanceof ZonedDateTime) {
59+
resize(1);
60+
values[0] = ((ZonedDateTime) value).toInstant().toEpochMilli();
5761
} else if (value.getClass().isArray()) {
5862
int length = Array.getLength(value);
5963
if (length == 0) {
@@ -89,6 +93,8 @@ private static double toDoubleValue(Object o) {
8993
} else if (o instanceof ReadableInstant) {
9094
// Dates are exposed in scripts as ReadableDateTimes but aggregations want them to be numeric
9195
return ((ReadableInstant) o).getMillis();
96+
} else if (o instanceof ZonedDateTime) {
97+
return ((ZonedDateTime) o).toInstant().toEpochMilli();
9298
} else if (o instanceof Boolean) {
9399
// We do expose boolean fields as boolean in scripts, however aggregations still expect
94100
// that scripts return the same internal representation as regular fields, so boolean

server/src/main/java/org/elasticsearch/search/aggregations/support/values/ScriptLongValues.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import java.io.IOException;
3030
import java.lang.reflect.Array;
31+
import java.time.ZonedDateTime;
3132
import java.util.Collection;
3233
import java.util.Iterator;
3334

@@ -91,6 +92,8 @@ private static long toLongValue(Object o) {
9192
} else if (o instanceof ReadableInstant) {
9293
// Dates are exposed in scripts as ReadableDateTimes but aggregations want them to be numeric
9394
return ((ReadableInstant) o).getMillis();
95+
} else if (o instanceof ZonedDateTime) {
96+
return ((ZonedDateTime) o).toInstant().toEpochMilli();
9497
} else if (o instanceof Boolean) {
9598
// We do expose boolean fields as boolean in scripts, however aggregations still expect
9699
// that scripts return the same internal representation as regular fields, so boolean

0 commit comments

Comments
 (0)