浏览代码

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
Stuart Tettemer 5 年之前
父节点
当前提交
779462596b
共有 52 个文件被更改,包括 1076 次插入355 次删除
  1. 2 2
      build.gradle
  2. 29 0
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java
  3. 40 0
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java
  4. 2 1
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java
  5. 5 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java
  6. 23 11
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java
  7. 3 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java
  8. 5 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java
  9. 8 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java
  10. 32 21
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java
  11. 7 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java
  12. 3 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java
  13. 1 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java
  14. 1 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java
  15. 3 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java
  16. 3 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java
  17. 2 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java
  18. 4 4
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.lang.txt
  19. 5 5
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt
  20. 21 21
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt
  21. 26 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java
  22. 2 0
      server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java
  23. 11 2
      server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java
  24. 10 0
      server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java
  25. 18 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java
  26. 19 7
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java
  27. 8 0
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java
  28. 27 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java
  29. 27 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java
  30. 27 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java
  31. 28 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java
  32. 35 11
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java
  33. 29 8
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java
  34. 30 3
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java
  35. 29 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java
  36. 19 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java
  37. 21 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java
  38. 19 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java
  39. 28 3
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java
  40. 19 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java
  41. 13 0
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java
  42. 21 3
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java
  43. 76 6
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java
  44. 18 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java
  45. 19 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java
  46. 19 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java
  47. 18 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java
  48. 39 10
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java
  49. 20 8
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java
  50. 45 0
      test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java
  51. 154 101
      test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java
  52. 3 1
      test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java

+ 2 - 2
build.gradle

@@ -205,8 +205,8 @@ task verifyVersions {
  * after the backport of the backcompat code is complete.
  */
 
-boolean bwc_tests_enabled = true
-final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
+boolean bwc_tests_enabled = false
+final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50106" /* place a PR link here when committing bwc changes */
 if (bwc_tests_enabled == false) {
   if (bwc_tests_disabled_issue.isEmpty()) {
     throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")

+ 29 - 0
modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java

@@ -0,0 +1,29 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.painless.spi.annotation;
+
+public class NonDeterministicAnnotation {
+
+    public static final String NAME = "nondeterministic";
+
+    public static final NonDeterministicAnnotation INSTANCE = new NonDeterministicAnnotation();
+
+    private NonDeterministicAnnotation() {}
+}

+ 40 - 0
modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java

@@ -0,0 +1,40 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.painless.spi.annotation;
+
+import java.util.Map;
+
+public class NonDeterministicAnnotationParser implements WhitelistAnnotationParser {
+
+    public static final NonDeterministicAnnotationParser INSTANCE = new NonDeterministicAnnotationParser();
+
+    private NonDeterministicAnnotationParser() {}
+
+    @Override
+    public Object parse(Map<String, String> arguments) {
+        if (arguments.isEmpty() == false) {
+            throw new IllegalArgumentException(
+                "unexpected parameters for [@" + NonDeterministicAnnotation.NAME + "] annotation, found " + arguments
+            );
+        }
+
+        return NonDeterministicAnnotation.INSTANCE;
+    }
+}

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

@@ -34,7 +34,8 @@ public interface WhitelistAnnotationParser {
     Map<String, WhitelistAnnotationParser> BASE_ANNOTATION_PARSERS = Collections.unmodifiableMap(
             Stream.of(
                     new AbstractMap.SimpleEntry<>(NoImportAnnotation.NAME, NoImportAnnotationParser.INSTANCE),
-                    new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE)
+                    new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE),
+                    new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE)
             ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
     );
 

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

@@ -26,7 +26,6 @@ import org.elasticsearch.painless.node.SClass;
 import org.elasticsearch.painless.spi.Whitelist;
 import org.objectweb.asm.util.Printer;
 
-import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -205,13 +204,14 @@ final class Compiler {
      * @param name The name of the script.
      * @param source The source code for the script.
      * @param settings The CompilerSettings to be used during the compilation.
-     * @return An executable script that implements both a specified interface and is a subclass of {@link PainlessScript}
+     * @return The ScriptRoot used to compile
      */
-    Constructor<?> compile(Loader loader, Set<String> extractedVariables, String name, String source, CompilerSettings settings) {
+    ScriptRoot compile(Loader loader, Set<String> extractedVariables, String name, String source,
+            CompilerSettings settings) {
         ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
         SClass root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, painlessLookup, null);
         root.extractVariables(extractedVariables);
-        root.analyze(painlessLookup, settings);
+        ScriptRoot scriptRoot = root.analyze(painlessLookup, settings);
         Map<String, Object> statics = root.write();
 
         try {
@@ -225,7 +225,7 @@ final class Compiler {
                 clazz.getField(statik.getKey()).set(null, statik.getValue());
             }
 
-            return clazz.getConstructors()[0];
+            return scriptRoot;
         } catch (Exception exception) {
             // Catch everything to let the user know this is something caused internally.
             throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception);

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

@@ -143,12 +143,13 @@ public final class PainlessScriptEngine implements ScriptEngine {
         });
 
         Set<String> extractedVariables = new HashSet<>();
-        compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params);
+        ScriptRoot scriptRoot = compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params);
 
         if (context.statefulFactoryClazz != null) {
-            return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables));
+            return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables),
+                scriptRoot);
         } else {
-            return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE);
+            return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE, scriptRoot);
         }
     }
 
