Skip to content

Commit 478f6d6

Browse files
authored
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 9fb790d commit 478f6d6

File tree

14 files changed

+175
-72
lines changed

14 files changed

+175
-72
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
@@ -777,11 +777,16 @@ class BuildPlugin implements Plugin<Project> {
777777
systemProperty property.getKey(), property.getValue()
778778
}
779779
}
780+
781+
// TODO: remove this once joda time is removed from scriptin in 7.0
782+
systemProperty 'es.scripting.use_java_time', 'true'
783+
780784
// Set the system keystore/truststore password if we're running tests in a FIPS-140 JVM
781785
if (project.inFipsJvm) {
782786
systemProperty 'javax.net.ssl.trustStorePassword', 'password'
783787
systemProperty 'javax.net.ssl.keyStorePassword', 'password'
784788
}
789+
785790
boolean assertionsEnabled = Boolean.parseBoolean(System.getProperty('tests.asserts', 'true'))
786791
enableSystemAssertions assertionsEnabled
787792
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
@@ -198,7 +198,7 @@ POST hockey/player/1/_update
198198
==== Dates
199199

200200
Date fields are exposed as
201-
`ReadableDateTime`
201+
`ReadableDateTime` or
202202
so they support methods like
203203
`getYear`,
204204
and `getDayOfWeek`.
@@ -220,6 +220,11 @@ GET hockey/_search
220220
}
221221
----------------------------------------------------------------
222222
// CONSOLE
223+
// 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]
224+
225+
NOTE: Date fields are changing in 7.0 to be exposed as `ZonedDateTime`
226+
from Java 8's time API. To switch to this functionality early,
227+
add `-Des.scripting.use_java_time=true` to `jvm.options`.
223228

224229
[float]
225230
[[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
@@ -77,8 +77,8 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Longs {
7777
}
7878

7979
class org.elasticsearch.index.fielddata.ScriptDocValues$Dates {
80-
org.joda.time.ReadableDateTime get(int)
81-
org.joda.time.ReadableDateTime getValue()
80+
Object get(int)
81+
Object getValue()
8282
List getValues()
8383
}
8484

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
---
110110
"geo_point":

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

Lines changed: 70 additions & 30 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;
@@ -29,18 +28,23 @@
2928
import org.elasticsearch.common.geo.GeoUtils;
3029
import org.elasticsearch.common.logging.DeprecationLogger;
3130
import org.elasticsearch.common.logging.ESLoggerFactory;
32-
import org.joda.time.DateTime;
3331
import org.joda.time.DateTimeZone;
3432
import org.joda.time.MutableDateTime;
35-
import org.joda.time.ReadableDateTime;
3633

3734
import java.io.IOException;
35+
import java.security.AccessController;
36+
import java.security.PrivilegedAction;
37+
import java.time.Instant;
38+
import java.time.ZoneOffset;
39+
import java.time.ZonedDateTime;
3840
import java.util.AbstractList;
3941
import java.util.Arrays;
4042
import java.util.Comparator;
4143
import java.util.List;
44+
import java.util.function.Consumer;
4245
import java.util.function.UnaryOperator;
4346

47+
import static org.elasticsearch.common.Booleans.parseBoolean;
4448

4549
/**
4650
* Script level doc values, the assumption is that any implementation will
@@ -52,6 +56,7 @@
5256
* values form multiple documents.
5357
*/
5458
public abstract class ScriptDocValues<T> extends AbstractList<T> {
59+
5560
/**
5661
* Set the current doc ID.
5762
*/
@@ -142,31 +147,55 @@ public int size() {
142147
}
143148
}
144149

145-
public static final class Dates extends ScriptDocValues<ReadableDateTime> {
146-
protected static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Dates.class));
150+
public static final class Dates extends ScriptDocValues<Object> {
147151

148-
private static final ReadableDateTime EPOCH = new DateTime(0, DateTimeZone.UTC);
152+
/** Whether scripts should expose dates as java time objects instead of joda time. */
153+
private static final boolean USE_JAVA_TIME = parseBoolean(System.getProperty("es.scripting.use_java_time"), false);
154+
155+
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Dates.class));
149156

150157
private final SortedNumericDocValues in;
158+
159+
/**
160+
* Method call to add deprecation message. Normally this is
161+
* {@link #deprecationLogger} but tests override.
162+
*/
163+
private final Consumer<String> deprecationCallback;
164+
151165
/**
152-
* Values wrapped in {@link MutableDateTime}. Null by default an allocated on first usage so we allocate a reasonably size. We keep
153-
* this array so we don't have allocate new {@link MutableDateTime}s on every usage. Instead we reuse them for every document.
166+
* Whether java time or joda time should be used. This is normally {@link #USE_JAVA_TIME} but tests override it.
154167
*/
155-
private MutableDateTime[] dates;
168+
private final boolean useJavaTime;
169+
170+
/**
171+
* Values wrapped in a date time object. The concrete type depends on the system property {@code es.scripting.use_java_time}.
172+
* When that system property is {@code false}, the date time objects are of type {@link MutableDateTime}. When the system
173+
* property is {@code true}, the date time objects are of type {@link java.time.ZonedDateTime}.
174+
*/
175+
private Object[] dates;
156176
private int count;
157177

