This is an automated email from the ASF dual-hosted git repository.

ab pushed a commit to branch jira/solr-15232
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 2cdfc69a5eff3092a5ba9d588877d8e9edbb9634
Author: Andrzej Bialecki <[email protected]>
AuthorDate: Tue Apr 6 14:43:24 2021 +0200

    SOLRR-15232: Add moroe commands to the DSL, hook up the CC shutdown, add 
unit tests.
---
 .../java/org/apache/solr/core/CoreContainer.java   |  29 +-
 .../org/apache/solr/util/scripting/Script.java     | 709 +++++++++++++++------
 solr/core/src/test-files/solr/initScript.txt       |  19 +
 solr/core/src/test-files/solr/initScript2.txt      |   4 +
 solr/core/src/test-files/solr/shutdownScript.txt   |   1 +
 .../apache/solr/util/scripting/ScriptingTest.java  |  89 ++-
 6 files changed, 622 insertions(+), 229 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java 
b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index e6e635a..942a320 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -939,11 +939,11 @@ public class CoreContainer {
     if (isZooKeeperAware()) {
       // run init script if needed
       String resourceName = System.getProperty(Script.INIT_SCRIPT);
-      runScript(resourceName, true);
+      runScript(resourceName, false);
     }
   }
 
-  public void runScript(String resourceName, boolean async) {
+  public void runScript(String resourceName, boolean inShutdown) {
     if (resourceName == null) {
       return;
     }
@@ -951,28 +951,21 @@ public class CoreContainer {
     String nodeName = zkSys.zkController.getNodeName();
     Script script = null;
     try {
-      script = Script.loadResource(loader, solrClient, nodeName, resourceName);
+      script = Script.parseResource(loader, solrClient, nodeName, 
resourceName);
     } catch (Exception e) {
       log.warn("Error loading script, ignoring " + resourceName, e);
       script = null;
     }
     final Script finalScript = script;
     if (finalScript != null) {
-      Runnable r = () -> {
-        try {
-          finalScript.run();
-        } catch (Exception e) {
-          log.warn("Error running script " + resourceName, e);
-          if (finalScript.getErrorHandling() == Script.ErrorHandling.FATAL) {
-            log.warn("Script error handling set to FATAL - shutting down.");
-            shutdown();
-          }
+      try {
+        finalScript.run();
+      } catch (Exception e) {
+        log.warn("Error running script " + resourceName, e);
+        if (!inShutdown && finalScript.getErrorHandling() == 
Script.ErrorHandling.FATAL) {
+          log.warn("Script error handling set to FATAL - shutting down.");
+          shutdown();
         }
-      };
-      if (async) {
-        runAsync(r);
-      } else {
-        r.run();
       }
     }
   }
@@ -1074,7 +1067,7 @@ public class CoreContainer {
       overseerCollectionQueue.allowOverseerPendingTasksToComplete();
       // run shutdown script if needed
       String resourceName = System.getProperty(Script.SHUTDOWN_SCRIPT);
-      runScript(resourceName, false);
+      runScript(resourceName, true);
     }
     if (log.isInfoEnabled()) {
       log.info("Shutting down CoreContainer instance={}", 
System.identityHashCode(this));
diff --git a/solr/core/src/java/org/apache/solr/util/scripting/Script.java 
b/solr/core/src/java/org/apache/solr/util/scripting/Script.java
index d02322d..ab8d167 100644
--- a/solr/core/src/java/org/apache/solr/util/scripting/Script.java
+++ b/solr/core/src/java/org/apache/solr/util/scripting/Script.java
@@ -18,6 +18,7 @@
 package org.apache.solr.util.scripting;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
@@ -26,44 +27,48 @@ import 
org.apache.solr.client.solrj.impl.SolrClientCloudManager;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.RequestWriter;
 import org.apache.solr.cloud.CloudUtil;
+import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.ExecutorUtil;
-import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.PropertiesUtil;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.common.util.TimeSource;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.util.TimeOut;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.lang.invoke.MethodHandles;
 import java.net.URLDecoder;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Properties;
 import java.util.Random;
+import java.util.Stack;
 import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
 
 /**
  * This class represents a script consisting of a series of operations.
  */
-public class Script implements AutoCloseable {
+public class Script implements Closeable {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static final String INIT_SCRIPT = "initScript";
@@ -78,18 +83,18 @@ public class Script implements AutoCloseable {
   public static final String LIVE_NODES_CTX_PROP = "_live_nodes_";
   /** Context variable: List of collections. */
   public static final String COLLECTIONS_CTX_PROP = "_collections_";
-  /** Context variable: List of SolrResponses of SOLR_REQUEST operations. */
-  public static final String RESPONSES_CTX_PROP = "_responses_";
+  /** Context variable: Last op result. */
+  public static final String LAST_RESULT_CTX_PROP = "_last_result_";
   /** Context variable: Current loop iteration or none if outside of loop. */
   public static final String LOOP_ITER_PROP = "_loop_iter_";
   /** Context variable: Timeout for operation to complete, in ms. */
   public static final String TIMEOUT_PROP = "_timeout_";
   /** Context variable: How to handle errors. */
-  public static final String ERROR_HANDLING_PROP = "errorHandling";
+  public static final String ERROR_HANDLING_PROP = "_error_handling_";
 
   public enum ErrorHandling {
     IGNORE,
-    END_SCRIPT,
+    ABORT,
     FATAL;
 
     public static ErrorHandling get(String str) {
@@ -105,7 +110,7 @@ public class Script implements AutoCloseable {
     }
   }
 
-  public static final int DEFAULT_OP_TIMEOUT_MS = 30000;
+  public static final int DEFAULT_OP_TIMEOUT_MS = 120000;
 
   public final List<ScriptOp> ops = new ArrayList<>();
   public final Map<String, Object> context = new HashMap<>();
@@ -113,15 +118,27 @@ public class Script implements AutoCloseable {
   public final String nodeName;
   public final SolrCloudManager cloudManager;
   public final TimeSource timeSource = TimeSource.NANO_TIME;
+  public final Random random = new Random();
+  public final List<OpResult> opResults = new ArrayList<>();
+  private ExecutorService executorService;
+  private final boolean shouldCloseExecutor;
   public PrintStream console = System.err;
   public boolean verbose;
   public boolean abortLoop;
   public boolean abortScript;
-  public ErrorHandling errorHandling = ErrorHandling.END_SCRIPT;
-  public final Random random = new Random();
+  public ErrorHandling errorHandling = ErrorHandling.ABORT;
+
+  public Function<String, String> propertyEvalFunc = (key) -> {
+    Object o = Utils.getObjectByPath(context, false, key);
+    if (o != null) {
+      return o.toString();
+    } else {
+      return null;
+    }
+  };
 
   /** Base class for implementation of script DSL ScriptActions. */
-  public static abstract class ScriptOp {
+  public static abstract class ScriptOp implements MapWriter {
     ModifiableSolrParams initParams;
     ModifiableSolrParams params;
 
@@ -138,37 +155,9 @@ public class Script implements AutoCloseable {
      */
     @SuppressWarnings({"unchecked"})
     public void prepareCurrentParams(Script script) {
-      Properties props = new Properties();
-      script.context.forEach((k, v) -> {
-        if (v instanceof String[]) {
-          v = String.join(",", (String[]) v);
-        } else if (v instanceof Collection) {
-          StringBuilder sb = new StringBuilder();
-          for (Object o : (Collection<Object>)v) {
-            if (sb.length() > 0) {
-              sb.append(',');
-            }
-            if ((o instanceof String) || (o instanceof Number)) {
-              sb.append(o);
-            } else {
-              // skip all values
-              return;
-            }
-          }
-          v = sb.toString();
-        } else if ((v instanceof String)) {
-          // don't convert, put as is
-        } else if ((v instanceof Number)) {
-          v = v.toString();
-        } else {
-          // skip
-          return;
-        }
-        props.put(k, v);
-      });
       ModifiableSolrParams currentParams = new ModifiableSolrParams();
       initParams.forEach(e -> {
-        String newKey = PropertiesUtil.substituteProperty(e.getKey(), props);
+        String newKey = PropertiesUtil.substitute(e.getKey(), 
script.propertyEvalFunc);
         if (newKey == null) {
           newKey = e.getKey();
         }
@@ -177,7 +166,7 @@ public class Script implements AutoCloseable {
           String[] values = e.getValue();
           newValues = new String[values.length];
           for (int k = 0; k < values.length; k++) {
-            String newVal = PropertiesUtil.substituteProperty(values[k], 
props);
+            String newVal = PropertiesUtil.substitute(values[k], 
script.propertyEvalFunc);
             if (newVal == null) {
               newVal = values[k];
             }
@@ -194,12 +183,38 @@ public class Script implements AutoCloseable {
     /**
      * Execute the operation.
      * @param script current script.
+     * @return result of the operation (status, response, etc) for informative 
purpose
      */
-    public abstract void execute (Script script) throws Exception;
+    public abstract Object execute (Script script) throws Exception;
 
     @Override
     public String toString() {
-      return this.getClass().getSimpleName() + "{" + (params != null ? params 
: initParams) + "}";
+      return this.getClass().getSimpleName() + "{" + initParams + "}";
+    }
+
+    @Override
+    public void writeMap(EntryWriter ew) throws IOException {
+      ew.put("name", getClass().getSimpleName());
+      ew.put("initParams", initParams);
+      if (params != null) {
+        ew.put("params", params);
+      }
+    }
+  }
+
+  public static class OpResult implements MapWriter {
+    public final ScriptOp op;
+    public final Object result;
+
+    public OpResult(ScriptOp op, Object result) {
+      this.op = op;
+      this.result = result;
+    }
+
+    @Override
+    public void writeMap(EntryWriter ew) throws IOException {
+      ew.put("op", op);
+      ew.put("result", result);
     }
   }
 
@@ -209,13 +224,19 @@ public class Script implements AutoCloseable {
    */
   public enum ScriptAction {
     /** Start a loop. */
-    LOOP_START,
-    /** End a loop. */
-    LOOP_END,
+    LOOP,
+    /** End a loop of if/else. */
+    END,
+    /** Start a conditional block. */
+    IF,
+    /** Start an ELSE conditional block. */
+    ELSE,
     /** Execute a SolrRequest. */
-    SOLR_REQUEST,
+    REQUEST,
     /** Wait for a collection to reach the indicated number of shards and 
replicas. */
     WAIT_COLLECTION,
+    /** Wait for a replica (or core) to reach the indicated state. */
+    WAIT_REPLICA,
     /** Prepare a listener to listen for an autoscaling event. */
     EVENT_LISTENER,
     /** Wait for an autoscaling event using previously prepared listener. */
@@ -230,6 +251,8 @@ public class Script implements AutoCloseable {
     ASSERT,
     /** Set an operation timeout. */
     TIMEOUT,
+    /** Log arbitrary data from context. */
+    LOG,
     /** Dump the script and the current context to the log. */
     DUMP;
 
@@ -252,22 +275,37 @@ public class Script implements AutoCloseable {
 
   public static Map<ScriptAction, Class<? extends ScriptOp>> supportedOps = 
new HashMap<>();
   static {
-    supportedOps.put(ScriptAction.LOOP_START, LoopOp.class);
-    supportedOps.put(ScriptAction.LOOP_END, null);
-    supportedOps.put(ScriptAction.SOLR_REQUEST, RunSolrRequest.class);
-    supportedOps.put(ScriptAction.WAIT, Wait.class);
-    supportedOps.put(ScriptAction.WAIT_COLLECTION, WaitCollection.class);
-    supportedOps.put(ScriptAction.SET, CtxSet.class);
-    supportedOps.put(ScriptAction.CLEAR, CtxClear.class);
-    supportedOps.put(ScriptAction.ASSERT, Assert.class);
-    supportedOps.put(ScriptAction.TIMEOUT, Timeout.class);
-    supportedOps.put(ScriptAction.DUMP, Dump.class);
+    supportedOps.put(ScriptAction.LOOP, LoopOp.class);
+    supportedOps.put(ScriptAction.REQUEST, RequestOp.class);
+    supportedOps.put(ScriptAction.WAIT, WaitOp.class);
+    supportedOps.put(ScriptAction.WAIT_COLLECTION, WaitCollectionOp.class);
+    supportedOps.put(ScriptAction.WAIT_REPLICA, WaitReplicaOp.class);
+    supportedOps.put(ScriptAction.SET, CtxSetOp.class);
+    supportedOps.put(ScriptAction.CLEAR, CtxClearOp.class);
+    supportedOps.put(ScriptAction.ASSERT, AssertOp.class);
+    supportedOps.put(ScriptAction.TIMEOUT, TimeoutOp.class);
+    supportedOps.put(ScriptAction.LOG, LogOp.class);
+    supportedOps.put(ScriptAction.IF, IfElseOp.class);
+    supportedOps.put(ScriptAction.ELSE, null);
+    supportedOps.put(ScriptAction.END, null);
+    supportedOps.put(ScriptAction.DUMP, DumpOp.class);
+  }
+
+  private Script(CloudSolrClient client, String nodeName) {
+    this(client, nodeName, null);
   }
-  
-  public Script(CloudSolrClient client, String nodeName) {
+
+  private Script(CloudSolrClient client, String nodeName, ExecutorService 
executorService) {
     this.client = client;
     this.nodeName = nodeName;
     this.cloudManager = new SolrClientCloudManager(null, client);
+    if (executorService == null) {
+      this.executorService = ExecutorUtil.newMDCAwareSingleThreadExecutor(new 
SolrNamedThreadFactory("Script"));
+      this.shouldCloseExecutor = true;
+    } else {
+      this.executorService = executorService;
+      this.shouldCloseExecutor = false;
+    }
     // We make things reproducible in tests by using test seed if any
     String seed = System.getProperty("tests.seed");
     if (seed != null) {
@@ -276,87 +314,178 @@ public class Script implements AutoCloseable {
   }
 
   /**
-   * Loop ScriptAction.
+   * Log some information.
    */
-  public static class LoopOp extends ScriptOp {
+  public static class LogOp extends ScriptOp {
+
+    @Override
+    public Object execute(Script script) throws Exception {
+      String msg = params.get("msg");
+      if (msg == null) {
+        String[] keys = params.required().getParams("key");
+        String format = params.required().get("format");
+        int count = StringUtils.countMatches(format, "{}");
+        if (count > keys.length) {
+          throw new Exception("Too few key names for the number of formal 
parameters: " + count);
+        } else if (count < keys.length) {
+          throw new Exception("Too many key names for the number of formal 
parameters: " + count);
+        }
+        Object[] vals = new Object[keys.length];
+        for (int i = 0; i < keys.length; i++) {
+          vals[i] = Utils.getObjectByPath(script.context, false, keys[i]);
+        }
+        log.info(format, vals);
+      } else {
+        log.info(msg);
+      }
+      return null;
+    }
+  }
+
+  public static abstract class CompoundOp extends ScriptOp {
     // populated by the DSL parser
-    List<ScriptOp> ops = new ArrayList<>();
+    public List<ScriptOp> ops = new ArrayList<>();
+
+    public void addOp(ScriptOp op) {
+      ops.add(op);
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder(this.getClass().getSimpleName() + 
"{" + params + ", ops=" + ops.size() + "}");
+      for (int i = 0; i < ops.size(); i++) {
+        sb.append("\n    " + (i + 1) + ". " + ops.get(i));
+      }
+      return sb.toString();
+    }
+  }
+
+  /**
+   * Loop ScriptAction.
+   */
+  public static class LoopOp extends CompoundOp {
     int iterations;
 
     @Override
-    public void execute(Script script) throws Exception {
-      iterations = Integer.parseInt(params.get("iterations", "10"));
+    public Object execute(Script script) throws Exception {
+      iterations = params.required().getInt("iterations", 10);
+      if (iterations < 0) {
+        throw new Exception("Number of iterations must be non-negative but was 
" + iterations);
+      }
       for (int i = 0; i < iterations; i++) {
         if (script.abortLoop) {
           log.info("        -- abortLoop requested, aborting after {} 
iterations.", i);
-          return;
+          return Map.of("iterations", i, "state", "aborted");
         }
         script.context.put(LOOP_ITER_PROP, String.valueOf(i));
-        log.info("   * iter {} :", i + 1); // logOK
+        log.debug("   * iter {} :", i + 1); // logOK
         for (ScriptOp op : ops) {
-          op.prepareCurrentParams(script);
           if (log.isInfoEnabled()) {
             log.info("     - {}\t{})", op.getClass().getSimpleName(), 
op.params);
           }
-          op.execute(script);
+          script.execOp(op);
           if (script.abortLoop) {
             log.info("        -- abortLoop requested, aborting after {} 
iterations.", i);
-            return;
+            return Map.of("iterations", i, "state", "aborted");
           }
         }
       }
+      return Map.of("iterations", iterations, "state", "finished");
     }
+  }
+
+  public static class IfElseOp extends CompoundOp {
+    public List<ScriptOp> elseOps = new ArrayList<>();
+    public boolean parsingElse = false;
 
     @Override
-    public String toString() {
-      StringBuilder sb = new StringBuilder(this.getClass().getSimpleName() + 
"{" + params + ", ops=" + ops.size());
-      for (int i = 0; i < ops.size(); i++) {
-        sb.append("\n    " + (i + 1) + ". " + ops.get(i));
+    public void addOp(ScriptOp op) {
+      if (parsingElse) {
+        elseOps.add(op);
+      } else {
+        ops.add(op);
       }
-      return sb.toString();
+    }
+
+    @Override
+    public Object execute(Script script) throws Exception {
+      String key = params.get("key");
+      Condition condition = Condition.get(params.required().get("condition"));
+      if (condition == null) {
+        throw new IOException("Invalid 'condition' in params: " + params);
+      }
+      String expected = params.get("expected");
+      if (condition != Condition.NOT_NULL && condition != Condition.NULL && 
expected == null) {
+        throw new IOException("'expected' param is required when condition is 
" + condition);
+      }
+      Object value;
+      if (key != null) {
+        value = Utils.getObjectByPath(script.context, true, key);
+      } else {
+        value = params.required().get("value");
+      }
+      boolean eval = Condition.eval(condition, value, expected);
+      List<ScriptOp> selectedOps = eval ? ops : elseOps;
+      for (ScriptOp op : selectedOps) {
+        if (log.isInfoEnabled()) {
+          log.info("     - {}\t{})", op.getClass().getSimpleName(), op.params);
+        }
+        script.execOp(op);
+      }
+      return Map.of("condition_eval", eval);
     }
   }
 
   /**
    * Set a context property.
    */
-  public static class CtxSet extends ScriptOp {
+  public static class CtxSetOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       String key = params.required().get("key");
-      String[] values = params.required().getParams("value");
-      if (values != null) {
-        script.context.put(key, Arrays.asList(values));
+      String value = params.required().get("value");
+      Object previous;
+      if (value != null) {
+        previous = script.context.put(key, value);
       } else {
-        script.context.remove(key);
+        previous = script.context.remove(key);
       }
+      Map<String, Object> res = new HashMap<>();
+      res.put("previous", previous);
+      res.put("new", value);
+      return res;
     }
   }
 
   /**
    * Remove a context property.
    */
-  public static class CtxClear extends ScriptOp {
+  public static class CtxClearOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       String key = params.required().get("key");
-      script.context.remove(key);
+      return script.context.remove(key);
     }
   }
 
   /**
-   * Dump the script and the current context to log output.
+   * Dump the script, execution trace and the current context to log output.
    */
-  public static class Dump extends ScriptOp {
+  public static class DumpOp extends ScriptOp {
 
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       log.info("========= START script dump ==========");
-      log.info("---------    Script Ops     ----------");
+      log.info("------    Script Operations    --------");
       for (int i = 0; i < script.ops.size(); i++) {
-        log.info("{}. {}", i + 1, script.ops.get(i));
+        log.info("{}.\t{}", i + 1, script.ops.get(i));
+      }
+      log.info("------    Script Execution     --------");
+      for (int i = 0; i < script.opResults.size(); i++) {
+        OpResult res = script.opResults.get(i);
+        dumpResult(res, i + 1, 0);
       }
-      log.info("---------  Script Context   ----------");
+      log.info("------  Final Script Context   --------");
       TreeMap<String, Object> map = new TreeMap<>(script.context);
       map.forEach((key, value) -> {
         if (value instanceof Collection) {
@@ -374,19 +503,62 @@ public class Script implements AutoCloseable {
           log.info("{} size={}", key, m.size());
           m.forEach((k, v) -> log.info("\t{}\t{}", k, v));
         } else {
-          log.info("{}\t{}", key, value);
+          if (value instanceof OpResult) {
+            dumpResult((OpResult) value, 0, 0);
+          } else {
+            log.info("{}\t{}", key, value);
+          }
         }
       });
       log.info("=========  END script dump  ==========");
+      return null;
+    }
+
+    private void dumpResult(OpResult res, int index, int indent) {
+      StringBuilder indentSb = new StringBuilder();
+      for (int i = 0; i < indent; i++) {
+        indentSb.append('\t');
+      }
+      String indentStr = indentSb.toString();
+      log.info("{}{}.\t{}", indentStr, index, res.op);
+      if (res.result == null) {
+        return;
+      }
+      if (res.result instanceof Collection) {
+        @SuppressWarnings("unchecked")
+        Collection<Object> col = (Collection<Object>) res.result;
+        log.info("{}\tRes: size={}", indentStr, col.size());
+        int k = 1;
+        for (Object o : col) {
+          if (o instanceof OpResult) { // LOOP op results
+            dumpResult((OpResult) o, k, indent + 1);
+          } else {
+            log.info("{}\t-\t{}. {}", indentStr, k, o);
+          }
+          k++;
+        }
+      } else if (res.result instanceof Map) {
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map = (Map<String, Object>) res.result;
+        log.info("{}\tRes: size={}", indentStr, map.size());
+        map.forEach((k, v) -> log.info("{}\t-\t{}\t{}", indentStr, k, v));
+      } else if (res.result instanceof MapWriter) {
+        @SuppressWarnings("unchecked")
+        MapWriter mw = (MapWriter) res.result;
+        log.info("{}\tRes: size={}", indentStr, mw._size());
+        log.info("{}\t{}", indentStr, mw.jsonStr());
+      } else {
+        log.info("{}\tRes: {}", indentStr, res.result);
+      }
     }
   }
 
   /**
    * Execute a SolrRequest.
    */
-  public static class RunSolrRequest extends ScriptOp {
+  public static class RequestOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       String path = params.get("path", "/");
       SolrRequest.METHOD m = 
SolrRequest.METHOD.valueOf(params.get("httpMethod", "GET"));
       params.remove("httpMethod");
@@ -396,53 +568,127 @@ public class Script implements AutoCloseable {
       if (streamBody != null) {
         req.setContentWriter(new 
RequestWriter.StringPayloadContentWriter(streamBody, "application/json"));
       }
-      NamedList<Object> rsp = script.client.request(req);
-      @SuppressWarnings("unchecked")
-      List<NamedList<Object>> responses = (List<NamedList<Object>>) 
script.context.computeIfAbsent(RESPONSES_CTX_PROP, o -> new 
ArrayList<NamedList<Object>>());
-      responses.add(rsp);
+      return script.client.request(req);
     }
   }
 
   /**
    * Set (or reset) the timeout for operations to execute.
    */
-  public static class Timeout extends ScriptOp {
+  public static class TimeoutOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       String value = params.get("value", "" + DEFAULT_OP_TIMEOUT_MS);
       long newTimeout = DEFAULT_OP_TIMEOUT_MS;
       if (!value.equals("default")) {
         newTimeout = Long.parseLong(value);
       }
       script.context.put(TIMEOUT_PROP, newTimeout);
+      return null;
     }
   }
 
   /**
    * Sit idle for a while, 10s by default.
    */
-  public static class Wait extends ScriptOp {
+  public static class WaitOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       int timeMs = params.getInt("time", 10000);
       script.timeSource.sleep(timeMs);
+      return null;
     }
   }
 
   /**
    * Wait for a specific collection shape.
    */
-  public static class WaitCollection extends ScriptOp {
+  public static class WaitCollectionOp extends ScriptOp {
     @Override
-    public void execute(Script script) throws Exception {
+    public Object execute(Script script) throws Exception {
       String collection = params.required().get("collection");
       int shards = Integer.parseInt(params.required().get("shards"));
       int replicas = Integer.parseInt(params.required().get("replicas"));
       boolean withInactive = params.getBool("withInactive", false);
       boolean requireLeaders = params.getBool("requireLeaders", true);
       int waitSec = params.required().getInt("wait", 
CloudUtil.DEFAULT_TIMEOUT);
-      CloudUtil.waitForState(script.cloudManager, collection, waitSec, 
TimeUnit.SECONDS,
-          CloudUtil.clusterShape(shards, replicas, withInactive, 
requireLeaders));
+      try {
+        CloudUtil.waitForState(script.cloudManager, collection, waitSec, 
TimeUnit.SECONDS,
+            CloudUtil.clusterShape(shards, replicas, withInactive, 
requireLeaders));
+      } catch (Exception e) {
+        DocCollection coll = 
script.cloudManager.getClusterStateProvider().getCollection(collection);
+        throw new Exception("last collection state: " + coll, e);
+      }
+      DocCollection coll = 
script.cloudManager.getClusterStateProvider().getCollection(collection);
+      return Utils.fromJSONString(Utils.toJSONString(coll));
+    }
+  }
+
+  /**
+   * Wait for specific core to reach a state.
+   */
+  public static class WaitReplicaOp extends ScriptOp {
+    @Override
+    public Object execute(Script script) throws Exception {
+      String collection = params.required().get("collection");
+      String coreName = params.get("core");
+      String replicaName = params.get("replica");
+      if (coreName == null && replicaName == null) {
+        throw new Exception("Either 'core' or 'replica' must be specified.");
+      } else if (coreName != null && replicaName != null) {
+        throw new Exception("Only one of 'core' or 'replica' must be 
specified.");
+      }
+      int waitSec = params.required().getInt("wait", 
CloudUtil.DEFAULT_TIMEOUT);
+      String stateStr = params.get("state", Replica.State.ACTIVE.toString());
+      Replica.State expectedState = Replica.State.getState(stateStr);
+
+      TimeOut timeout = new TimeOut(waitSec, TimeUnit.SECONDS, 
script.timeSource);
+      DocCollection coll = null;
+      Replica replica = null;
+      while (!timeout.hasTimedOut()) {
+        ClusterState clusterState = 
script.cloudManager.getClusterStateProvider().getClusterState();
+        coll = clusterState.getCollectionOrNull(collection);
+        if (coll == null) { // does not yet exist?
+          timeout.sleep(250);
+          continue;
+        }
+        for (Replica r : coll.getReplicas()) {
+          if (coreName != null && coreName.equals(r.getCoreName())) {
+            replica = r;
+            break;
+          } else if (replicaName != null && replicaName.equals(r.getName())) {
+            replica = r;
+            break;
+          }
+        }
+        if (replica == null) {
+          timeout.sleep(250);
+          continue;
+        }
+        if (replica.getState() != expectedState) {
+          timeout.sleep(250);
+          continue;
+        } else {
+          break;
+        }
+      }
+      if (timeout.hasTimedOut()) {
+        String msg = "Timed out waiting for replica: collection=" + collection 
+ ", ";
+        if (coreName != null) {
+          msg += "core=" + coreName;
+        } else {
+          msg += "replica=" + replicaName;
+        }
+        if (coll == null) {
+          msg += ". Collection '" + collection + " not found.";
+        } else if (replica == null) {
+          msg += ". Replica not found, last collection state: " + coll;
+        } else {
+          msg += ". Replica did not reach desired state, last state: " + 
replica;
+        }
+        throw new Exception(msg);
+      }
+      return replica;
     }
   }
 
@@ -463,57 +709,63 @@ public class Script implements AutoCloseable {
         }
       }
     }
-  }
-
-  public static class Assert extends ScriptOp {
 
-    @Override
-    public void execute(Script script) throws Exception {
-      String key = params.get("key");
-      Condition condition = Condition.get(params.required().get("condition"));
-      if (condition == null) {
-        throw new IOException("Invalid 'condition' in params: " + params);
-      }
-      String expected = params.get("expected");
-      if (condition != Condition.NOT_NULL && condition != Condition.NULL && 
expected == null) {
-        throw new IOException("'expected' param is required when condition is 
" + condition);
-      }
-      Object value;
-      if (key != null) {
-        if (key.contains("/")) {
-          value = Utils.getObjectByPath(script.context, true, key);
-        } else {
-          value = script.context.get(key);
-        }
-      } else {
-        value = params.required().get("value");
-      }
+    public static boolean eval(Condition condition, Object value, String 
expected) {
       switch (condition) {
         case NULL:
           if (value != null) {
-            throw new IOException("expected value should be null but was '" + 
value + "'");
+            return false;
           }
           break;
         case NOT_NULL:
           if (value == null) {
-            throw new IOException("expected value should not be null");
+            return false;
           }
           break;
         case EQUALS:
           if (!expected.equals(String.valueOf(value))) {
-            throw new IOException("expected value is '" + expected + "' but 
actual value is '" + value + "'");
+            return false;
           }
           break;
         case NOT_EQUALS:
           if (expected.equals(String.valueOf(value))) {
-            throw new IOException("expected value is '" + expected + "' and 
actual value is the same while it should be different");
+            return false;
           }
           break;
       }
+      return true;
     }
   }
 
-  public static Script loadResource(SolrResourceLoader loader, CloudSolrClient 
client, String nodeName, String resource) throws Exception {
+  public static class AssertOp extends ScriptOp {
+
+    @Override
+    public Object execute(Script script) throws Exception {
+      String key = params.get("key");
+      Condition condition = Condition.get(params.required().get("condition"));
+      if (condition == null) {
+        throw new IOException("Invalid 'condition' in params: " + params);
+      }
+      String expected = params.get("expected");
+      if (condition != Condition.NOT_NULL && condition != Condition.NULL && 
expected == null) {
+        throw new IOException("'expected' param is required when condition is 
" + condition);
+      }
+      Object value;
+      if (key != null) {
+        value = Utils.getObjectByPath(script.context, true, key);
+      } else {
+        value = params.required().get("value");
+      }
+      if (!Condition.eval(condition, value, expected)) {
+        throw new Exception("Assertion failed: expected=" + expected +
+            ", condition=" + condition +
+            ", value=" + value);
+      }
+      return Boolean.TRUE;
+    }
+  }
+
+  public static Script parseResource(SolrResourceLoader loader, 
CloudSolrClient client, String nodeName, String resource) throws Exception {
     String data;
     try {
       InputStream in = loader.openResource(resource);
@@ -521,24 +773,37 @@ public class Script implements AutoCloseable {
     } catch (IOException e) {
       throw new Exception("cannot open script resource " + resource, e);
     }
-    return load(client, nodeName, data);
+    return parse(client, nodeName, data);
   }
 
+  private static final String PROP_PREFIX = "script.";
+
   /**
-   * Parse a DSL string and create a scenario ready to run.
+   * Parse a DSL string and create a script ready to run.
    * @param client connected Solr client
    * @param nodeName my node name, if applicable, or null
    * @param data DSL string with commands and parameters
-   * @return configured scenario
+   * @return configured script
    * @throws Exception on syntax errors
    */
-  public static Script load(CloudSolrClient client, String nodeName, String 
data) throws Exception {
+  public static Script parse(CloudSolrClient client, String nodeName, String 
data) throws Exception {
     Objects.requireNonNull(client, "Solr client must not be null here");
     Objects.requireNonNull(data, "script data must not be null");
     @SuppressWarnings("resource")
     Script script = new Script(client, nodeName);
+    // process system properties and put them in context
+    System.getProperties().forEach((name, value) -> {
+      String key = name.toString();
+      if (!key.startsWith(PROP_PREFIX)) {
+        return;
+      }
+      script.context.put(key.substring(PROP_PREFIX.length()), value);
+    });
+    Stack<CompoundOp> compoundOps = new Stack<>();
+    Stack<Integer> compoundStarts = new Stack<>();
     String[] lines = data.split("\\r?\\n");
     for (int i = 0; i < lines.length; i++) {
+      int lineNum = i + 1;
       String line = lines[i];
       line = line.trim();
       if (line.trim().isEmpty() || line.startsWith("#") || 
line.startsWith("//")) {
@@ -550,19 +815,24 @@ public class Script implements AutoCloseable {
       // split on blank
       String[] parts = expr.split("\\s+");
       if (parts.length > 2) {
-        log.warn("Invalid line - wrong number of parts {}, skipping: {}", 
parts.length, line);
-        continue;
+        throw new Exception("Syntax error on line " + lineNum + ": invalid 
line - wrong number of parts " + parts.length + ", expected at most 2.");
       }
       ScriptAction action = ScriptAction.get(parts[0]);
       if (action == null) {
-        log.warn("Invalid script action {}, skipping...", parts[0]);
-        continue;
+        throw new Exception("Syntax error on line " + lineNum + ": invalid 
script action '" + parts[0] + "'.");
       }
-      if (action == ScriptAction.LOOP_END) {
-        if (!script.context.containsKey("loop")) {
-          throw new IOException("LOOP_END without start!");
+      if (action == ScriptAction.END) {
+        if (compoundOps.isEmpty()) {
+          throw new Exception("Syntax error on line " + lineNum + ": compound 
block END without start.");
         }
-        script.context.remove("loop");
+        compoundOps.pop();
+        compoundStarts.pop();
+        continue;
+      } else if (action == ScriptAction.ELSE) {
+        if (!(compoundOps.peek() instanceof IfElseOp)) {
+          throw new Exception("Syntax error on line " + lineNum + ": ELSE 
without IF start.");
+        }
+        ((IfElseOp) compoundOps.peek()).parsingElse = true;
         continue;
       }
       Class<? extends ScriptOp> opClass = supportedOps.get(action);
@@ -585,70 +855,94 @@ public class Script implements AutoCloseable {
       }
       op.init(params);
       // loop handling
-      if (action == ScriptAction.LOOP_START) {
-        if (script.context.containsKey("loop")) {
-          throw new IOException("only one loop level is allowed");
-        }
-        script.context.put("loop", op);
+      if (action == ScriptAction.LOOP || action == ScriptAction.IF) {
+        compoundOps.add((CompoundOp) op);
+        compoundStarts.add(lineNum);
         script.ops.add(op);
         continue;
       }
-      LoopOp currentLoop = (LoopOp) script.context.get("loop");
-      if (currentLoop != null) {
-        currentLoop.ops.add(op);
+      CompoundOp currentCompound = compoundOps.isEmpty() ? null : 
compoundOps.peek();
+      if (currentCompound != null) {
+        currentCompound.addOp(op);
       } else {
         script.ops.add(op);
       }
     }
-    if (script.context.containsKey("loop")) {
-      throw new IOException("Unterminated loop statement");
+    if (!compoundOps.isEmpty()) {
+      throw new Exception("Syntax error on EOF: unterminated statement " +
+          compoundOps.peek() + ". Started on line " + compoundStarts.peek());
     }
     return script;
   }
 
+  /**
+   * Run the script. This is a one-shot operation
+   */
   public void run() throws Exception {
-    ExecutorService executor = 
ExecutorUtil.newMDCAwareSingleThreadExecutor(new 
SolrNamedThreadFactory("Script"));
+    if (executorService == null) {
+      throw new Exception("This script instance cannot be reused.");
+    }
     try {
-      run(executor);
+      for (int i = 0; i < ops.size(); i++) {
+        if (abortScript) {
+          log.info("-- abortScript requested, aborting after {} ops.", i);
+          return;
+        }
+        ScriptOp op = ops.get(i);
+        if (log.isInfoEnabled()) {
+          log.info("{}.\t{}\t{}", i + 1, op.getClass().getSimpleName(), 
op.initParams); // logOk
+        }
+        execOp(op);
+      }
+    } catch (Exception e) {
+      throw e;
     } finally {
-      executor.shutdownNow();
-      executor.awaitTermination(30, TimeUnit.SECONDS);
+      close();
     }
   }
 
-  /**
-   * Run the script.
-   */
-  public void run(ExecutorService executor) throws Exception {
-    for (int i = 0; i < ops.size(); i++) {
-      if (abortScript) {
-        log.info("-- abortScript requested, aborting after {} ops.", i);
-        return;
+  private void execOp(ScriptOp op) throws Exception {
+    // substitute parameters based on the current context
+    ClusterStateProvider clusterStateProvider = 
client.getClusterStateProvider();
+    ArrayList<String> liveNodes = new 
ArrayList<>(clusterStateProvider.getLiveNodes());
+    context.put(LIVE_NODES_CTX_PROP, liveNodes);
+    String randomNode = liveNodes.get(random.nextInt(liveNodes.size()));
+    context.put(RANDOM_NODE_CTX_PROP, randomNode);
+    ClusterState clusterState = clusterStateProvider.getClusterState();
+    context.put(COLLECTIONS_CTX_PROP, new 
ArrayList<>(clusterState.getCollectionStates().keySet()));
+    if (nodeName != null) {
+      context.put(NODE_NAME_CTX_PROP, nodeName);
+    }
+    op.prepareCurrentParams(this);
+    if (log.isInfoEnabled()) {
+      log.info("\t\t{}\t{}", op.getClass().getSimpleName(), op.params);
+    }
+    final ErrorHandling currentErrorHandling = getErrorHandling();
+    OpResult opResult = null;
+    if (op instanceof LoopOp) {
+      // execute directly - the loop will submit its ops to the same executor
+      try {
+        Object loopRes = op.execute(this);
+        if (loopRes instanceof Exception) {
+          throw (Exception) loopRes;
+        } else {
+          opResult = new OpResult(op, loopRes);
+          opResults.add(opResult);
+        }
+      } catch (InterruptedException e) {
+        throw new Exception("Interrupted while executing op " + op);
+      } catch (Exception e) {
+        if (currentErrorHandling != ErrorHandling.IGNORE) {
+          throw new Exception("Error executing op " + op, e);
+        } else {
+          opResult = new OpResult(op, e);
+          opResults.add(opResult);
+        }
       }
-      ScriptOp op = ops.get(i);
-      if (log.isInfoEnabled()) {
-        log.info("{}.\t{}\t{}", i + 1, op.getClass().getSimpleName(), 
op.initParams); // logOk
-      }
-      // substitute parameters based on the current context
-      ClusterStateProvider clusterStateProvider = 
client.getClusterStateProvider();
-      ArrayList<String> liveNodes = new 
ArrayList<>(clusterStateProvider.getLiveNodes());
-      context.put(LIVE_NODES_CTX_PROP, liveNodes);
-      String randomNode = liveNodes.get(random.nextInt(liveNodes.size()));
-      context.put(RANDOM_NODE_CTX_PROP, randomNode);
-      ClusterState clusterState = clusterStateProvider.getClusterState();
-      context.put(COLLECTIONS_CTX_PROP, new 
ArrayList<>(clusterState.getCollectionStates().keySet()));
-      if (nodeName != null) {
-        context.put(NODE_NAME_CTX_PROP, nodeName);
-      }
-      op.prepareCurrentParams(this);
-      if (log.isInfoEnabled()) {
-        log.info("\t\t{}\t{}", op.getClass().getSimpleName(), op.params);
-      }
-      final ErrorHandling currentErrorHandling = getErrorHandling();
-      Future<Exception> res = executor.submit(() -> {
+    } else {
+      Future<Object> res = executorService.submit(() -> {
         try {
-          op.execute(this);
-          return null;
+          return op.execute(this);
         } catch (Exception e) {
           this.abortScript = currentErrorHandling != ErrorHandling.IGNORE ? 
true : false;
           return e;
@@ -656,9 +950,12 @@ public class Script implements AutoCloseable {
       });
       long timeout = 
Long.parseLong(String.valueOf(context.getOrDefault(TIMEOUT_PROP, 
DEFAULT_OP_TIMEOUT_MS)));
       try {
-        Exception error = res.get(timeout, TimeUnit.MILLISECONDS);
-        if (error != null) {
-          throw error;
+        Object result = res.get(timeout, TimeUnit.MILLISECONDS);
+        if (result instanceof Exception) {
+          throw (Exception) result;
+        } else {
+          opResult = new OpResult(op, result);
+          opResults.add(opResult);
         }
       } catch (TimeoutException e) {
         throw new Exception("Timeout executing op " + op);
@@ -666,19 +963,35 @@ public class Script implements AutoCloseable {
         throw new Exception("Interrupted while executing op " + op);
       } catch (Exception e) {
         if (currentErrorHandling != ErrorHandling.IGNORE) {
-          throw e;
+          throw new Exception("Error executing op " + op, e);
         } else {
-          continue;
+          opResult = new OpResult(op, e);
+          opResults.add(opResult);
         }
       }
     }
+    if (log.isInfoEnabled()) {
+      log.info("\t\tResult\t{}", opResult.result);
+    }
+    context.put(LAST_RESULT_CTX_PROP, opResult);
   }
 
   public ErrorHandling getErrorHandling() {
-    return 
ErrorHandling.get(String.valueOf(context.getOrDefault(ERROR_HANDLING_PROP, 
ErrorHandling.END_SCRIPT)));
+    return 
ErrorHandling.get(String.valueOf(context.getOrDefault(ERROR_HANDLING_PROP, 
ErrorHandling.ABORT)));
   }
 
   @Override
-  public void close() throws IOException {
+  public void close() {
+    if (executorService != null && shouldCloseExecutor) {
+      executorService.shutdownNow();
+      try {
+        executorService.awaitTermination(60, TimeUnit.SECONDS);
+      } catch (InterruptedException e) {
+        // basically ignore, restore interrupted status
+        Thread.currentThread().interrupt();
+      } finally {
+        executorService = null;
+      }
+    }
   }
 }
diff --git a/solr/core/src/test-files/solr/initScript.txt 
b/solr/core/src/test-files/solr/initScript.txt
new file mode 100644
index 0000000..24f99ba
--- /dev/null
+++ b/solr/core/src/test-files/solr/initScript.txt
@@ -0,0 +1,19 @@
+# simple comment
+// another comment
+      
+set key=myNode&value=${_random_node_}
+request 
/admin/collections?action=CREATE&name=ScriptingTest_collection&numShards=2&nrtReplicas=2
+set key=oneCore&value=${_last_result_/result/success[0]/value/core}
+wait_collection collection=ScriptingTest_collection&shards=2&replicas=2
+set key=myNode&value=${_random_node_}
+request 
/admin/collections?action=ADDREPLICA&collection=ScriptingTest_collection&shard=shard1&node=${myNode}
+set key=myNode&value=${_live_nodes_[0]}
+request 
/admin/collections?action=ADDREPLICA&collection=ScriptingTest_collection&shard=shard1&node=${myNode}
+loop iterations=2
+  request 
/admin/collections?action=ADDREPLICA&collection=ScriptingTest_collection&shard=shard2&node=${myNode}
+  set key=oneCore&value=${_last_result_/result/success[0]/value/core}
+  wait_replica collection=ScriptingTest_collection&core=${oneCore}&state=ACTIVE
+  log 
key=_last_result_/result[0]/key&format=******%20Replica%20name:%20{}%20******
+end
+dump
+
diff --git a/solr/core/src/test-files/solr/initScript2.txt 
b/solr/core/src/test-files/solr/initScript2.txt
new file mode 100644
index 0000000..9f6a9aa
--- /dev/null
+++ b/solr/core/src/test-files/solr/initScript2.txt
@@ -0,0 +1,4 @@
+set key=_error_handling_&value=${testErrorHandling:fatal}
+request /admin/collections?action=DELETE&name=_nonexistent_
+request /admin/collections?action=CREATE&name=foobar&numShards=1&nrtReplicas=1
+wait_collection collection=foobar&shards=1&replicas=1
diff --git a/solr/core/src/test-files/solr/shutdownScript.txt 
b/solr/core/src/test-files/solr/shutdownScript.txt
new file mode 100644
index 0000000..ab9bc3b
--- /dev/null
+++ b/solr/core/src/test-files/solr/shutdownScript.txt
@@ -0,0 +1 @@
+request /admin/collections?action=DELETE&name=foobar
diff --git 
a/solr/core/src/test/org/apache/solr/util/scripting/ScriptingTest.java 
b/solr/core/src/test/org/apache/solr/util/scripting/ScriptingTest.java
index a387f3f..161ed1d 100644
--- a/solr/core/src/test/org/apache/solr/util/scripting/ScriptingTest.java
+++ b/solr/core/src/test/org/apache/solr/util/scripting/ScriptingTest.java
@@ -1,12 +1,18 @@
 package org.apache.solr.util.scripting;
 
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
 import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrResourceLoader;
 import org.junit.After;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.io.File;
+
 public class ScriptingTest extends SolrCloudTestCase {
 
   private static final String COLLECTION = ScriptingTest.class.getSimpleName() 
+ "_collection";
@@ -16,6 +22,8 @@ public class ScriptingTest extends SolrCloudTestCase {
 
   @BeforeClass
   public static void setupCluster() throws Exception {
+    // either set this, or copy the scripts into each node's solrHome dir
+    System.setProperty("solr.allow.unsafe.resourceloading", "true");
     configureCluster(3)
         .addConfig("conf", configset("cloud-minimal"))
         .configure();
@@ -26,38 +34,93 @@ public class ScriptingTest extends SolrCloudTestCase {
   @After
   public void cleanup() throws Exception {
     cluster.deleteAllCollections();
+    System.clearProperty("initScript");
+    System.clearProperty("shutdownScript");
+    System.clearProperty("testErrorHandling");
+    System.clearProperty("solr.allow.unsafe.resourceloading");
   }
 
   String testBasicScript =
       "# simple comment\n" +
           "// another comment\n" +
           "      \n" +
-          "solr_request /admin/collections?action=CREATE&name=" + COLLECTION + 
"&numShards=2&nrtReplicas=2\n" +
+          "set key=myNode&value=${_random_node_}\n" +
+          "request /admin/collections?action=CREATE&name=" + COLLECTION + 
"&numShards=2&nrtReplicas=2\n" +
+          // this one is tricky - retrieve object by index because we don't 
know the key, and
+          // then it's a map Entry, so retrieve its 'value' because again we 
don't know the key
+          "set 
key=oneCore&value=${_last_result_/result/success[0]/value/core}\n" +
           "wait_collection collection=" + COLLECTION + 
"&shards=2&replicas=2\n" +
-          "ctx_set key=myNode&value=${_random_node_}\n" +
-          "solr_request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
-          "ctx_set key=myNode&value=${_random_node_}\n" +
-          "solr_request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
-          "loop_start iterations=${iterative}\n" +
-          "  solr_request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
-          "  solr_request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
-          "loop_end\n" +
+          "set key=myNode&value=${_random_node_}\n" +
+          "request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
+          "set key=myNode&value=${_live_nodes_[0]}\n" +
+          "request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard1&node=${myNode}\n" +
+          "loop iterations=2\n" +
+          "  request /admin/collections?action=ADDREPLICA&collection=" + 
COLLECTION + "&shard=shard2&node=${myNode}\n" +
+          "  set 
key=oneCore&value=${_last_result_/result/success[0]/value/core}\n" +
+          "  wait_replica collection=" + COLLECTION + 
"&core=${oneCore}&state=ACTIVE\n" +
+          "  log 
key=_last_result_/result[0]/key&format=******%20Replica%20name:%20{}%20******\n"
 +
+          "end\n" +
           "dump\n";
 
   @Test
   public void testBasicScript() throws Exception {
-    Script script = Script.load(cluster.getSolrClient(), null, 
testBasicScript);
-    // use string value, otherwise PropertiesUtil won't work properly
-    script.context.put("iterative", "2");
+    Script script = Script.parse(cluster.getSolrClient(), null, 
testBasicScript);
     script.run();
+    DocCollection coll = 
cloudManager.getClusterStateProvider().getCollection(COLLECTION);
+    assertEquals("numShards", 2, coll.getSlices().size());
+    assertEquals("num replicas shard1", 4, 
coll.getSlice("shard1").getReplicas().size());
+    assertEquals("num replicas shard2", 4, 
coll.getSlice("shard2").getReplicas().size());
   }
 
   @Test
   public void testLoadResource() throws Exception {
-
+    String scriptName = "initScript.txt";
+    JettySolrRunner jetty = cluster.getRandomJetty(random());
+    String nodeName = jetty.getNodeName();
+    SolrResourceLoader loader = jetty.getCoreContainer().getResourceLoader();
+    File scriptFile = new File(TEST_PATH().resolve(scriptName).toString());
+//    File destFile = new File(jetty.getSolrHome(), scriptName);
+//    FileUtils.copyFile(scriptFile, destFile);
+    Script script = Script.parseResource(loader, cluster.getSolrClient(), 
nodeName, scriptFile.toString());
+    script.run();
+    DocCollection coll = 
cloudManager.getClusterStateProvider().getCollection(COLLECTION);
+    assertEquals("numShards", 2, coll.getSlices().size());
+    assertEquals("num replicas shard1", 4, 
coll.getSlice("shard1").getReplicas().size());
+    assertEquals("num replicas shard2", 4, 
coll.getSlice("shard2").getReplicas().size());
   }
 
   public void testErrorHandling() throws Exception {
+    String scriptName = "initScript2.txt";
+    File scriptFile = new File(TEST_PATH().resolve(scriptName).toString());
+    System.setProperty("initScript", scriptFile.toString());
+    JettySolrRunner jetty = cluster.startJettySolrRunner();
+    if (!jetty.getCoreContainer().isShutDown()) {
+      fail("should have failed - error handling set in the script to FATAL by 
default");
+    }
+    cluster.stopJettySolrRunner(jetty);
+    // abort the script
+    System.setProperty("testErrorHandling", "ABORT");
+    jetty = cluster.startJettySolrRunner();
+    cluster.waitForNode(jetty, 10);
+    assertFalse("node should be running when error handling is ABORT", 
jetty.getCoreContainer().isShutDown());
+    ClusterState clusterState = 
cloudManager.getClusterStateProvider().getClusterState();
+    assertNull("there should be no 'foobar' collection", 
clusterState.getCollectionOrNull("foobar"));
+    cluster.stopJettySolrRunner(jetty);
+
+    // ignore errors and continue
+    System.setProperty("testErrorHandling", "IGNORE");
+    jetty = cluster.startJettySolrRunner();
+    cluster.waitForNode(jetty, 10);
+    assertFalse("node should be running when error handling is IGNORE", 
jetty.getCoreContainer().isShutDown());
+    clusterState = cloudManager.getClusterStateProvider().getClusterState();
+    assertNotNull("there should be 'foobar' collection", 
clusterState.getCollectionOrNull("foobar"));
 
+    // test shutdown script
+    scriptFile = new 
File(TEST_PATH().resolve("shutdownScript.txt").toString());
+    System.setProperty("shutdownScript", scriptFile.toString());
+    cluster.stopJettySolrRunner(jetty);
+    cluster.waitForJettyToStop(jetty);
+    clusterState = cloudManager.getClusterStateProvider().getClusterState();
+    assertNull("there should be no 'foobar' collection", 
clusterState.getCollectionOrNull("foobar"));
   }
 }

Reply via email to