@@ -270,6 +271,7 @@ public final class PainlessScriptEngine implements ScriptEngine {
      * @param context The {@link ScriptContext}'s semantics are used to define the factory class.
      * @param classType The type to be instaniated in the newFactory or newInstance method.  Depends
      *                  on whether a {@link ScriptContext#statefulFactoryClazz} is specified.
+     * @param scriptRoot the {@link ScriptRoot} used to do the compilation
      * @param <T> The factory class.
      * @return A factory class that will return script instances.
      */
@@ -277,7 +279,8 @@ public final class PainlessScriptEngine implements ScriptEngine {
         Loader loader,
         ScriptContext<T> context,
         Set<String> extractedVariables,
-        Type classType
+        Type classType,
+        ScriptRoot scriptRoot
     ) {
         int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS;
         int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER| Opcodes.ACC_FINAL;
@@ -330,8 +333,19 @@ public final class PainlessScriptEngine implements ScriptEngine {
         adapter.endMethod();
 
         writeNeedsMethods(context.factoryClazz, writer, extractedVariables);
-        writer.visitEnd();
 
+        String methodName = "isResultDeterministic";
+        org.objectweb.asm.commons.Method isResultDeterministic = new org.objectweb.asm.commons.Method(methodName,
+            MethodType.methodType(boolean.class).toMethodDescriptorString());
+
+        GeneratorAdapter deterAdapter = new GeneratorAdapter(Opcodes.ASM5, isResultDeterministic,
+            writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, isResultDeterministic.getDescriptor(), null, null));
+        deterAdapter.visitCode();
+        deterAdapter.push(scriptRoot.deterministic);
+        deterAdapter.returnValue();
+        deterAdapter.endMethod();
+
+        writer.visitEnd();
         Class<?> factory = loader.defineFactory(className.replace('/', '.'), writer.toByteArray());
 
         try {
@@ -364,19 +378,17 @@ public final class PainlessScriptEngine implements ScriptEngine {
         }
     }
 
-    void compile(Compiler compiler, Loader loader, Set<String> extractedVariables,
+    ScriptRoot compile(Compiler compiler, Loader loader, Set<String> extractedVariables,
                  String scriptName, String source, Map<String, String> params) {
         final CompilerSettings compilerSettings = buildCompilerSettings(params);
 
         try {
             // Drop all permissions to actually compile the code itself.
-            AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            return AccessController.doPrivileged(new PrivilegedAction<ScriptRoot>() {
                 @Override
-                public Void run() {
+                public ScriptRoot run() {
                     String name = scriptName == null ? source : scriptName;
-                    compiler.compile(loader, extractedVariables, name, source, compilerSettings);
-
-                    return null;
+                    return compiler.compile(loader, extractedVariables, name, source, compilerSettings);
                 }
             }, COMPILATION_CONTEXT);
             // Note that it is safe to catch any of the following errors since Painless is stateless.

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

@@ -38,6 +38,7 @@ public class ScriptRoot {
 
     protected final FunctionTable functionTable = new FunctionTable();
     protected int syntheticCounter = 0;
+    protected boolean deterministic = true;
 
     public ScriptRoot(PainlessLookup painlessLookup, CompilerSettings compilerSettings, ScriptClassInfo scriptClassInfo, SClass classRoot) {
         this.painlessLookup = Objects.requireNonNull(painlessLookup);
@@ -72,4 +73,6 @@ public class ScriptRoot {
     public String getNextSyntheticName(String prefix) {
         return prefix + "$synthetic$" + syntheticCounter++;
     }
+
+    public void markNonDeterministic(boolean nondeterministic) { this.deterministic &= !nondeterministic; }
 }

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

@@ -22,6 +22,7 @@ package org.elasticsearch.painless.lookup;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 public class PainlessClassBinding {
@@ -31,13 +32,16 @@ public class PainlessClassBinding {
 
     public final Class<?> returnType;
     public final List<Class<?>> typeParameters;
+    public final Map<Class<?>, Object> annotations;
 
-    PainlessClassBinding(Constructor<?> javaConstructor, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters) {
+    PainlessClassBinding(Constructor<?> javaConstructor, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters,
+        Map<Class<?>, Object> annotations) {
         this.javaConstructor = javaConstructor;
         this.javaMethod = javaMethod;
 
         this.returnType = returnType;
         this.typeParameters = typeParameters;
+        this.annotations = annotations;
     }
 
     @Override

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

@@ -23,6 +23,7 @@ import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodType;
 import java.lang.reflect.Constructor;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 public class PainlessConstructor {
@@ -31,12 +32,15 @@ public class PainlessConstructor {
     public final List<Class<?>> typeParameters;
     public final MethodHandle methodHandle;
     public final MethodType methodType;
+    public final Map<Class<?>, Object> annotations;
 
-    PainlessConstructor(Constructor<?> javaConstructor, List<Class<?>> typeParameters, MethodHandle methodHandle, MethodType methodType) {
+    PainlessConstructor(Constructor<?> javaConstructor, List<Class<?>> typeParameters, MethodHandle methodHandle, MethodType methodType,
+            Map<Class<?>, Object> annotations) {
         this.javaConstructor = javaConstructor;
         this.typeParameters = typeParameters;
         this.methodHandle = methodHandle;
         this.methodType = methodType;
+        this.annotations = annotations;
     }
 
     @Override
@@ -53,11 +57,12 @@ public class PainlessConstructor {
 
         return Objects.equals(javaConstructor, that.javaConstructor) &&
                 Objects.equals(typeParameters, that.typeParameters) &&
-                Objects.equals(methodType, that.methodType);
+                Objects.equals(methodType, that.methodType) &&
+                Objects.equals(annotations, that.annotations);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(javaConstructor, typeParameters, methodType);
+        return Objects.hash(javaConstructor, typeParameters, methodType, annotations);
     }
 }

+ 32 - 21
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java

@@ -50,6 +50,7 @@ import java.security.PrivilegedAction;
 import java.security.SecureClassLoader;
 import java.security.cert.Certificate;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -132,7 +133,8 @@ public final class PainlessLookupBuilder {
                     for (WhitelistConstructor whitelistConstructor : whitelistClass.whitelistConstructors) {
                         origin = whitelistConstructor.origin;
                         painlessLookupBuilder.addPainlessConstructor(
-                                targetCanonicalClassName, whitelistConstructor.canonicalTypeNameParameters);
+                                targetCanonicalClassName, whitelistConstructor.canonicalTypeNameParameters,
+                                whitelistConstructor.painlessAnnotations);
                     }
 
                     for (WhitelistMethod whitelistMethod : whitelistClass.whitelistMethods) {
@@ -140,7 +142,7 @@ public final class PainlessLookupBuilder {
                         painlessLookupBuilder.addPainlessMethod(
                                 whitelist.classLoader, targetCanonicalClassName, whitelistMethod.augmentedCanonicalClassName,
                                 whitelistMethod.methodName, whitelistMethod.returnCanonicalTypeName,
-                                whitelistMethod.canonicalTypeNameParameters);
+                                whitelistMethod.canonicalTypeNameParameters, whitelistMethod.painlessAnnotations);
                     }
 
                     for (WhitelistField whitelistField : whitelistClass.whitelistFields) {
@@ -155,14 +157,16 @@ public final class PainlessLookupBuilder {
                     painlessLookupBuilder.addImportedPainlessMethod(
                             whitelist.classLoader, whitelistStatic.augmentedCanonicalClassName,
                             whitelistStatic.methodName, whitelistStatic.returnCanonicalTypeName,
-                            whitelistStatic.canonicalTypeNameParameters);
+                            whitelistStatic.canonicalTypeNameParameters,
+                            whitelistStatic.painlessAnnotations);
                 }
 
                 for (WhitelistClassBinding whitelistClassBinding : whitelist.whitelistClassBindings) {
                     origin = whitelistClassBinding.origin;
                     painlessLookupBuilder.addPainlessClassBinding(
                             whitelist.classLoader, whitelistClassBinding.targetJavaClassName, whitelistClassBinding.methodName,
-                            whitelistClassBinding.returnCanonicalTypeName, whitelistClassBinding.canonicalTypeNameParameters);
+                            whitelistClassBinding.returnCanonicalTypeName, whitelistClassBinding.canonicalTypeNameParameters,
+                            whitelistClassBinding.painlessAnnotations);
                 }
 
                 for (WhitelistInstanceBinding whitelistInstanceBinding : whitelist.whitelistInstanceBindings) {
@@ -313,7 +317,8 @@ public final class PainlessLookupBuilder {
         }
     }
 
-    public void addPainlessConstructor(String targetCanonicalClassName, List<String> canonicalTypeNameParameters) {
+    public void addPainlessConstructor(String targetCanonicalClassName, List<String> canonicalTypeNameParameters,
+            Map<Class<?>, Object> annotations) {
         Objects.requireNonNull(targetCanonicalClassName);
         Objects.requireNonNull(canonicalTypeNameParameters);
 
@@ -337,10 +342,10 @@ public final class PainlessLookupBuilder {
             typeParameters.add(typeParameter);
         }
 
-        addPainlessConstructor(targetClass, typeParameters);
+        addPainlessConstructor(targetClass, typeParameters, annotations);
     }
 
-    public void addPainlessConstructor(Class<?> targetClass, List<Class<?>> typeParameters) {
+    public void addPainlessConstructor(Class<?> targetClass, List<Class<?>> typeParameters, Map<Class<?>, Object> annotations) {
         Objects.requireNonNull(targetClass);
         Objects.requireNonNull(typeParameters);
 
@@ -390,7 +395,8 @@ public final class PainlessLookupBuilder {
 
         String painlessConstructorKey = buildPainlessConstructorKey(typeParametersSize);
         PainlessConstructor existingPainlessConstructor = painlessClassBuilder.constructors.get(painlessConstructorKey);
-        PainlessConstructor newPainlessConstructor = new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType);
+        PainlessConstructor newPainlessConstructor = new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType,
+            annotations);
 
         if (existingPainlessConstructor == null) {
             newPainlessConstructor = painlessConstructorCache.computeIfAbsent(newPainlessConstructor, key -> key);
@@ -403,7 +409,8 @@ public final class PainlessLookupBuilder {
     }
 
     public void addPainlessMethod(ClassLoader classLoader, String targetCanonicalClassName, String augmentedCanonicalClassName,
-            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
+            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters,
+            Map<Class<?>, Object> annotations) {
 
         Objects.requireNonNull(classLoader);
         Objects.requireNonNull(targetCanonicalClassName);
@@ -449,11 +456,11 @@ public final class PainlessLookupBuilder {
                     "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
         }
 
-        addPainlessMethod(targetClass, augmentedClass, methodName, returnType, typeParameters);
+        addPainlessMethod(targetClass, augmentedClass, methodName, returnType, typeParameters, annotations);
     }
 
     public void addPainlessMethod(Class<?> targetClass, Class<?> augmentedClass,
-            String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
+            String methodName, Class<?> returnType, List<Class<?>> typeParameters, Map<Class<?>, Object> annotations) {
 
         Objects.requireNonNull(targetClass);
         Objects.requireNonNull(methodName);
@@ -562,7 +569,7 @@ public final class PainlessLookupBuilder {
                 painlessClassBuilder.staticMethods.get(painlessMethodKey) :
                 painlessClassBuilder.methods.get(painlessMethodKey);
         PainlessMethod newPainlessMethod =
-                new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType);
+                new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType, annotations);
 
         if (existingPainlessMethod == null) {
             newPainlessMethod = painlessMethodCache.computeIfAbsent(newPainlessMethod, key -> key);
@@ -708,7 +715,8 @@ public final class PainlessLookupBuilder {
     }
 
     public void addImportedPainlessMethod(ClassLoader classLoader, String targetJavaClassName,
-            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
+            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters,
+            Map<Class<?>, Object> annotations) {
 
         Objects.requireNonNull(classLoader);
         Objects.requireNonNull(targetJavaClassName);
@@ -751,10 +759,11 @@ public final class PainlessLookupBuilder {
                     "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
         }
 
-        addImportedPainlessMethod(targetClass, methodName, returnType, typeParameters);
+        addImportedPainlessMethod(targetClass, methodName, returnType, typeParameters, annotations);
     }
 
-    public void addImportedPainlessMethod(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
+    public void addImportedPainlessMethod(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters,
+            Map<Class<?>, Object> annotations) {
         Objects.requireNonNull(targetClass);
         Objects.requireNonNull(methodName);
         Objects.requireNonNull(returnType);
@@ -841,7 +850,7 @@ public final class PainlessLookupBuilder {
 
         PainlessMethod existingImportedPainlessMethod = painlessMethodKeysToImportedPainlessMethods.get(painlessMethodKey);
         PainlessMethod newImportedPainlessMethod =
-                new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType);
+                new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType, annotations);
 
         if (existingImportedPainlessMethod == null) {
             newImportedPainlessMethod = painlessMethodCache.computeIfAbsent(newImportedPainlessMethod, key -> key);
@@ -859,7 +868,8 @@ public final class PainlessLookupBuilder {
     }
 
     public void addPainlessClassBinding(ClassLoader classLoader, String targetJavaClassName,
-            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
+            String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters,
+            Map<Class<?>, Object> annotations) {
 
         Objects.requireNonNull(classLoader);
         Objects.requireNonNull(targetJavaClassName);
@@ -896,10 +906,11 @@ public final class PainlessLookupBuilder {
                     "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
         }
 
-        addPainlessClassBinding(targetClass, methodName, returnType, typeParameters);
+        addPainlessClassBinding(targetClass, methodName, returnType, typeParameters, annotations);
     }
 
-    public void addPainlessClassBinding(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
+    public void addPainlessClassBinding(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters,
+            Map<Class<?>, Object> annotations) {
         Objects.requireNonNull(targetClass);
         Objects.requireNonNull(methodName);
         Objects.requireNonNull(returnType);
@@ -1036,7 +1047,7 @@ public final class PainlessLookupBuilder {
 
         PainlessClassBinding existingPainlessClassBinding = painlessMethodKeysToPainlessClassBindings.get(painlessMethodKey);
         PainlessClassBinding newPainlessClassBinding =
-                new PainlessClassBinding(javaConstructor, javaMethod, returnType, typeParameters);
+                new PainlessClassBinding(javaConstructor, javaMethod, returnType, typeParameters, annotations);
 
         if (existingPainlessClassBinding == null) {
             newPainlessClassBinding = painlessClassBindingCache.computeIfAbsent(newPainlessClassBinding, key -> key);
@@ -1444,7 +1455,7 @@ public final class PainlessLookupBuilder {
                         painlessMethod.javaMethod.getName(), bridgeTypeParameters.toArray(new Class<?>[0]));
                 MethodHandle bridgeHandle = MethodHandles.publicLookup().in(bridgeClass).unreflect(bridgeClass.getMethods()[0]);
                 bridgePainlessMethod = new PainlessMethod(bridgeMethod, bridgeClass,
-                        painlessMethod.returnType, bridgeTypeParameters, bridgeHandle, bridgeMethodType);
+                        painlessMethod.returnType, bridgeTypeParameters, bridgeHandle, bridgeMethodType, Collections.emptyMap());
                 painlessClassBuilder.runtimeMethods.put(painlessMethodKey.intern(), bridgePainlessMethod);
                 painlessBridgeCache.put(painlessMethod, bridgePainlessMethod);
             } catch (Exception exception) {

+ 7 - 3
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java

@@ -23,6 +23,7 @@ import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodType;
 import java.lang.reflect.Method;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 public class PainlessMethod {
@@ -33,9 +34,10 @@ public class PainlessMethod {
     public final List<Class<?>> typeParameters;
     public final MethodHandle methodHandle;
     public final MethodType methodType;
+    public final Map<Class<?>, Object> annotations;
 
     public PainlessMethod(Method javaMethod, Class<?> targetClass, Class<?> returnType, List<Class<?>> typeParameters,
-            MethodHandle methodHandle, MethodType methodType) {
+            MethodHandle methodHandle, MethodType methodType, Map<Class<?>, Object> annotations) {
 
         this.javaMethod = javaMethod;
         this.targetClass = targetClass;
@@ -43,6 +45,7 @@ public class PainlessMethod {
         this.typeParameters = List.copyOf(typeParameters);
         this.methodHandle = methodHandle;
         this.methodType = methodType;
+        this.annotations = annotations;
     }
 
     @Override
@@ -61,11 +64,12 @@ public class PainlessMethod {
                 Objects.equals(targetClass, that.targetClass) &&
                 Objects.equals(returnType, that.returnType) &&
                 Objects.equals(typeParameters, that.typeParameters) &&
-                Objects.equals(methodType, that.methodType);
+                Objects.equals(methodType, that.methodType) &&
+                Objects.equals(annotations, that.annotations);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(javaMethod, targetClass, returnType, typeParameters, methodType);
+        return Objects.hash(javaMethod, targetClass, returnType, typeParameters, methodType, annotations);
     }
 }

+ 3 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java

@@ -28,6 +28,7 @@ import org.elasticsearch.painless.ScriptRoot;
 import org.elasticsearch.painless.lookup.PainlessClassBinding;
 import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
 import org.elasticsearch.painless.lookup.PainlessMethod;
+import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation;
 import org.elasticsearch.painless.symbol.FunctionTable;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.Type;
@@ -127,9 +128,11 @@ public final class ECallLocal extends AExpression {
             typeParameters = new ArrayList<>(localFunction.getTypeParameters());
             actual = localFunction.getReturnType();
         } else if (importedMethod != null) {
+            scriptRoot.markNonDeterministic(importedMethod.annotations.containsKey(NonDeterministicAnnotation.class));
             typeParameters = new ArrayList<>(importedMethod.typeParameters);
             actual = importedMethod.returnType;
         } else if (classBinding != null) {
+            scriptRoot.markNonDeterministic(classBinding.annotations.containsKey(NonDeterministicAnnotation.class));
             typeParameters = new ArrayList<>(classBinding.typeParameters);
             actual = classBinding.returnType;
             bindingName = scriptRoot.getNextSyntheticName("class_binding");

+ 1 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java

@@ -37,7 +37,7 @@ import java.util.Objects;
 import java.util.Set;
 
 /**
- * Represents a capturing function reference.
+ * Represents a capturing function reference.  For member functions that require a this reference, ie not static.
  */
 public final class ECapturingFunctionRef extends AExpression implements ILambda {
     private final String variable;

+ 1 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java

@@ -32,7 +32,7 @@ import java.util.Objects;
 import java.util.Set;
 
 /**
- * Represents a cast that is inserted into the tree replacing other casts.  (Internal only.)
+ * Represents a cast that is inserted into the tree replacing other casts.  (Internal only.)  Casts are inserted during semantic checking.
  */
 final class ECast extends AExpression {
 

+ 3 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java

@@ -27,6 +27,7 @@ import org.elasticsearch.painless.MethodWriter;
 import org.elasticsearch.painless.ScriptRoot;
 import org.elasticsearch.painless.lookup.PainlessConstructor;
 import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.commons.Method;
 
@@ -75,6 +76,8 @@ public final class ENewObj extends AExpression {
                     "constructor [" + typeToCanonicalTypeName(actual) + ", <init>/" + arguments.size() + "] not found"));
         }
 
+        scriptRoot.markNonDeterministic(constructor.annotations.containsKey(NonDeterministicAnnotation.class));
+
         Class<?>[] types = new Class<?>[constructor.typeParameters.size()];
         constructor.typeParameters.toArray(types);
 

+ 3 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java

@@ -27,6 +27,7 @@ import org.elasticsearch.painless.MethodWriter;
 import org.elasticsearch.painless.ScriptRoot;
 import org.elasticsearch.painless.lookup.PainlessMethod;
 import org.elasticsearch.painless.lookup.def;
+import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation;
 
 import java.util.List;
 import java.util.Objects;
@@ -79,6 +80,8 @@ public final class PCallInvoke extends AExpression {
                         "method [" + typeToCanonicalTypeName(prefix.actual) + ", " + name + "/" + arguments.size() + "] not found"));
             }
 
+            scriptRoot.markNonDeterministic(method.annotations.containsKey(NonDeterministicAnnotation.class));
+
             sub = new PSubCallInvoke(location, method, prefix.actual, arguments);
         }
 

+ 2 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java

@@ -130,7 +130,7 @@ public final class SClass extends AStatement {
         extractedVariables.addAll(variables);
     }
 
-    public void analyze(PainlessLookup painlessLookup, CompilerSettings settings) {
+    public ScriptRoot analyze(PainlessLookup painlessLookup, CompilerSettings settings) {
         this.settings = settings;
         table = new ScriptRoot(painlessLookup, settings, scriptClassInfo, this);
 
@@ -148,6 +148,7 @@ public final class SClass extends AStatement {
 
         Locals locals = Locals.newProgramScope();
         analyze(table, locals);
+        return table;
     }
 
     @Override

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

@@ -636,7 +636,7 @@ class java.lang.Math {
   double nextDown(double)
   double nextUp(double)
   double pow(double,double)
-  double random()
+  double random() @nondeterministic
   double rint(double)
   long round(double)
   double scalb(double,int)
@@ -729,7 +729,7 @@ class java.lang.StrictMath {
   double nextDown(double)
   double nextUp(double)
   double pow(double,double)
-  double random()
+  double random() @nondeterministic
   double rint(double)
   long round(double)
   double scalb(double,int)
@@ -844,8 +844,8 @@ class java.lang.StringBuilder {
 
 class java.lang.System {
   void arraycopy(Object,int,Object,int,int)
-  long currentTimeMillis()
-  long nanoTime()
+  long currentTimeMillis() @nondeterministic
+  long nanoTime() @nondeterministic
 }
 
 # Thread: skipped

+ 5 - 5
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt

@@ -26,11 +26,11 @@
 
 class java.time.Clock {
   Clock fixed(Instant,ZoneId)
-  ZoneId getZone()
-  Instant instant()
-  long millis()
-  Clock offset(Clock,Duration)
-  Clock tick(Clock,Duration)
+  ZoneId getZone() @nondeterministic
+  Instant instant() @nondeterministic
+  long millis() @nondeterministic
+  Clock offset(Clock,Duration) @nondeterministic
+  Clock tick(Clock,Duration) @nondeterministic
 }
 
 class java.time.Duration {

+ 21 - 21
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt

@@ -490,9 +490,9 @@ class java.util.Calendar {
   Map getDisplayNames(int,int,Locale)
   int getFirstDayOfWeek()
   int getGreatestMinimum(int)
-  Calendar getInstance()
-  Calendar getInstance(TimeZone)
-  Calendar getInstance(TimeZone,Locale)
+  Calendar getInstance() @nondeterministic
+  Calendar getInstance(TimeZone) @nondeterministic
+  Calendar getInstance(TimeZone,Locale) @nondeterministic
   int getLeastMaximum(int)
   int getMaximum(int)
   int getMinimalDaysInFirstWeek()
@@ -574,7 +574,7 @@ class java.util.Collections {
   Comparator reverseOrder()
   Comparator reverseOrder(Comparator)
   void rotate(List,int)
-  void shuffle(List)
+  void shuffle(List) @nondeterministic
   void shuffle(List,Random)
   Set singleton(def)
   List singletonList(def)
@@ -605,7 +605,7 @@ class java.util.Currency {
 }
 
 class java.util.Date {
-  ()
+  () @nondeterministic
   (long)
   boolean after(Date)
   boolean before(Date)
@@ -910,22 +910,22 @@ class java.util.PriorityQueue {
 }
 
 class java.util.Random {
-  ()
+  () @nondeterministic
   (long)
-  DoubleStream doubles(long)
-  DoubleStream doubles(long,double,double)
-  IntStream ints(long)
-  IntStream ints(long,int,int)
-  LongStream longs(long)
-  LongStream longs(long,long,long)
-  boolean nextBoolean()
-  void nextBytes(byte[])
-  double nextDouble()
-  float nextFloat()
-  double nextGaussian()
-  int nextInt()
-  int nextInt(int)
-  long nextLong()
+  DoubleStream doubles(long) @nondeterministic
+  DoubleStream doubles(long,double,double) @nondeterministic
+  IntStream ints(long) @nondeterministic
+  IntStream ints(long,int,int) @nondeterministic
+  LongStream longs(long) @nondeterministic
+  LongStream longs(long,long,long) @nondeterministic
+  boolean nextBoolean() @nondeterministic
+  void nextBytes(byte[]) @nondeterministic
+  double nextDouble() @nondeterministic
+  float nextFloat() @nondeterministic
+  double nextGaussian() @nondeterministic
+  int nextInt() @nondeterministic
+  int nextInt(int) @nondeterministic
+  long nextLong() @nondeterministic
   void setSeed(long)
 }
 
@@ -1031,7 +1031,7 @@ class java.util.UUID {
   UUID fromString(String)
   long getLeastSignificantBits()
   long getMostSignificantBits()
-  UUID randomUUID()
+  UUID randomUUID() @nondeterministic
   UUID nameUUIDFromBytes(byte[])
   long node()
   long timestamp()

+ 26 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java

@@ -162,6 +162,32 @@ public class FactoryTests extends ScriptTestCase {
         assertEquals(false, factory.needsNothing());
     }
 
+    public void testDeterministic() {
+        FactoryTestScript.Factory factory =
+            scriptEngine.compile("deterministic_test", "Integer.parseInt('123')",
+                FactoryTestScript.CONTEXT, Collections.emptyMap());
+        assertTrue(factory.isResultDeterministic());
+        assertEquals(123, factory.newInstance(Collections.emptyMap()).execute(0));
+    }
+
+    public void testNotDeterministic() {
+        FactoryTestScript.Factory factory =
+            scriptEngine.compile("not_deterministic_test", "Math.random()",
+                FactoryTestScript.CONTEXT, Collections.emptyMap());
+        assertFalse(factory.isResultDeterministic());
+        Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0);
+        assertTrue(d >= 0.0 && d <= 1.0);
+    }
+
+    public void testMixedDeterministicIsNotDeterministic() {
+        FactoryTestScript.Factory factory =
+            scriptEngine.compile("not_deterministic_test", "Integer.parseInt('123') + Math.random()",
+                FactoryTestScript.CONTEXT, Collections.emptyMap());
+        assertFalse(factory.isResultDeterministic());
+        Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0);
+        assertTrue(d >= 123.0 && d <= 124.0);
+    }
+
     public abstract static class EmptyTestScript {
         public static final String[] PARAMETERS = {};
         public abstract Object execute();

+ 2 - 0
server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java

@@ -325,6 +325,8 @@ public class QueryShardContext extends QueryRewriteContext {
     /** Compile script using script service */
     public <FactoryType extends ScriptFactory> FactoryType compile(Script script, ScriptContext<FactoryType> context) {
         FactoryType factory = scriptService.compile(script, context);
+        // TODO(stu): can check `factory instanceof ScriptFactory && ((ScriptFactory) factory).isResultDeterministic() == false`
+        // to avoid being so intrusive
         if (factory.isResultDeterministic() == false) {
             failIfFrozen();
         }

+ 11 - 2
server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.search.aggregations.support;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -80,7 +81,11 @@ public class MultiValuesSourceFieldConfig implements Writeable, ToXContentObject
     }
 
     public MultiValuesSourceFieldConfig(StreamInput in) throws IOException {
-        this.fieldName = in.readString();
+        if (in.getVersion().onOrAfter(Version.V_7_6_0)) {
+            this.fieldName = in.readOptionalString();
+        } else {
+            this.fieldName = in.readString();
+        }
         this.missing = in.readGenericValue();
         this.script = in.readOptionalWriteable(Script::new);
         this.timeZone = in.readOptionalZoneId();
@@ -104,7 +109,11 @@ public class MultiValuesSourceFieldConfig implements Writeable, ToXContentObject
 
     @Override
     public void writeTo(StreamOutput out) throws IOException {
-        out.writeString(fieldName);
+        if (out.getVersion().onOrAfter(Version.V_7_6_0)) {
+            out.writeOptionalString(fieldName);
+        } else {
+            out.writeString(fieldName);
+        }
         out.writeGenericValue(missing);
         out.writeOptionalWriteable(script);
         out.writeOptionalZoneId(timeZone);

+ 10 - 0
server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java

@@ -23,6 +23,7 @@ import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.script.MockScriptPlugin;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptType;
+import org.elasticsearch.test.ESTestCase;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -116,4 +117,13 @@ public class AggregationTestScriptsPlugin extends MockScriptPlugin {
 
         return scripts;
     }
+
+    @Override
+    protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+        Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+        scripts.put("Math.random()", vars -> ESTestCase.randomDouble());
+
+        return scripts;
+    }
 }

+ 18 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java

@@ -1466,10 +1466,10 @@ public class DateHistogramIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=date")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -1484,10 +1484,21 @@ public class DateHistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         Map<String, Object> params = new HashMap<>();
         params.put("fieldname", "d");
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateHistogram("histo").field("d")
+            .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.CURRENT_DATE, params))
+            .dateHistogramInterval(DateHistogramInterval.MONTH)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(0L));
+
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateHistogram("histo").field("d")
                 .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.LONG_PLUS_ONE_MONTH, params))
                 .dateHistogramInterval(DateHistogramInterval.MONTH)).get();
         assertSearchResponse(r);
@@ -1495,10 +1506,9 @@ public class DateHistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(0L));
+                .getMissCount(), equalTo(1L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Ensure that non-scripted requests are cached as normal
         r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(dateHistogram("histo").field("d").dateHistogramInterval(DateHistogramInterval.MONTH)).get();
         assertSearchResponse(r);
@@ -1506,7 +1516,7 @@ public class DateHistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(1L));
+                .getMissCount(), equalTo(2L));
     }
 
     public void testSingleValuedFieldOrderedBySingleValueSubAggregationAscAndKeyDesc() throws Exception {

+ 19 - 7
server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java

@@ -884,10 +884,10 @@ public class DateRangeIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "date", "type=date")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -903,11 +903,11 @@ public class DateRangeIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         Map<String, Object> params = new HashMap<>();
         params.put("fieldname", "date");
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date")
-                .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.DOUBLE_PLUS_ONE_MONTH, params))
+                .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.CURRENT_DATE, params))
                 .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC),
                     ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)))
                 .get();
@@ -918,9 +918,9 @@ public class DateRangeIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
         r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date")
+                .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.DOUBLE_PLUS_ONE_MONTH, params))
                 .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC),
                     ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)))
                 .get();
@@ -930,6 +930,18 @@ public class DateRangeIT extends ESIntegTestCase {
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date")
+                .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC),
+                    ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)))
+                .get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
     /**

+ 8 - 0
server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java

@@ -38,6 +38,7 @@ public class DateScriptMocksPlugin extends MockScriptPlugin {
     static final String EXTRACT_FIELD = "extract_field";
     static final String DOUBLE_PLUS_ONE_MONTH = "double_date_plus_1_month";
     static final String LONG_PLUS_ONE_MONTH = "long_date_plus_1_month";
+    static final String CURRENT_DATE = "current_date";
 
     @Override
     public Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
@@ -53,4 +54,11 @@ public class DateScriptMocksPlugin extends MockScriptPlugin {
             new DateTime((long) params.get("_value"), DateTimeZone.UTC).plusMonths(1).getMillis());
         return scripts;
     }
+
+    @Override
+    protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+        Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+        scripts.put(CURRENT_DATE, params -> new DateTime().getMillis());
+        return scripts;
+    }
 }

+ 27 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java

@@ -112,6 +112,15 @@ public class DoubleTermsIT extends AbstractTermsTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> DoubleTermsIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     private static final int NUM_DOCS = 5; // TODO: randomize the size?
@@ -917,10 +926,10 @@ public class DoubleTermsIT extends AbstractTermsTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=float")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -933,10 +942,10 @@ public class DoubleTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
                 terms("terms").field("d").script(
-                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -944,14 +953,24 @@ public class DoubleTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
+            terms("terms").field("d").script(
+                new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
         r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(1L));
+                .getMissCount(), equalTo(2L));
     }
 }

+ 27 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java

@@ -110,6 +110,15 @@ public class HistogramIT extends ESIntegTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> HistogramIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     @Override
@@ -1102,10 +1111,10 @@ public class HistogramIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=float")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -1118,9 +1127,10 @@ public class HistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d")
-                .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", emptyMap())).interval(0.7).offset(0.05)).get();
+                .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", emptyMap())).interval(0.7).offset(0.05))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -1128,8 +1138,17 @@ public class HistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d")
+                .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", emptyMap())).interval(0.7).offset(0.05)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
         r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d").interval(0.7).offset(0.05))
                 .get();
         assertSearchResponse(r);
@@ -1137,7 +1156,7 @@ public class HistogramIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(1L));
+                .getMissCount(), equalTo(2L));
     }
 
     public void testSingleValuedFieldOrderedBySingleValueSubAggregationAscAndKeyDesc() throws Exception {

+ 27 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java

@@ -99,6 +99,15 @@ public class LongTermsIT extends AbstractTermsTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> LongTermsIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     private static final int NUM_DOCS = 5; // TODO randomize the size?
@@ -894,10 +903,10 @@ public class LongTermsIT extends AbstractTermsTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -910,10 +919,10 @@ public class LongTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
                 terms("terms").field("d").script(
-                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -921,14 +930,24 @@ public class LongTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
+                terms("terms").field("d").script(
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 }

+ 28 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java

@@ -92,6 +92,15 @@ public class RangeIT extends ESIntegTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> RangeIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     @Override
@@ -945,10 +954,10 @@ public class RangeIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "i", "type=integer")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -962,12 +971,12 @@ public class RangeIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         Map<String, Object> params = new HashMap<>();
         params.put("fieldname", "date");
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
                 range("foo").field("i").script(
-                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap())).addRange(0, 10))
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap())).addRange(0, 10))
                 .get();
         assertSearchResponse(r);
 
@@ -976,15 +985,26 @@ public class RangeIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(range("foo").field("i").addRange(0, 10)).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
+                range("foo").field("i").script(
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap())).addRange(0, 10))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(range("foo").field("i").addRange(0, 10)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
     public void testFieldAlias() {

+ 35 - 11
server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java

@@ -193,6 +193,15 @@ public class SignificantTermsSignificanceScoreIT extends ESIntegTestCase {
             return scripts;
         }
 
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> SignificantTermsSignificanceScoreIT.randomDouble());
+
+            return scripts;
+        }
+
         private static long longValue(Object value) {
             return ((ScriptHeuristic.LongAccessor) value).longValue();
         }
@@ -678,10 +687,10 @@ public class SignificantTermsSignificanceScoreIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -694,8 +703,10 @@ public class SignificantTermsSignificanceScoreIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
-        ScriptHeuristic scriptHeuristic = getScriptSignificanceHeuristic();
+        // Test that a request using a nondeterministic script does not get cached
+        ScriptHeuristic scriptHeuristic = new ScriptHeuristic(
+            new Script(ScriptType.INLINE, "mockscript", "Math.random()", Collections.emptyMap())
+        );
         boolean useSigText = randomBoolean();
         SearchResponse r;
         if (useSigText) {
@@ -712,12 +723,15 @@ public class SignificantTermsSignificanceScoreIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
+        scriptHeuristic = getScriptSignificanceHeuristic();
+        useSigText = randomBoolean();
         if (useSigText) {
-            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantText("foo", "s")).get();
+            r = client().prepareSearch("cache_test_idx").setSize(0)
+                    .addAggregation(significantText("foo", "s").significanceHeuristic(scriptHeuristic)).get();
         } else {
-            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantTerms("foo").field("s")).get();
+            r = client().prepareSearch("cache_test_idx").setSize(0)
+                    .addAggregation(significantTerms("foo").field("s").significanceHeuristic(scriptHeuristic)).get();
         }
         assertSearchResponse(r);
 
@@ -725,8 +739,18 @@ public class SignificantTermsSignificanceScoreIT extends ESIntegTestCase {
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
-    }
-
 
+        // Ensure that non-scripted requests are cached as normal
+        if (useSigText) {
+            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantText("foo", "s")).get();
+        } else {
+            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantTerms("foo").field("s")).get();
+        }
+        assertSearchResponse(r);
 
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
+    }
 }

+ 29 - 8
server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java

@@ -119,6 +119,15 @@ public class StringTermsIT extends AbstractTermsTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> StringTermsIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     @Override
@@ -1115,10 +1124,10 @@ public class StringTermsIT extends AbstractTermsTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=keyword")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -1131,11 +1140,11 @@ public class StringTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(
                         terms("terms").field("d").script(
-                            new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "'foo_' + _value", Collections.emptyMap())))
+                            new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -1144,14 +1153,26 @@ public class StringTermsIT extends AbstractTermsTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(
+                        terms("terms").field("d").script(
+                            new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "'foo_' + _value", Collections.emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 }

+ 30 - 3
server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java

@@ -83,6 +83,9 @@ public class AvgAggregatorTests extends AggregatorTestCase {
     /** Script to return the {@code _value} provided by aggs framework. */
     public static final String VALUE_SCRIPT = "_value";
 
+    /** Script to return a random double */
+    public static final String RANDOM_SCRIPT = "Math.random()";
+
     @Override
     protected ScriptService getMockScriptService() {
         Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
@@ -115,8 +118,12 @@ public class AvgAggregatorTests extends AggregatorTestCase {
             return ((Number) vars.get("_value")).doubleValue() + inc;
         });
 
+        Map<String, Function<Map<String, Object>, Object>> nonDeterministicScripts = new HashMap<>();
+        nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> AvgAggregatorTests.randomDouble());
+
         MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
             scripts,
+            nonDeterministicScripts,
             Collections.emptyMap());
         Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
 
@@ -638,9 +645,10 @@ public class AvgAggregatorTests extends AggregatorTestCase {
     }
 
     /**
-     * Make sure that an aggregation using a script does not get cached.
+     * Make sure that an aggregation using a deterministic script does gets cached while
+     * one using a nondeterministic script does not.
      */
-    public void testDontCacheScripts() throws IOException {
+    public void testScriptCaching() throws IOException {
         Directory directory = newDirectory();
         RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
         final int numDocs = 10;
@@ -675,7 +683,26 @@ public class AvgAggregatorTests extends AggregatorTestCase {
         assertEquals("avg", avg.getName());
         assertTrue(AggregationInspectionHelper.hasValue(avg));
 
-        // Test that an aggregation using a script does not get cached
+        // Test that an aggregation using a deterministic script gets cached
+        assertTrue(aggregator.context().getQueryShardContext().isCacheable());
+
+        aggregationBuilder = new AvgAggregationBuilder("avg")
+            .field("value")
+            .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap()));
+
+        aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType);
+        aggregator.preCollection();
+        indexSearcher.search(new MatchAllDocsQuery(), aggregator);
+        aggregator.postCollection();
+
+        avg = (InternalAvg) aggregator.buildAggregation(0L);
+
+        assertTrue(avg.getValue() >= 0.0);
+        assertTrue(avg.getValue() <= 1.0);
+        assertEquals("avg", avg.getName());
+        assertTrue(AggregationInspectionHelper.hasValue(avg));
+
+        // Test that an aggregation using a nondeterministic script does not get cached
         assertFalse(aggregator.context().getQueryShardContext().isCacheable());
 
         multiReader.close();

+ 29 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java

@@ -90,6 +90,15 @@ public class CardinalityIT extends ESIntegTestCase {
 
             return scripts;
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("Math.random()", vars -> CardinalityIT.randomDouble());
+
+            return scripts;
+        }
     }
 
     @Override
@@ -449,10 +458,10 @@ public class CardinalityIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -465,10 +474,11 @@ public class CardinalityIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(
-                        cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value", emptyMap())))
+                        cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()",
+                                                                              emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -477,14 +487,25 @@ public class CardinalityIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(cardinality("foo").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(
+                        cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value", emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(cardinality("foo").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 }

+ 19 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java

@@ -639,10 +639,10 @@ public class ExtendedStatsIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -655,10 +655,10 @@ public class ExtendedStatsIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(extendedStats("foo").field("d")
-                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap())))
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", Collections.emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -667,15 +667,26 @@ public class ExtendedStatsIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(extendedStats("foo").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(extendedStats("foo").field("d")
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(extendedStats("foo").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
 }

+ 21 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java

@@ -558,10 +558,10 @@ public class HDRPercentileRanksIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -574,12 +574,12 @@ public class HDRPercentileRanksIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client()
                 .prepareSearch("cache_test_idx").setSize(0)
                     .addAggregation(percentileRanks("foo", new double[]{50.0})
                         .method(PercentilesMethod.HDR).field("d")
-                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -588,8 +588,21 @@ public class HDRPercentileRanksIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
+        r = client()
+                .prepareSearch("cache_test_idx").setSize(0)
+                    .addAggregation(percentileRanks("foo", new double[]{50.0})
+                        .method(PercentilesMethod.HDR).field("d")
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                .get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
         r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(percentileRanks("foo", new double[]{50.0}).method(PercentilesMethod.HDR).field("d")).get();
         assertSearchResponse(r);
@@ -597,7 +610,7 @@ public class HDRPercentileRanksIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(1L));
+                .getMissCount(), equalTo(2L));
     }
 
 }

+ 19 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java

@@ -523,10 +523,10 @@ public class HDRPercentilesIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -539,10 +539,10 @@ public class HDRPercentilesIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)
-                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -551,16 +551,27 @@ public class HDRPercentilesIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
+        // Test that a request using a deterministic script gets cached
         r = client().prepareSearch("cache_test_idx").setSize(0)
-                .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)).get();
+                .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)
+                        .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
 }

