Skip to content

Commit 7794625

Browse files
authored
Scripting: Cache script results if deterministic (#50106)
Cache results from queries that use scripts if they use only deterministic API calls. Nondeterministic API calls are marked in the whitelist with the `@nondeterministic` annotation. Examples are `Math.random()` and `new Date()`. Refs: #49466
1 parent a762c29 commit 7794625

File tree

52 files changed

+1076
-355
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1076
-355
lines changed

build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ task verifyVersions {
205205
* after the backport of the backcompat code is complete.
206206
*/
207207

208-
boolean bwc_tests_enabled = true
209-
final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
208+
boolean bwc_tests_enabled = false
209+
final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50106" /* place a PR link here when committing bwc changes */
210210
if (bwc_tests_enabled == false) {
211211
if (bwc_tests_disabled_issue.isEmpty()) {
212212
throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi.annotation;
21+
22+
public class NonDeterministicAnnotation {
23+
24+
public static final String NAME = "nondeterministic";
25+
26+
public static final NonDeterministicAnnotation INSTANCE = new NonDeterministicAnnotation();
27+
28+
private NonDeterministicAnnotation() {}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi.annotation;
21+
22+
import java.util.Map;
23+
24+
public class NonDeterministicAnnotationParser implements WhitelistAnnotationParser {
25+
26+
public static final NonDeterministicAnnotationParser INSTANCE = new NonDeterministicAnnotationParser();
27+
28+
private NonDeterministicAnnotationParser() {}
29+
30+
@Override
31+
public Object parse(Map<String, String> arguments) {
32+
if (arguments.isEmpty() == false) {
33+
throw new IllegalArgumentException(
34+
"unexpected parameters for [@" + NonDeterministicAnnotation.NAME + "] annotation, found " + arguments
35+
);
36+
}
37+
38+
return NonDeterministicAnnotation.INSTANCE;
39+
}
40+
}

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public interface WhitelistAnnotationParser {
3434
Map<String, WhitelistAnnotationParser> BASE_ANNOTATION_PARSERS = Collections.unmodifiableMap(
3535
Stream.of(
3636
new AbstractMap.SimpleEntry<>(NoImportAnnotation.NAME, NoImportAnnotationParser.INSTANCE),
37-
new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE)
37+
new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE),
38+
new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE)
3839
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
3940
);
4041

modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import org.elasticsearch.painless.spi.Whitelist;
2727
import org.objectweb.asm.util.Printer;
2828

29-
import java.lang.reflect.Constructor;
3029
import java.lang.reflect.Method;
3130
import java.net.MalformedURLException;
3231
import java.net.URL;
@@ -205,13 +204,14 @@ private static void addFactoryMethod(Map<String, Class<?>> additionalClasses, Cl
205204
* @param name The name of the script.
206205
* @param source The source code for the script.
207206
* @param settings The CompilerSettings to be used during the compilation.
208-
* @return An executable script that implements both a specified interface and is a subclass of {@link PainlessScript}
207+
* @return The ScriptRoot used to compile
209208
*/
210-
Constructor<?> compile(Loader loader, Set<String> extractedVariables, String name, String source, CompilerSettings settings) {
209+
ScriptRoot compile(Loader loader, Set<String> extractedVariables, String name, String source,
210+
CompilerSettings settings) {
211211
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
212212
SClass root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, painlessLookup, null);
213213
root.extractVariables(extractedVariables);
214-
root.analyze(painlessLookup, settings);
214+
ScriptRoot scriptRoot = root.analyze(painlessLookup, settings);
215215
Map<String, Object> statics = root.write();
216216

217217
try {
@@ -225,7 +225,7 @@ Constructor<?> compile(Loader loader, Set<String> extractedVariables, String nam
225225
clazz.getField(statik.getKey()).set(null, statik.getValue());
226226
}
227227

228-
return clazz.getConstructors()[0];
228+
return scriptRoot;
229229
} catch (Exception exception) {
230230
// Catch everything to let the user know this is something caused internally.
231231
throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception);

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java

+23-11
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,13 @@ public Loader run() {
143143
});
144144

145145
Set<String> extractedVariables = new HashSet<>();
146-
compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params);
146+
ScriptRoot scriptRoot = compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params);
147147

148148
if (context.statefulFactoryClazz != null) {
149-
return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables));
149+
return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables),
150+
scriptRoot);
150151
} else {
151-
return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE);
152+
return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE, scriptRoot);
152153
}
153154
}
154155