158178
/**
159179
* Standard constructor.
160180
*/
161181
public Dates(SortedNumericDocValues in) {
182+
this(in, message -> deprecationLogger.deprecatedAndMaybeLog("scripting_joda_time_deprecation", message), USE_JAVA_TIME);
183+
}
184+
185+
/**
186+
* Constructor for testing with a deprecation callback.
187+
*/
188+
Dates(SortedNumericDocValues in, Consumer<String> deprecationCallback, boolean useJavaTime) {
162189
this.in = in;
190+
this.deprecationCallback = deprecationCallback;
191+
this.useJavaTime = useJavaTime;
163192
}
164193

165194
/**
166195
* Fetch the first field value or 0 millis after epoch if there are no
167196
* in.
168197
*/
169-
public ReadableDateTime getValue() {
198+
public Object getValue() {
170199
if (count == 0) {
171200
throw new IllegalStateException("A document doesn't have a value for a field! " +
172201
"Use doc[<field>].size()==0 to check if a document is missing a field!");
@@ -175,7 +204,7 @@ public ReadableDateTime getValue() {
175204
}
176205

177206
@Override
178-
public ReadableDateTime get(int index) {
207+
public Object get(int index) {
179208
if (index >= count) {
180209
throw new IndexOutOfBoundsException(
181210
"attempted to fetch the [" + index + "] date when there are only ["
@@ -206,31 +235,42 @@ void refreshArray() throws IOException {
206235
if (count == 0) {
207236
return;
208237
}
209-
if (dates == null) {
210-
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
211-
dates = new MutableDateTime[count];
212-
for (int i = 0; i < dates.length; i++) {
213-
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
238+
if (useJavaTime) {
239+
if (dates == null || count > dates.length) {
240+
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
241+
dates = new ZonedDateTime[count];
214242
}
215-
return;
216-
}
217-
if (count > dates.length) {
218-
// Happens when we move to a new document and it has more dates than any documents before it.
219-
MutableDateTime[] backup = dates;
220-
dates = new MutableDateTime[count];
221-
System.arraycopy(backup, 0, dates, 0, backup.length);
222-
for (int i = 0; i < backup.length; i++) {
223-
dates[i].setMillis(in.nextValue());
243+
for (int i = 0; i < count; ++i) {
244+
dates[i] = ZonedDateTime.ofInstant(Instant.ofEpochMilli(in.nextValue()), ZoneOffset.UTC);
224245
}
225-
for (int i = backup.length; i < dates.length; i++) {
246+
} else {
247+
deprecated("The joda time api for doc values is deprecated. Use -Des.scripting.use_java_time=true" +
248+
" to use the java time api for date field doc values");
249+
if (dates == null || count > dates.length) {
250+
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
251+
dates = new MutableDateTime[count];
252+
}
253+
for (int i = 0; i < count; i++) {
226254
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
227255
}
228-
return;
229-
}
230-
for (int i = 0; i < count; i++) {
231-
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
232256
}
233257
}
258+
259+
/**
260+
* Log a deprecation log, with the server's permissions, not the permissions of the
261+
* script calling this method. We need to do this to prevent errors when rolling
262+
* the log file.
263+
*/
264+
private void deprecated(String message) {
265+
// Intentionally not calling SpecialPermission.check because this is supposed to be called by scripts
266+
AccessController.doPrivileged(new PrivilegedAction<Void>() {
267+
@Override
268+
public Void run() {
269+
deprecationCallback.accept(message);
270+
return null;
271+
}
272+
});
273+
}
234274
}
235275

236276
public static final class Doubles extends ScriptDocValues<Double> {

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

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

2020
package org.elasticsearch.script;
2121

22+
import org.elasticsearch.common.settings.ClusterSettings;
23+
import org.elasticsearch.common.settings.Settings;
24+
import org.elasticsearch.plugins.ScriptPlugin;
25+
import org.elasticsearch.search.aggregations.pipeline.movfn.MovingFunctionScript;
26+
2227
import java.util.Collections;
2328
import java.util.HashMap;
2429
import java.util.List;
@@ -27,12 +32,6 @@
2732
import java.util.stream.Collectors;
2833
import java.util.stream.Stream;
2934

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-
35-
3635
/**
3736
* Manages building {@link ScriptService}.
3837
*/

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)