+ 28 - 3
server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java

@@ -110,6 +110,9 @@ public class MaxAggregatorTests extends AggregatorTestCase {
     /** Script to return the {@code _value} provided by aggs framework. */
     public static final String VALUE_SCRIPT = "_value";
 
+    /** Script to return a random double */
+    public static final String RANDOM_SCRIPT = "Math.random()";
+
     @Override
     protected ScriptService getMockScriptService() {
         Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
@@ -143,8 +146,12 @@ public class MaxAggregatorTests extends AggregatorTestCase {
             return ((Number) vars.get("_value")).doubleValue() + inc;
         });
 
+        Map<String, Function<Map<String, Object>, Object>> nonDeterministicScripts = new HashMap<>();
+        nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> MaxAggregatorTests.randomDouble());
+
         MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
             scripts,
+            nonDeterministicScripts,
             Collections.emptyMap());
         Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
 
@@ -948,9 +955,10 @@ public class MaxAggregatorTests extends AggregatorTestCase {
     }
 
     /**
-     * Make sure that an aggregation using a script does not get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws IOException {
+    public void testScriptCaching() throws Exception {
         Directory directory = newDirectory();
         RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
         final int numDocs = 10;
@@ -968,7 +976,6 @@ public class MaxAggregatorTests extends AggregatorTestCase {
         MultiReader multiReader = new MultiReader(indexReader, unamappedIndexReader);
         IndexSearcher indexSearcher = newSearcher(multiReader, true, true);
 
-
         MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
         fieldType.setName("value");
         MaxAggregationBuilder aggregationBuilder = new MaxAggregationBuilder("max")
@@ -987,6 +994,24 @@ public class MaxAggregatorTests extends AggregatorTestCase {
         assertTrue(AggregationInspectionHelper.hasValue(max));
 
         // Test that an aggregation using a script does not get cached
+        assertTrue(aggregator.context().getQueryShardContext().isCacheable());
+        aggregationBuilder = new MaxAggregationBuilder("max")
+            .field("value")
+            .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap()));
+
+        aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType);
+        aggregator.preCollection();
+        indexSearcher.search(new MatchAllDocsQuery(), aggregator);
+        aggregator.postCollection();
+
+        max = (InternalMax) aggregator.buildAggregation(0L);
+
+        assertTrue(max.getValue() >= 0.0);
+        assertTrue(max.getValue() <= 1.0);
+        assertEquals("max", max.getName());
+        assertTrue(AggregationInspectionHelper.hasValue(max));
+
+        // Test that an aggregation using a nondeterministic script does not get cached
         assertFalse(aggregator.context().getQueryShardContext().isCacheable());
 
         multiReader.close();

+ 19 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java

@@ -556,10 +556,10 @@ public class MedianAbsoluteDeviationIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(
             prepareCreate("cache_test_idx")
                 .addMapping("type", "d", "type=long")
@@ -579,11 +579,11 @@ public class MedianAbsoluteDeviationIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
             .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
             .addAggregation(randomBuilder()
                 .field("d")
-                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get();
+                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -591,14 +591,25 @@ public class MedianAbsoluteDeviationIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
             .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(randomBuilder().field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+            .addAggregation(randomBuilder()
+                .field("d")
+                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
             .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
             .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(randomBuilder().field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(2L));
     }
 }

+ 13 - 0
server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java

@@ -28,6 +28,7 @@ import java.util.function.Function;
 
 import org.elasticsearch.script.MockScriptPlugin;
 import org.elasticsearch.search.lookup.LeafDocLookup;
+import org.elasticsearch.test.ESTestCase;
 
 /**
  * Provides a number of dummy scripts for tests.
@@ -52,6 +53,9 @@ public class MetricAggScriptPlugin extends MockScriptPlugin {
     /** Script to return the {@code _value} provided by aggs framework. */
     public static final String VALUE_SCRIPT = "_value";
 