@@ -270,14 +271,16 @@ private <T extends ScriptFactory> Type generateStatefulFactory(
270271
* @param context The {@link ScriptContext}'s semantics are used to define the factory class.
271272
* @param classType The type to be instaniated in the newFactory or newInstance method. Depends
272273
* on whether a {@link ScriptContext#statefulFactoryClazz} is specified.
274+
* @param scriptRoot the {@link ScriptRoot} used to do the compilation
273275
* @param <T> The factory class.
274276
* @return A factory class that will return script instances.
275277
*/
276278
private <T extends ScriptFactory> T generateFactory(
277279
Loader loader,
278280
ScriptContext<T> context,
279281
Set<String> extractedVariables,
280-
Type classType
282+
Type classType,
283+
ScriptRoot scriptRoot
281284
) {
282285
int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS;
283286
int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER| Opcodes.ACC_FINAL;
@@ -330,8 +333,19 @@ private <T extends ScriptFactory> T generateFactory(
330333
adapter.endMethod();
331334

332335
writeNeedsMethods(context.factoryClazz, writer, extractedVariables);
333-
writer.visitEnd();
334336

337+
String methodName = "isResultDeterministic";
338+
org.objectweb.asm.commons.Method isResultDeterministic = new org.objectweb.asm.commons.Method(methodName,
339+
MethodType.methodType(boolean.class).toMethodDescriptorString());
340+
341+
GeneratorAdapter deterAdapter = new GeneratorAdapter(Opcodes.ASM5, isResultDeterministic,
342+
writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, isResultDeterministic.getDescriptor(), null, null));
343+
deterAdapter.visitCode();
344+
deterAdapter.push(scriptRoot.deterministic);
345+
deterAdapter.returnValue();
346+
deterAdapter.endMethod();
347+
348+
writer.visitEnd();
335349
Class<?> factory = loader.defineFactory(className.replace('/', '.'), writer.toByteArray());
336350

337351
try {
@@ -364,19 +378,17 @@ private void writeNeedsMethods(Class<?> clazz, ClassWriter writer, Set<String> e
364378
}
365379
}
366380

367-
void compile(Compiler compiler, Loader loader, Set<String> extractedVariables,
381+
ScriptRoot compile(Compiler compiler, Loader loader, Set<String> extractedVariables,
368382
String scriptName, String source, Map<String, String> params) {
369383
final CompilerSettings compilerSettings = buildCompilerSettings(params);
370384

371385
try {
372386
// Drop all permissions to actually compile the code itself.
373-
AccessController.doPrivileged(new PrivilegedAction<Void>() {
387+
return AccessController.doPrivileged(new PrivilegedAction<ScriptRoot>() {
374388
@Override
375-
public Void run() {
389+
public ScriptRoot run() {
376390
String name = scriptName == null ? source : scriptName;
377-
compiler.compile(loader, extractedVariables, name, source, compilerSettings);
378-
379-
return null;
391+
return compiler.compile(loader, extractedVariables, name, source, compilerSettings);
380392
}
381393
}, COMPILATION_CONTEXT);
382394
// Note that it is safe to catch any of the following errors since Painless is stateless.

modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class ScriptRoot {
3838

3939
protected final FunctionTable functionTable = new FunctionTable();
4040
protected int syntheticCounter = 0;
41+
protected boolean deterministic = true;
4142

4243
public ScriptRoot(PainlessLookup painlessLookup, CompilerSettings compilerSettings, ScriptClassInfo scriptClassInfo, SClass classRoot) {
4344
this.painlessLookup = Objects.requireNonNull(painlessLookup);
@@ -72,4 +73,6 @@ public FunctionTable getFunctionTable() {
7273
public String getNextSyntheticName(String prefix) {
7374
return prefix + "$synthetic$" + syntheticCounter++;
7475
}
76+
77+
public void markNonDeterministic(boolean nondeterministic) { this.deterministic &= !nondeterministic; }
7578
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.lang.reflect.Constructor;
2323
import java.lang.reflect.Method;
2424
import java.util.List;
25+
import java.util.Map;
2526
import java.util.Objects;
2627

2728
public class PainlessClassBinding {
@@ -31,13 +32,16 @@ public class PainlessClassBinding {
3132

3233
public final Class<?> returnType;
3334
public final List<Class<?>> typeParameters;
35+
public final Map<Class<?>, Object> annotations;
3436

35-
PainlessClassBinding(Constructor<?> javaConstructor, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters) {
37+
PainlessClassBinding(Constructor<?> javaConstructor, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters,
38+
Map<Class<?>, Object> annotations) {
3639
this.javaConstructor = javaConstructor;
3740
this.javaMethod = javaMethod;
3841

3942
this.returnType = returnType;
4043
this.typeParameters = typeParameters;
44+
this.annotations = annotations;
4145
}
4246

4347
@Override

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.invoke.MethodType;
2424
import java.lang.reflect.Constructor;
2525
import java.util.List;
26+
import java.util.Map;
2627
import java.util.Objects;
2728

2829
public class PainlessConstructor {
@@ -31,12 +32,15 @@ public class PainlessConstructor {
3132
public final List<Class<?>> typeParameters;
3233
public final MethodHandle methodHandle;
3334
public final MethodType methodType;
35+
public final Map<Class<?>, Object> annotations;
3436

35-
PainlessConstructor(Constructor<?> javaConstructor, List<Class<?>> typeParameters, MethodHandle methodHandle, MethodType methodType) {
37+
PainlessConstructor(Constructor<?> javaConstructor, List<Class<?>> typeParameters, MethodHandle methodHandle, MethodType methodType,
38+
Map<Class<?>, Object> annotations) {
3639
this.javaConstructor = javaConstructor;
3740
this.typeParameters = typeParameters;
3841
this.methodHandle = methodHandle;
3942
this.methodType = methodType;
43+
this.annotations = annotations;
4044
}
4145

4246
@Override
@@ -53,11 +57,12 @@ public boolean equals(Object object) {
5357

5458
return Objects.equals(javaConstructor, that.javaConstructor) &&
5559
Objects.equals(typeParameters, that.typeParameters) &&
56-
Objects.equals(methodType, that.methodType);
60+
Objects.equals(methodType, that.methodType) &&
61+
Objects.equals(annotations, that.annotations);
5762
}
5863

5964
@Override
6065
public int hashCode() {
61-
return Objects.hash(javaConstructor, typeParameters, methodType);
66+
return Objects.hash(javaConstructor, typeParameters, methodType, annotations);
6267
}
6368
}

0 commit comments

Comments
 (0)