+    /** Script to return a random double */
+    public static final String RANDOM_SCRIPT = "Math.random()";
+
     @Override
     public String pluginScriptLang() {
         return METRIC_SCRIPT_ENGINE;
@@ -88,4 +92,13 @@ public class MetricAggScriptPlugin extends MockScriptPlugin {
         });
         return scripts;
     }
+
+    @Override
+    protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+        Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+        scripts.put("Math.random()", vars -> ESTestCase.randomDouble());
+
+        return scripts;
+    }
 }

+ 21 - 3
server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java

@@ -127,6 +127,8 @@ public class MinAggregatorTests extends AggregatorTestCase {
 
     private static final String INVERT_SCRIPT = "invert";
 
+    private static final String RANDOM_SCRIPT = "random";
+
     @Override
     protected ScriptService getMockScriptService() {
         Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
@@ -161,8 +163,12 @@ public class MinAggregatorTests extends AggregatorTestCase {
         });
         scripts.put(INVERT_SCRIPT, vars -> -((Number) vars.get("_value")).doubleValue());
 
+        Map<String, Function<Map<String, Object>, Object>> nonDeterministicScripts = new HashMap<>();
+        nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> AggregatorTestCase.randomDouble());
+
         MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
             scripts,
+            nonDeterministicScripts,
             Collections.emptyMap());
         Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
 
@@ -649,7 +655,7 @@ public class MinAggregatorTests extends AggregatorTestCase {
         }
     }
 
-    public void testNoCachingWithScript() throws IOException {
+    public void testScriptCaching() throws IOException {
 
         MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
         fieldType.setName("number");
@@ -657,6 +663,10 @@ public class MinAggregatorTests extends AggregatorTestCase {
             .field("number")
             .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap()));;
 
+        MinAggregationBuilder nonDeterministicAggregationBuilder = new MinAggregationBuilder("min")
+            .field("number")
+            .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap()));;
+
         try (Directory directory = newDirectory()) {
             RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
             indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7)));
@@ -668,11 +678,19 @@ public class MinAggregatorTests extends AggregatorTestCase {
             try (IndexReader indexReader = DirectoryReader.open(directory)) {
                 IndexSearcher indexSearcher = newSearcher(indexReader, true, true);
 
-                InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType);
-                assertEquals(-7.0, min.getValue(), 0);
+                InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), nonDeterministicAggregationBuilder, fieldType);
+                assertTrue(min.getValue() >= 0.0 && min.getValue() <= 1.0);
                 assertTrue(AggregationInspectionHelper.hasValue(min));
 
                 assertFalse(queryShardContext.isCacheable());
+
+                indexSearcher = newSearcher(indexReader, true, true);
+
+                min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType);
+                assertEquals(-7.0, min.getValue(), 0);
+                assertTrue(AggregationInspectionHelper.hasValue(min));
+
+                assertTrue(queryShardContext.isCacheable());
             }
         }
     }

+ 76 - 6
server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java

@@ -247,6 +247,23 @@ public class ScriptedMetricIT extends ESIntegTestCase {
             return scripts;
         }
 
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
+
+            scripts.put("state.data = Math.random()", vars ->
+                aggScript(vars, state -> state.put("data", ScriptedMetricIT.randomDouble())));
+
+
+            scripts.put("state['count'] = Math.random() >= 0.5 ? 1 : 0", vars ->
+                aggScript(vars, state -> state.put("count", ScriptedMetricIT.randomDouble() >= 0.5 ? 1 : 0)));
+
+
+            scripts.put("return Math.random()", vars -> ScriptedMetricIT.randomDouble());
+
+            return scripts;
+        }
+
         @SuppressWarnings("unchecked")
         static Map<String, Object> aggScript(Map<String, Object> vars, Consumer<Map<String, Object>> fn) {
             Map<String, Object> aggState = (Map<String, Object>) vars.get("state");
@@ -1015,17 +1032,27 @@ public class ScriptedMetricIT extends ESIntegTestCase {
         assertThat(aggregationResult.get(0), equalTo(0));
     }
 
+
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script gets cached and nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
+        // TODO(stu): add non-determinism in init, agg, combine and reduce, ensure not cached
         Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = 1", Collections.emptyMap());
         Script combineScript =
             new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "no-op aggregation", Collections.emptyMap());
         Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME,
             "no-op list aggregation", Collections.emptyMap());
 
+        Script ndInitScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.data = Math.random()",
+                                         Collections.emptyMap());
+
+        Script ndMapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = Math.random() >= 0.5 ? 1 : 0",
+                                        Collections.emptyMap());
+
+        Script ndRandom = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "return Math.random()",
+                                     Collections.emptyMap());
+
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -1038,15 +1065,58 @@ public class ScriptedMetricIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a non-deterministic init script causes the result to not be cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
-                .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(reduceScript)).get();
+            .addAggregation(scriptedMetric("foo").initScript(ndInitScript).mapScript(mapScript).combineScript(combineScript)
+                            .reduceScript(reduceScript)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(0L));
+
+        // Test that a non-deterministic map script causes the result to not be cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+            .addAggregation(scriptedMetric("foo").mapScript(ndMapScript).combineScript(combineScript).reduceScript(reduceScript))
+            .get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(0L));
+
+        // Test that a non-deterministic combine script causes the result to not be cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+            .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(ndRandom).reduceScript(reduceScript)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(0L));
+
+        // NOTE: random reduce scripts don't hit the query shard context (they are done on the coordinator) and so can be cached.
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+            .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(ndRandom)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+            .getMissCount(), equalTo(1L));
+
+        // Test that all deterministic scripts cause the request to be cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(reduceScript))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
-                .getMissCount(), equalTo(0L));
+                .getMissCount(), equalTo(2L));
     }
 
     public void testConflictingAggAndScriptParams() {

+ 18 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java

@@ -488,10 +488,10 @@ public class StatsIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -504,10 +504,10 @@ public class StatsIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
                 stats("foo").field("d").script(
-                    new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
+                    new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -515,14 +515,24 @@ public class StatsIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(stats("foo").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(
+                stats("foo").field("d").script(
+                    new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(stats("foo").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 }

+ 19 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java

@@ -47,6 +47,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.histogra
 import static org.elasticsearch.search.aggregations.AggregationBuilders.sum;
 import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.METRIC_SCRIPT_ENGINE;
+import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.RANDOM_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_VALUES_FIELD_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_FIELD_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_SCRIPT;
@@ -374,10 +375,10 @@ public class SumIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -390,10 +391,10 @@ public class SumIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(sum("foo").field("d").script(
-                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_SCRIPT, Collections.emptyMap()))).get();
+                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, RANDOM_SCRIPT, Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -401,15 +402,25 @@ public class SumIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(sum("foo").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(sum("foo").field("d").script(
+                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_SCRIPT, Collections.emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(sum("foo").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
     public void testFieldAlias() {

+ 19 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java

@@ -484,10 +484,10 @@ public class TDigestPercentileRanksIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -500,11 +500,11 @@ public class TDigestPercentileRanksIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
             .addAggregation(percentileRanks("foo", new double[]{50.0})
                 .field("d")
-                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get();
+                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -512,15 +512,26 @@ public class TDigestPercentileRanksIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentileRanks("foo", new double[]{50.0}).field("d")).get();
+        // Test that a request using a deterministic script gets cached
+         r = client().prepareSearch("cache_test_idx").setSize(0)
+            .addAggregation(percentileRanks("foo", new double[]{50.0})
+                .field("d")
+                .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentileRanks("foo", new double[]{50.0}).field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
 }

+ 18 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java

@@ -467,10 +467,10 @@ public class TDigestPercentilesIT extends AbstractNumericTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -483,9 +483,9 @@ public class TDigestPercentilesIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d")
-                .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -494,14 +494,24 @@ public class TDigestPercentilesIT extends AbstractNumericTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d").percentiles(50.0)).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d")
+                .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d").percentiles(50.0)).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 }

+ 39 - 10
server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java

@@ -107,6 +107,11 @@ public class TopHitsIT extends ESIntegTestCase {
         protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
             return Collections.singletonMap("5", script -> "5");
         }
+
+        @Override
+        protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() {
+            return Collections.singletonMap("Math.random()", script -> TopHitsIT.randomDouble());
+        }
     }
 
     public static String randomExecutionHint() {
@@ -1086,10 +1091,10 @@ public class TopHitsIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         try {
             assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(
@@ -1107,10 +1112,10 @@ public class TopHitsIT extends ESIntegTestCase {
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-            // Test that a request using a script field does not get cached
+            // Test that a request using a nondeterministic script field does not get cached
             SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(topHits("foo").scriptField("bar",
-                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()))).get();
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get();
             assertSearchResponse(r);
 
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
@@ -1118,11 +1123,12 @@ public class TopHitsIT extends ESIntegTestCase {
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-            // Test that a request using a script sort does not get cached
+            // Test that a request using a nondeterministic script sort does not get cached
             r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(topHits("foo").sort(
                     SortBuilders.scriptSort(
-                        new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()), ScriptSortType.STRING)))
+                        new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()),
+                                   ScriptSortType.STRING)))
                 .get();
             assertSearchResponse(r);
 
@@ -1131,15 +1137,38 @@ public class TopHitsIT extends ESIntegTestCase {
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-            // To make sure that the cache is working test that a request not using
-            // a script is cached
-            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(topHits("foo")).get();
+            // Test that a request using a deterministic script field does not get cached
+            r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(topHits("foo").scriptField("bar",
+                    new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()))).get();
             assertSearchResponse(r);
 
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
             assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+            // Test that a request using a deterministic script sort does not get cached
+            r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(topHits("foo").sort(
+                    SortBuilders.scriptSort(
+                        new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()), ScriptSortType.STRING)))
+                .get();
+            assertSearchResponse(r);
+
+            assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+            assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
+
+            // Ensure that non-scripted requests are cached as normal
+            r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(topHits("foo")).get();
+            assertSearchResponse(r);
+
+            assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+            assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(3L));
         } finally {
             assertAcked(client().admin().indices().prepareDelete("cache_test_idx")); // delete this - if we use tests.iters it would fail
         }

+ 20 - 8
server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java

@@ -43,6 +43,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.filter;
 import static org.elasticsearch.search.aggregations.AggregationBuilders.global;
 import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.METRIC_SCRIPT_ENGINE;
+import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.RANDOM_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_FIELD_PARAMS_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_VALUES_FIELD_SCRIPT;
 import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_FIELD_SCRIPT;
@@ -211,10 +212,10 @@ public class ValueCountIT extends ESIntegTestCase {
     }
 
     /**
-     * Make sure that a request using a script does not get cached and a request
-     * not using a script does get cached.
+     * Make sure that a request using a deterministic script or not using a script get cached.
+     * Ensure requests using nondeterministic scripts do not get cached.
      */
-    public void testDontCacheScripts() throws Exception {
+    public void testScriptCaching() throws Exception {
         assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long")
                 .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1))
                 .get());
@@ -227,10 +228,10 @@ public class ValueCountIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // Test that a request using a script does not get cached
+        // Test that a request using a nondeterministic script does not get cached
         SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0)
                 .addAggregation(count("foo").field("d").script(
-                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_FIELD_SCRIPT, Collections.emptyMap())))
+                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, RANDOM_SCRIPT, Collections.emptyMap())))
                 .get();
         assertSearchResponse(r);
 
@@ -239,15 +240,26 @@ public class ValueCountIT extends ESIntegTestCase {
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(0L));
 
-        // To make sure that the cache is working test that a request not using
-        // a script is cached
-        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(count("foo").field("d")).get();
+        // Test that a request using a deterministic script gets cached
+        r = client().prepareSearch("cache_test_idx").setSize(0)
+                .addAggregation(count("foo").field("d").script(
+                    new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_FIELD_SCRIPT, Collections.emptyMap())))
+                .get();
         assertSearchResponse(r);
 
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getHitCount(), equalTo(0L));
         assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
                 .getMissCount(), equalTo(1L));
+
+        // Ensure that non-scripted requests are cached as normal
+        r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(count("foo").field("d")).get();
+        assertSearchResponse(r);
+
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getHitCount(), equalTo(0L));
+        assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache()
+                .getMissCount(), equalTo(2L));
     }
 
     public void testOrderByEmptyAggregation() throws Exception {

+ 45 - 0
test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java

@@ -0,0 +1,45 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.script;
+
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * A mocked script used for testing purposes.  {@code deterministic} implies cacheability in query shard cache.
+ */
+public abstract class MockDeterministicScript implements Function<Map<String, Object>, Object>, ScriptFactory {
+    public abstract Object apply(Map<String, Object> vars);
+    public abstract boolean isResultDeterministic();
+
+    public static MockDeterministicScript asDeterministic(Function<Map<String, Object>, Object> script) {
+        return new MockDeterministicScript() {
+            @Override public boolean isResultDeterministic() { return true; }
+            @Override public Object apply(Map<String, Object> vars) { return script.apply(vars); }
+        };
+    }
+
+    public static MockDeterministicScript asNonDeterministic(Function<Map<String, Object>, Object> script) {
+        return new MockDeterministicScript() {
+            @Override public boolean isResultDeterministic() { return false; }
+            @Override public Object apply(Map<String, Object> vars) { return script.apply(vars); }
+        };
+    }
+}

+ 154 - 101
test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java

@@ -62,11 +62,22 @@ public class MockScriptEngine implements ScriptEngine {
     public static final String NAME = "mockscript";
 
     private final String type;
-    private final Map<String, Function<Map<String, Object>, Object>> scripts;
+    private final Map<String, MockDeterministicScript> scripts;
     private final Map<ScriptContext<?>, ContextCompiler> contexts;
 
     public MockScriptEngine(String type, Map<String, Function<Map<String, Object>, Object>> scripts,
                             Map<ScriptContext<?>, ContextCompiler> contexts) {
+        this(type, scripts, Collections.emptyMap(), contexts);
+    }
+
+    public MockScriptEngine(String type, Map<String, Function<Map<String, Object>, Object>> deterministicScripts,
+                            Map<String, Function<Map<String, Object>, Object>> nonDeterministicScripts,
+                            Map<ScriptContext<?>, ContextCompiler> contexts) {
+
+        Map<String, MockDeterministicScript> scripts = new HashMap<>(deterministicScripts.size() + nonDeterministicScripts.size());
+        deterministicScripts.forEach((key, value) -> scripts.put(key, MockDeterministicScript.asDeterministic(value)));
+        nonDeterministicScripts.forEach((key, value) -> scripts.put(key, MockDeterministicScript.asNonDeterministic(value)));
+
         this.type = type;
         this.scripts = Collections.unmodifiableMap(scripts);
         this.contexts = Collections.unmodifiableMap(contexts);
@@ -85,34 +96,14 @@ public class MockScriptEngine implements ScriptEngine {
     public <T extends ScriptFactory> T compile(String name, String source, ScriptContext<T> context, Map<String, String> params) {
         // Scripts are always resolved using the script's source. For inline scripts, it's easy because they don't have names and the
         // source is always provided. For stored and file scripts, the source of the script must match the key of a predefined script.
-        Function<Map<String, Object>, Object> script = scripts.get(source);
+        MockDeterministicScript script = scripts.get(source);
         if (script == null) {
             throw new IllegalArgumentException("No pre defined script matching [" + source + "] for script with name [" + name + "], " +
                     "did you declare the mocked script?");
         }
         MockCompiledScript mockCompiled = new MockCompiledScript(name, params, source, script);
         if (context.instanceClazz.equals(FieldScript.class)) {
-            FieldScript.Factory factory = (parameters, lookup) ->
-                ctx -> new FieldScript(parameters, lookup, ctx) {
-                    @Override
-                    public Object execute() {
-                        Map<String, Object> vars = createVars(parameters);
-                        vars.putAll(getLeafLookup().asMap());
-                        return script.apply(vars);
-                    }
-                };
-            return context.factoryClazz.cast(factory);
-        } else if (context.instanceClazz.equals(FieldScript.class)) {
-            FieldScript.Factory factory = (parameters, lookup) ->
-                ctx -> new FieldScript(parameters, lookup, ctx) {
-                    @Override
-                    public Object execute() {
-                        Map<String, Object> vars = createVars(parameters);
-                        vars.putAll(getLeafLookup().asMap());
-                        return script.apply(vars);
-                    }
-                };
-            return context.factoryClazz.cast(factory);
+            return context.factoryClazz.cast(new MockFieldScriptFactory(script));
         } else if(context.instanceClazz.equals(TermsSetQueryScript.class)) {
             TermsSetQueryScript.Factory factory = (parameters, lookup) -> (TermsSetQueryScript.LeafFactory) ctx
                 -> new TermsSetQueryScript(parameters, lookup, ctx) {
@@ -147,17 +138,7 @@ public class MockScriptEngine implements ScriptEngine {
             };
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(StringSortScript.class)) {
-            StringSortScript.Factory factory = (parameters, lookup) -> (StringSortScript.LeafFactory) ctx
-                -> new StringSortScript(parameters, lookup, ctx) {
-                @Override
-                public String execute() {
-                    Map<String, Object> vars = new HashMap<>(parameters);
-                    vars.put("params", parameters);
-                    vars.put("doc", getDoc());
-                    return String.valueOf(script.apply(vars));
-                }
-            };
-            return context.factoryClazz.cast(factory);
+            return context.factoryClazz.cast(new MockStringSortScriptFactory(script));
         } else if (context.instanceClazz.equals(IngestScript.class)) {
             IngestScript.Factory factory = vars -> new IngestScript(vars) {
                 @Override
@@ -167,37 +148,7 @@ public class MockScriptEngine implements ScriptEngine {
             };
             return context.factoryClazz.cast(factory);
         } else if(context.instanceClazz.equals(AggregationScript.class)) {
-            AggregationScript.Factory factory = (parameters, lookup) -> new AggregationScript.LeafFactory() {
-                @Override
-                public AggregationScript newInstance(final LeafReaderContext ctx) {
-                    return new AggregationScript(parameters, lookup, ctx) {
-                        @Override
-                        public Object execute() {
-                            Map<String, Object> vars = new HashMap<>(parameters);
-                            vars.put("params", parameters);
-                            vars.put("doc", getDoc());
-                            vars.put("_score", get_score());
-                            vars.put("_value", get_value());
-                            return script.apply(vars);
-                        }
-                    };
-                }
-
-                @Override
-                public boolean needs_score() {
-                    return true;
-                }
-            };
-            return context.factoryClazz.cast(factory);
-        } else if (context.instanceClazz.equals(IngestScript.class)) {
-            IngestScript.Factory factory = vars ->
-                new IngestScript(vars) {
-                    @Override
-                    public void execute(Map<String, Object> ctx) {
-                        script.apply(ctx);
-                    }
-                };
-            return context.factoryClazz.cast(factory);
+            return context.factoryClazz.cast(new MockAggregationScript(script));
         } else if (context.instanceClazz.equals(IngestConditionalScript.class)) {
             IngestConditionalScript.Factory factory = parameters -> new IngestConditionalScript(parameters) {
                 @Override
@@ -240,13 +191,7 @@ public class MockScriptEngine implements ScriptEngine {
             };
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(SignificantTermsHeuristicScoreScript.class)) {
-            SignificantTermsHeuristicScoreScript.Factory factory = () -> new SignificantTermsHeuristicScoreScript() {
-                @Override
-                public double execute(Map<String, Object> vars) {
-                    return ((Number) script.apply(vars)).doubleValue();
-                }
-            };
-            return context.factoryClazz.cast(factory);
+            return context.factoryClazz.cast(new MockSignificantTermsHeuristicScoreScript(script));
         } else if (context.instanceClazz.equals(TemplateScript.class)) {
             TemplateScript.Factory factory = vars -> {
                 Map<String, Object> varsWithParams = new HashMap<>();
@@ -280,19 +225,19 @@ public class MockScriptEngine implements ScriptEngine {
             };
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(ScoreScript.class)) {
-            ScoreScript.Factory factory = new MockScoreScript(script);
+            ScoreScript.Factory factory = new MockScoreScript(script::apply);
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.InitScript.class)) {
-            ScriptedMetricAggContexts.InitScript.Factory factory = mockCompiled::createMetricAggInitScript;
+            ScriptedMetricAggContexts.InitScript.Factory factory = new MockMetricAggInitScriptFactory(script);
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.MapScript.class)) {
-            ScriptedMetricAggContexts.MapScript.Factory factory = mockCompiled::createMetricAggMapScript;
+            ScriptedMetricAggContexts.MapScript.Factory factory = new MockMetricAggMapScriptFactory(script);
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.CombineScript.class)) {
-            ScriptedMetricAggContexts.CombineScript.Factory factory = mockCompiled::createMetricAggCombineScript;
+            ScriptedMetricAggContexts.CombineScript.Factory factory = new MockMetricAggCombineScriptFactory(script);
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.ReduceScript.class)) {
-            ScriptedMetricAggContexts.ReduceScript.Factory factory = mockCompiled::createMetricAggReduceScript;
+            ScriptedMetricAggContexts.ReduceScript.Factory factory = new MockMetricAggReduceScriptFactory(script);
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(IntervalFilterScript.class)) {
             IntervalFilterScript.Factory factory = mockCompiled::createIntervalFilterScript;
@@ -300,7 +245,7 @@ public class MockScriptEngine implements ScriptEngine {
         }
         ContextCompiler compiler = contexts.get(context);
         if (compiler != null) {
-            return context.factoryClazz.cast(compiler.compile(script, params));
+            return context.factoryClazz.cast(compiler.compile(script::apply, params));
         }
         throw new IllegalArgumentException("mock script engine does not know how to handle context [" + context.name + "]");
     }
@@ -370,25 +315,6 @@ public class MockScriptEngine implements ScriptEngine {
             return new MockSimilarityWeightScript(script != null ? script : ctx -> 42d);
         }
 
-        public ScriptedMetricAggContexts.InitScript createMetricAggInitScript(Map<String, Object> params, Map<String, Object> state) {
-            return new MockMetricAggInitScript(params, state, script != null ? script : ctx -> 42d);
-        }
-
-        public ScriptedMetricAggContexts.MapScript.LeafFactory createMetricAggMapScript(Map<String, Object> params,
-                                                                                        Map<String, Object> state,
-                                                                                        SearchLookup lookup) {
-            return new MockMetricAggMapScript(params, state, lookup, script != null ? script : ctx -> 42d);
-        }
-
-        public ScriptedMetricAggContexts.CombineScript createMetricAggCombineScript(Map<String, Object> params,
-                                                                                    Map<String, Object> state) {
-            return new MockMetricAggCombineScript(params, state, script != null ? script : ctx -> 42d);
-        }
-
-        public ScriptedMetricAggContexts.ReduceScript createMetricAggReduceScript(Map<String, Object> params, List<Object> states) {
-            return new MockMetricAggReduceScript(params, states, script != null ? script : ctx -> 42d);
-        }
-
         public IntervalFilterScript createIntervalFilterScript() {
             return new IntervalFilterScript() {
                 @Override
@@ -469,6 +395,17 @@ public class MockScriptEngine implements ScriptEngine {
         }
     }
 
+    public static class MockMetricAggInitScriptFactory implements ScriptedMetricAggContexts.InitScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockMetricAggInitScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public ScriptedMetricAggContexts.InitScript newInstance(Map<String, Object> params, Map<String, Object> state) {
+            return new MockMetricAggInitScript(params, state, script);
+        }
+    }
+
     public static class MockMetricAggInitScript extends ScriptedMetricAggContexts.InitScript {
         private final Function<Map<String, Object>, Object> script;
 
@@ -491,6 +428,18 @@ public class MockScriptEngine implements ScriptEngine {
         }
     }
 
+    public static class MockMetricAggMapScriptFactory implements  ScriptedMetricAggContexts.MapScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockMetricAggMapScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public ScriptedMetricAggContexts.MapScript.LeafFactory newFactory(Map<String, Object> params, Map<String, Object> state,
+                                                                          SearchLookup lookup) {
+            return new MockMetricAggMapScript(params, state, lookup, script);
+        }
+    }
+
     public static class MockMetricAggMapScript implements ScriptedMetricAggContexts.MapScript.LeafFactory {
         private final Map<String, Object> params;
         private final Map<String, Object> state;
@@ -527,11 +476,21 @@ public class MockScriptEngine implements ScriptEngine {
         }
     }
 
+    public static class MockMetricAggCombineScriptFactory implements ScriptedMetricAggContexts.CombineScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockMetricAggCombineScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public ScriptedMetricAggContexts.CombineScript newInstance(Map<String, Object> params, Map<String, Object> state) {
+            return new MockMetricAggCombineScript(params, state, script);
+        }
+    }
+
     public static class MockMetricAggCombineScript extends ScriptedMetricAggContexts.CombineScript {
         private final Function<Map<String, Object>, Object> script;
 
-        MockMetricAggCombineScript(Map<String, Object> params, Map<String, Object> state,
-                                Function<Map<String, Object>, Object> script) {
+        MockMetricAggCombineScript(Map<String, Object> params, Map<String, Object> state, Function<Map<String, Object>, Object> script) {
             super(params, state);
             this.script = script;
         }
@@ -549,11 +508,21 @@ public class MockScriptEngine implements ScriptEngine {
         }
     }
 
+    public static class MockMetricAggReduceScriptFactory implements ScriptedMetricAggContexts.ReduceScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockMetricAggReduceScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public ScriptedMetricAggContexts.ReduceScript newInstance(Map<String, Object> params, List<Object> states) {
+            return new MockMetricAggReduceScript(params, states, script);
+        }
+    }
+
     public static class MockMetricAggReduceScript extends ScriptedMetricAggContexts.ReduceScript {
         private final Function<Map<String, Object>, Object> script;
 
-        MockMetricAggReduceScript(Map<String, Object> params, List<Object> states,
-                                  Function<Map<String, Object>, Object> script) {
+        MockMetricAggReduceScript(Map<String, Object> params, List<Object> states, Function<Map<String, Object>, Object> script) {
             super(params, states);
             this.script = script;
         }
@@ -615,4 +584,88 @@ public class MockScriptEngine implements ScriptEngine {
         }
     }
 
+    class MockAggregationScript implements AggregationScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockAggregationScript(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public AggregationScript.LeafFactory newFactory(Map<String, Object> params, SearchLookup lookup) {
+            return new AggregationScript.LeafFactory() {
+                @Override
+                public AggregationScript newInstance(final LeafReaderContext ctx) {
+                    return new AggregationScript(params, lookup, ctx) {
+                        @Override
+                        public Object execute() {
+                            Map<String, Object> vars = new HashMap<>(params);
+                            vars.put("params", params);
+                            vars.put("doc", getDoc());
+                            vars.put("_score", get_score());
+                            vars.put("_value", get_value());
+                            return script.apply(vars);
+                        }
+                    };
+                }
+
+                @Override
+                public boolean needs_score() {
+                    return true;
+                }
+            };
+        }
+    }
+
+    class MockSignificantTermsHeuristicScoreScript implements SignificantTermsHeuristicScoreScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockSignificantTermsHeuristicScoreScript(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public SignificantTermsHeuristicScoreScript newInstance() {
+            return new SignificantTermsHeuristicScoreScript() {
+                @Override
+                public double execute(Map<String, Object> vars) {
+                    return ((Number) script.apply(vars)).doubleValue();
+                }
+            };
+        }
+    }
+
+    class MockFieldScriptFactory implements FieldScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockFieldScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public FieldScript.LeafFactory newFactory(Map<String, Object> parameters, SearchLookup lookup) {
+            return ctx -> new FieldScript(parameters, lookup, ctx) {
+                @Override
+                public Object execute() {
+                    Map<String, Object> vars = createVars(parameters);
+                    vars.putAll(getLeafLookup().asMap());
+                    return script.apply(vars);
+
+                }
+            };
+        }
+    }
+
+    class MockStringSortScriptFactory implements StringSortScript.Factory, ScriptFactory {
+        private final MockDeterministicScript script;
+        MockStringSortScriptFactory(MockDeterministicScript script) { this.script = script; }
+        @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
+
+        @Override
+        public StringSortScript.LeafFactory newFactory(Map<String, Object> parameters, SearchLookup lookup) {
+            return ctx -> new StringSortScript(parameters, lookup, ctx) {
+                @Override
+                public String execute() {
+                    Map<String, Object> vars = new HashMap<>(parameters);
+                    vars.put("params", parameters);
+                    vars.put("doc", getDoc());
+                    return String.valueOf(script.apply(vars));
+                }
+            };
+        }
+    }
 }

+ 3 - 1
test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java

@@ -37,11 +37,13 @@ public abstract class MockScriptPlugin extends Plugin implements ScriptPlugin {
 
     @Override
     public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
-        return new MockScriptEngine(pluginScriptLang(), pluginScripts(), pluginContextCompilers());
+        return new MockScriptEngine(pluginScriptLang(), pluginScripts(), nonDeterministicPluginScripts(), pluginContextCompilers());
     }
 
     protected abstract Map<String, Function<Map<String, Object>, Object>> pluginScripts();
 
+    protected Map<String, Function<Map<String, Object>, Object>> nonDeterministicPluginScripts() { return Collections.emptyMap(); }
+
     protected Map<ScriptContext<?>, MockScriptEngine.ContextCompiler> pluginContextCompilers() {
         return Collections.emptyMap();
     }