Merge branch 'master' into jira/solr-12730
diff --git a/lucene/core/src/java/org/apache/lucene/index/IndexFileDeleter.java b/lucene/core/src/java/org/apache/lucene/index/IndexFileDeleter.java
index cc9d2e0..cd5cf7f 100644
--- a/lucene/core/src/java/org/apache/lucene/index/IndexFileDeleter.java
+++ b/lucene/core/src/java/org/apache/lucene/index/IndexFileDeleter.java
@@ -20,6 +20,8 @@
 import java.io.Closeable;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.file.NoSuchFileException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -108,7 +110,7 @@
 
   /** Change to true to see details of reference counts when
    *  infoStream is enabled */
-  public static boolean VERBOSE_REF_COUNTS = false;
+  public static boolean VERBOSE_REF_COUNTS = true;
 
   private final IndexWriter writer;
 
@@ -564,7 +566,16 @@
     RefCount rc = getRefCount(fileName);
     if (infoStream.isEnabled("IFD")) {
       if (VERBOSE_REF_COUNTS) {
-        infoStream.message("IFD", "  IncRef \"" + fileName + "\": pre-incr count is " + rc.count);
+        // todo nocommit remove the extra logging
+        String extra = null;
+        if ("_0.fdt".equals(fileName))  {
+          StringWriter stringWriter = new StringWriter();
+          PrintWriter writer = new PrintWriter(stringWriter);
+          new Exception().printStackTrace(writer);
+          extra = stringWriter.toString();
+        }
+
+        infoStream.message("IFD", "  IncRef \"" + fileName + "\": pre-incr count is " + rc.count + (extra != null ? " and stack is " + extra : ""));
       }
     }
     rc.IncRef();
@@ -602,7 +613,15 @@
     RefCount rc = getRefCount(fileName);
     if (infoStream.isEnabled("IFD")) {
       if (VERBOSE_REF_COUNTS) {
-        infoStream.message("IFD", "  DecRef \"" + fileName + "\": pre-decr count is " + rc.count);
+        String extra = null;
+        // todo nocommit remove the extra logging
+        if ("_0.fdt".equals(fileName))  {
+          StringWriter stringWriter = new StringWriter();
+          PrintWriter writer = new PrintWriter(stringWriter);
+          new Exception().printStackTrace(writer);
+          extra = stringWriter.toString();
+        }
+        infoStream.message("IFD", "  DecRef \"" + fileName + "\": pre-decr count is " + rc.count + (extra != null ? " and stack is " + extra : ""));
       }
     }
     if (rc.DecRef() == 0) {
diff --git a/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java b/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java
index fb03f85..db4e1f9 100644
--- a/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java
+++ b/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java
@@ -339,6 +339,7 @@
     }
     privateDeleteFile(name, false);
     maybeDeletePendingFiles();
+    System.out.println("Deleted file: " + name);
   }
 
   /** Try to delete any pending files that we had previously tried to delete but failed
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
index aa4909d..b9370ac 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
@@ -686,6 +686,13 @@
                                   boolean firstReplicaNrt) {
     String splitKey = message.getStr("split.key");
     String rangesStr = message.getStr(CoreAdminParams.RANGES);
+    String fuzzStr = message.getStr(CommonAdminParams.SPLIT_FUZZ, "0");
+    float fuzz = 0.0f;
+    try {
+      fuzz = Float.parseFloat(fuzzStr);
+    } catch (Exception e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "?Invalid numeric value of 'fuzz': " + fuzzStr);
+    }
 
     DocRouter.Range range = parentSlice.getRange();
     if (range == null) {
@@ -752,7 +759,7 @@
         throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
             "A shard can only be split into "+MIN_NUM_SUB_SHARDS+" to " + MAX_NUM_SUB_SHARDS
             + " subshards in one split request. Provided "+NUM_SUB_SHARDS+"=" + numSubShards);
-      subRanges.addAll(router.partitionRange(numSubShards, range));
+      subRanges.addAll(router.partitionRange(numSubShards, range, fuzz));
     }
 
     for (int i = 0; i < subRanges.size(); i++) {
diff --git a/solr/core/src/java/org/apache/solr/cloud/autoscaling/IndexSizeTrigger.java b/solr/core/src/java/org/apache/solr/cloud/autoscaling/IndexSizeTrigger.java
index 31322c0..27daec2 100644
--- a/solr/core/src/java/org/apache/solr/cloud/autoscaling/IndexSizeTrigger.java
+++ b/solr/core/src/java/org/apache/solr/cloud/autoscaling/IndexSizeTrigger.java
@@ -65,6 +65,7 @@
   public static final String BELOW_OP_PROP = "belowOp";
   public static final String COLLECTIONS_PROP = "collections";
   public static final String MAX_OPS_PROP = "maxOps";
+  public static final String SPLIT_FUZZ_PROP = CommonAdminParams.SPLIT_FUZZ;
   public static final String SPLIT_METHOD_PROP = CommonAdminParams.SPLIT_METHOD;
 
   public static final String BYTES_SIZE_PROP = "__bytes__";
@@ -80,6 +81,7 @@
   private long aboveBytes, aboveDocs, belowBytes, belowDocs;
   private int maxOps;
   private SolrIndexSplitter.SplitMethod splitMethod;
+  private float splitFuzz;
   private CollectionParams.CollectionAction aboveOp, belowOp;
   private final Set<String> collections = new HashSet<>();
   private final Map<String, Long> lastAboveEventMap = new ConcurrentHashMap<>();
@@ -89,7 +91,7 @@
     super(TriggerEventType.INDEXSIZE, name);
     TriggerUtils.validProperties(validProperties,
         ABOVE_BYTES_PROP, ABOVE_DOCS_PROP, BELOW_BYTES_PROP, BELOW_DOCS_PROP,
-        COLLECTIONS_PROP, MAX_OPS_PROP, SPLIT_METHOD_PROP);
+        COLLECTIONS_PROP, MAX_OPS_PROP, SPLIT_METHOD_PROP, SPLIT_FUZZ_PROP);
   }
 
   @Override
@@ -175,6 +177,12 @@
       throw new TriggerValidationException(getName(), SPLIT_METHOD_PROP, "Unknown value '" + CommonAdminParams.SPLIT_METHOD +
           ": " + methodStr);
     }
+    String fuzzStr = String.valueOf(properties.getOrDefault(SPLIT_FUZZ_PROP, 0.0f));
+    try {
+      splitFuzz = Float.parseFloat(fuzzStr);
+    } catch (Exception e) {
+      throw new TriggerValidationException(getName(), SPLIT_FUZZ_PROP, "invalid value: '" + fuzzStr + "': " + e.getMessage());
+    }
   }
 
   @Override
@@ -278,6 +286,7 @@
             String registry = SolrCoreMetricManager.createRegistryName(true, coll, sh, replicaName, null);
             String tag = "metrics:" + registry + ":INDEX.sizeInBytes";
             metricTags.put(tag, info);
+            metricTags.put("metrics:" + registry + ":INDEX.sizeDetails", info);
             tag = "metrics:" + registry + ":SEARCHER.searcher.numDocs";
             metricTags.put(tag, info);
           });
@@ -399,6 +408,9 @@
         op.addHint(Suggester.Hint.COLL_SHARD, new Pair<>(coll, r.getShard()));
         Map<String, Object> params = new HashMap<>();
         params.put(CommonAdminParams.SPLIT_METHOD, splitMethod.toLower());
+        if (splitFuzz > 0) {
+          params.put(CommonAdminParams.SPLIT_FUZZ, splitFuzz);
+        }
         op.addHint(Suggester.Hint.PARAMS, params);
         ops.add(op);
         Long time = lastAboveEventMap.get(r.getCore());
@@ -451,6 +463,14 @@
     if (ops.isEmpty()) {
       return;
     }
+    try {
+      ClusterState cs = cloudManager.getClusterStateProvider().getClusterState();
+      cs.forEachCollection(coll -> {
+        log.debug("##== Collection: {}", coll);
+      });
+    } catch (IOException e) {
+      throw new RuntimeException("oops: ", e);
+    }
     if (processor.process(new IndexSizeEvent(getName(), eventTime.get(), ops, aboveSize, belowSize))) {
       // update last event times
       aboveSize.forEach((coll, replicas) -> {
diff --git a/solr/core/src/java/org/apache/solr/core/DirectoryFactory.java b/solr/core/src/java/org/apache/solr/core/DirectoryFactory.java
index fab3300..a05a36c 100644
--- a/solr/core/src/java/org/apache/solr/core/DirectoryFactory.java
+++ b/solr/core/src/java/org/apache/solr/core/DirectoryFactory.java
@@ -280,6 +280,8 @@
         break;
       }
     }
+
+    System.out.println(Arrays.toString(files) + " size=" + size + " on path: " + directory.toString());
     
     return size;
   }
diff --git a/solr/core/src/java/org/apache/solr/core/IndexDeletionPolicyWrapper.java b/solr/core/src/java/org/apache/solr/core/IndexDeletionPolicyWrapper.java
index 40e65b7..ee7ed36 100644
--- a/solr/core/src/java/org/apache/solr/core/IndexDeletionPolicyWrapper.java
+++ b/solr/core/src/java/org/apache/solr/core/IndexDeletionPolicyWrapper.java
@@ -130,6 +130,7 @@
     if (reserveCount == null) reserveCount = new AtomicInteger();
     reserveCount.incrementAndGet();
     savedCommits.put(indexCommitGen, reserveCount);
+    log.debug("Saving commit point for generation {}", indexCommitGen);
   }
 
   /** Release a previously saved commit point */
@@ -138,6 +139,7 @@
     if (reserveCount == null) return;// this should not happen
     if (reserveCount.decrementAndGet() <= 0) {
       savedCommits.remove(indexCommitGen);
+      log.debug("Releasing commit point for generation {}", indexCommitGen);
     }
   }
 
@@ -189,11 +191,20 @@
     @Override
     public void delete() {
       Long gen = delegate.getGeneration();
+      log.debug("Checking whether we can delete commit point with generation: {}", gen);
       Long reserve = reserves.get(gen);
-      if (reserve != null && System.nanoTime() < reserve) return;
-      if (savedCommits.containsKey(gen)) return;
+      long currentTime = System.nanoTime();
+      if (reserve != null && currentTime < reserve) {
+        log.debug("Commit point with generation: {} not deleted because its reserve {} is less than current time {}", gen, reserve, currentTime);
+        return;
+      }
+      if (savedCommits.containsKey(gen))  {
+        log.debug("Commit point with generation: {} not deleted because it is saved");
+        return;
+      }
       if (snapshotMgr.isSnapshotted(gen)) return;
       delegate.delete();
+      log.debug("Commit point with generation: {} deleted", gen);
     }
 
     @Override
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index 6e13039..1af7771 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -433,6 +433,7 @@
         dir = directoryFactory.get(getIndexDir(), DirContext.DEFAULT, solrConfig.indexConfig.lockType);
         try {
           size = DirectoryFactory.sizeOfDirectory(dir);
+          System.out.println("Found size=" + size + " for indexDir=" + getIndexDir());
         } finally {
           directoryFactory.release(dir);
         }
@@ -443,6 +444,31 @@
     return size;
   }
 
+  String getIndexSizeDetails() {
+    Directory dir;
+    StringBuilder sb = new StringBuilder();
+    try {
+      if (directoryFactory.exists(getIndexDir())) {
+        dir = directoryFactory.get(getIndexDir(), DirContext.DEFAULT, solrConfig.indexConfig.lockType);
+        try {
+          String[] files = dir.listAll();
+          Arrays.sort(files);
+          for (String file : files) {
+            sb.append('\n');
+            sb.append(file);
+            sb.append('\t');
+            sb.append(String.valueOf(dir.fileLength(file)));
+          }
+        } finally {
+          directoryFactory.release(dir);
+        }
+      }
+    } catch (IOException e) {
+      SolrException.log(log, "IO error while trying to get the details of the Directory", e);
+    }
+    return sb.toString();
+  }
+
   @Override
   public String getName() {
     return name;
@@ -1161,6 +1187,7 @@
     manager.registerGauge(this, registry, () -> resourceLoader.getInstancePath().toString(), getMetricTag(), true, "instanceDir", Category.CORE.toString());
     manager.registerGauge(this, registry, () -> isClosed() ? "(closed)" : getIndexDir(), getMetricTag(), true, "indexDir", Category.CORE.toString());
     manager.registerGauge(this, registry, () -> isClosed() ? 0 : getIndexSize(), getMetricTag(), true, "sizeInBytes", Category.INDEX.toString());
+    manager.registerGauge(this, registry, () -> isClosed() ? "" : getIndexSizeDetails(), getMetricTag(), true, "sizeDetails", Category.INDEX.toString());
     manager.registerGauge(this, registry, () -> isClosed() ? "(closed)" : NumberUtils.readableSize(getIndexSize()), getMetricTag(), true, "size", Category.INDEX.toString());
     if (coreContainer != null) {
       manager.registerGauge(this, registry, () -> coreContainer.getNamesForCore(this), getMetricTag(), true, "aliases", Category.CORE.toString());
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index dfb3c6b..462da77 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -144,6 +144,7 @@
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.IN_PLACE_MOVE;
 import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
 import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CommonParams.NAME;
@@ -640,6 +641,7 @@
       String rangesStr = req.getParams().get(CoreAdminParams.RANGES);
       String splitKey = req.getParams().get("split.key");
       String numSubShards = req.getParams().get(NUM_SUB_SHARDS);
+      String fuzz = req.getParams().get(SPLIT_FUZZ);
 
       if (splitKey == null && shard == null) {
         throw new SolrException(ErrorCode.BAD_REQUEST, "At least one of shard, or split.key should be specified.");
@@ -656,6 +658,10 @@
         throw new SolrException(ErrorCode.BAD_REQUEST,
             "numSubShards can not be specified with split.key or ranges parameters");
       }
+      if (fuzz != null && (splitKey != null || rangesStr != null)) {
+        throw new SolrException(ErrorCode.BAD_REQUEST,
+            "fuzz can not be specified with split.key or ranges parameters");
+      }
 
       Map<String, Object> map = copy(req.getParams(), null,
           COLLECTION_PROP,
@@ -665,7 +671,8 @@
           WAIT_FOR_FINAL_STATE,
           TIMING,
           SPLIT_METHOD,
-          NUM_SUB_SHARDS);
+          NUM_SUB_SHARDS,
+          SPLIT_FUZZ);
       return copyPropertiesWithPrefix(req.getParams(), map, COLL_PROP_PREFIX);
     }),
     DELETESHARD_OP(DELETESHARD, (req, rsp, h) -> {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
index 752e021..9b9f948 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
@@ -17,6 +17,7 @@
 
 package org.apache.solr.handler.admin;
 
+import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -41,6 +42,7 @@
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.metrics.SolrMetricManager;
@@ -49,11 +51,15 @@
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 import org.apache.solr.util.stats.MetricUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Request handler to return metrics
  */
 public class MetricsHandler extends RequestHandlerBase implements PermissionNameProvider {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
   final SolrMetricManager metricManager;
 
   public static final String COMPACT_PARAM = "compact";
@@ -99,6 +105,8 @@
     }
 
     handleRequest(req.getParams(), (k, v) -> rsp.add(k, v));
+    log.debug("##== Req: {}", req);
+    log.debug("##== Rsp: {}", Utils.toJSONString(rsp.getValues()));
   }
   
   public void handleRequest(SolrParams params, BiConsumer<String, Object> consumer) throws Exception {
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
index 48b2417..0df2b16 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
@@ -18,6 +18,7 @@
 
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -25,6 +26,7 @@
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
 import org.apache.lucene.index.ConcurrentMergeScheduler;
+import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriter.IndexReaderWarmer;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.MergePolicy;
@@ -48,6 +50,7 @@
 import org.apache.solr.util.SolrPluginUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
 
 import static org.apache.solr.core.Config.assertWarnOrFail;
 
@@ -283,7 +286,30 @@
 
   private MergeScheduler buildMergeScheduler(IndexSchema schema) {
     String msClassName = mergeSchedulerInfo == null ? SolrIndexConfig.DEFAULT_MERGE_SCHEDULER_CLASSNAME : mergeSchedulerInfo.className;
-    MergeScheduler scheduler = schema.getResourceLoader().newInstance(msClassName, MergeScheduler.class);
+    // todo nocommit -- remove this scheduler instance with proper MDC logging support inside merge scheduler threads
+    MergeScheduler scheduler = new ConcurrentMergeScheduler() {
+      @Override
+      protected synchronized MergeThread getMergeThread(IndexWriter writer, MergePolicy.OneMerge merge) throws IOException {
+        MergeThread mergeThread = super.getMergeThread(writer, merge);
+        final Map<String, String> submitterContext = MDC.getCopyOfContextMap();
+        StringBuilder contextString = new StringBuilder();
+        if (submitterContext != null) {
+          Collection<String> values = submitterContext.values();
+
+          for (String value : values) {
+            contextString.append(value + " ");
+          }
+          if (contextString.length() > 1) {
+            contextString.setLength(contextString.length() - 1);
+          }
+        }
+
+        String ctxStr = contextString.toString().replace("/", "//");
+        final String submitterContextStr = ctxStr.length() <= 512 ? ctxStr : ctxStr.substring(0, 512);
+        mergeThread.setName(mergeThread.getName() + "-processing-" + submitterContextStr);
+        return mergeThread;
+      }
+    };
 
     if (mergeSchedulerInfo != null) {
       // LUCENE-5080: these two setters are removed, so we have to invoke setMaxMergesAndThreads
@@ -311,5 +337,4 @@
 
     return scheduler;
   }
-
 }
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
index 334a29d..6ed0b1f 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
@@ -174,7 +174,7 @@
           log.error("Original error closing IndexWriter:", e);
           throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error reopening IndexWriter after failed close", e1);
         }
-        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error closing current IndexWriter, aborting offline split...", e);
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error closing current IndexWriter, aborting 'link' split...", e);
       }
     }
     boolean success = false;
@@ -195,7 +195,7 @@
         t = timings.sub("parentApplyBufferedUpdates");
         ulog.applyBufferedUpdates();
         t.stop();
-        log.info("Splitting in 'offline' mode " + (success? "finished" : "FAILED") +
+        log.info("Splitting in 'link' mode " + (success? "finished" : "FAILED") +
             ": re-opened parent IndexWriter.");
       }
     }
diff --git a/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessor.java b/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessor.java
index 004f4f7..8bdc8bc 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessor.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessor.java
@@ -235,7 +235,8 @@
     
     // this should always be used - see filterParams
     DistributedUpdateProcessorFactory.addParamToDistributedRequestWhitelist
-      (this.req, UpdateParams.UPDATE_CHAIN, TEST_DISTRIB_SKIP_SERVERS, CommonParams.VERSION_FIELD);
+      (this.req, UpdateParams.UPDATE_CHAIN, TEST_DISTRIB_SKIP_SERVERS, CommonParams.VERSION_FIELD,
+          UpdateParams.EXPUNGE_DELETES, UpdateParams.OPTIMIZE, UpdateParams.MAX_OPTIMIZE_SEGMENTS);
 
     CoreContainer cc = req.getCore().getCoreContainer();
 
@@ -1390,7 +1391,7 @@
     }
 
     if (dropCmd) {
-      // TODO: do we need to add anything to the response?
+      // TODO: do we need to add anytprocehing to the response?
       return;
     }
 
diff --git a/solr/core/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
index f671842..d98ba99 100644
--- a/solr/core/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
+++ b/solr/core/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
@@ -46,6 +46,7 @@
   </requestHandler>
   <indexConfig>
     <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+    <infoStream>true</infoStream>
   </indexConfig>
 </config>
 
diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/IndexSizeTriggerTest.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/IndexSizeTriggerTest.java
index ec2315d..314ddbd 100644
--- a/solr/core/src/test/org/apache/solr/cloud/autoscaling/IndexSizeTriggerTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/IndexSizeTriggerTest.java
@@ -68,7 +68,7 @@
 /**
  *
  */
-@LogLevel("org.apache.solr.cloud.autoscaling=DEBUG")
+//@LogLevel("org.apache.solr.cloud.autoscaling=DEBUG;org.apache.solr.handler.admin.MetricsHandler=DEBUG;org.apache.solr.core.SolrDeletionPolicy=DEBUG;org.apache.solr.core.IndexDeletionPolicyWrapper=DEBUG;org.apache.solr.client.solrj.cloud.autoscaling=DEBUG;org.apache.solr.cloud.api.collections=DEBUG;org.apache.solr.cloud.Overseer=DEBUG;org.apache.solr.cloud.overseer=DEBUGorg.apache.solr.client.solrj.cloud.autoscaling=DEBUG;org.apache.solr.cloud.api.collections=DEBUG;org.apache.solr.cloud.Overseer=DEBUG;org.apache.solr.cloud.overseer=DEBUG")
 public class IndexSizeTriggerTest extends SolrCloudTestCase {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
@@ -91,10 +91,11 @@
 
   @BeforeClass
   public static void setupCluster() throws Exception {
+    System.setProperty("solr.directoryFactory", "solr.StandardDirectoryFactory");
     configureCluster(2)
         .addConfig("conf", configset("cloud-minimal"))
         .configure();
-    if (random().nextBoolean()) {
+    if (random().nextBoolean() || true) {
       cloudManager = cluster.getJettySolrRunner(0).getCoreContainer().getZkController().getSolrCloudManager();
       solrClient = cluster.getSolrClient();
       loader = cluster.getJettySolrRunner(0).getCoreContainer().getResourceLoader();
@@ -505,9 +506,10 @@
     }
     assertTrue("maxSize should be non-zero", maxSize > 0);
 
-    int aboveBytes = maxSize * 2 / 3;
+    int aboveBytes = maxSize * 9 / 10;
 
-    long waitForSeconds = 3 + random().nextInt(5);
+    // need to wait for recovery after splitting
+    long waitForSeconds = 10 + random().nextInt(5);
 
     // the trigger is initially disabled so that we have time to add listeners
     // and have them capture all events once the trigger is enabled
@@ -566,6 +568,7 @@
         "'name' : 'index_size_trigger4'" +
         "}" +
         "}";
+    log.info("-- resuming trigger");
     req = createAutoScalingRequest(SolrRequest.METHOD.POST, resumeTriggerCommand);
     response = solrClient.request(req);
     assertEquals(response.get("result").toString(), "success");
@@ -574,6 +577,17 @@
 
     boolean await = finished.await(90000 / SPEED, TimeUnit.MILLISECONDS);
     assertTrue("did not finish processing in time", await);
+    log.info("-- suspending trigger");
+    // suspend the trigger to avoid generating more events
+    String suspendTriggerCommand = "{" +
+        "'suspend-trigger' : {" +
+        "'name' : 'index_size_trigger4'" +
+        "}" +
+        "}";
+    req = createAutoScalingRequest(SolrRequest.METHOD.POST, suspendTriggerCommand);
+    response = solrClient.request(req);
+    assertEquals(response.get("result").toString(), "success");
+
     assertEquals(1, listenerEvents.size());
     List<CapturedEvent> events = listenerEvents.get("capturing4");
     assertNotNull("'capturing4' events not found", events);
@@ -617,16 +631,8 @@
     listenerEvents.clear();
     finished = new CountDownLatch(1);
 
-    // suspend the trigger first so that we can safely delete all docs
-    String suspendTriggerCommand = "{" +
-        "'suspend-trigger' : {" +
-        "'name' : 'index_size_trigger4'" +
-        "}" +
-        "}";
-    req = createAutoScalingRequest(SolrRequest.METHOD.POST, suspendTriggerCommand);
-    response = solrClient.request(req);
-    assertEquals(response.get("result").toString(), "success");
 
+    log.info("-- deleting documents");
     for (int j = 0; j < 10; j++) {
       UpdateRequest ureq = new UpdateRequest();
       ureq.setParam("collection", collectionName);
@@ -635,6 +641,7 @@
       }
       solrClient.request(ureq);
     }
+    cloudManager.getTimeSource().sleep(5000);
     // make sure the actual index size is reduced by deletions, otherwise we may still violate aboveBytes
     UpdateRequest ur = new UpdateRequest();
     ur.setParam(UpdateParams.COMMIT, "true");
@@ -643,9 +650,28 @@
     ur.setParam(UpdateParams.MAX_OPTIMIZE_SEGMENTS, "1");
     ur.setParam(UpdateParams.WAIT_SEARCHER, "true");
     ur.setParam(UpdateParams.OPEN_SEARCHER, "true");
+    log.info("-- requesting optimize / expungeDeletes / commit");
     solrClient.request(ur, collectionName);
 
-    // resume trigger
+    // wait for the segments to merge to reduce the index size
+    cloudManager.getTimeSource().sleep(50000);
+
+    // add some docs so that every shard gets an update
+    // we can reduce the number of docs here but this also works
+    for (int j = 0; j < 1; j++) {
+      UpdateRequest ureq = new UpdateRequest();
+      ureq.setParam("collection", collectionName);
+      for (int i = 0; i < 98; i++) {
+        ureq.add("id", "id-" + (i * 100) + "-" + j);
+      }
+      solrClient.request(ureq);
+    }
+
+    log.info("-- requesting commit");
+    solrClient.commit(collectionName, true, true);
+
+    // resume the trigger
+    log.info("-- resuming trigger");
     req = createAutoScalingRequest(SolrRequest.METHOD.POST, resumeTriggerCommand);
     response = solrClient.request(req);
     assertEquals(response.get("result").toString(), "success");
@@ -654,6 +680,12 @@
 
     await = finished.await(90000 / SPEED, TimeUnit.MILLISECONDS);
     assertTrue("did not finish processing in time", await);
+    log.info("-- suspending trigger");
+    req = createAutoScalingRequest(SolrRequest.METHOD.POST, suspendTriggerCommand);
+    response = solrClient.request(req);
+    assertEquals(response.get("result").toString(), "success");
+//    System.exit(-1);
+
     assertEquals(1, listenerEvents.size());
     events = listenerEvents.get("capturing4");
     assertNotNull("'capturing4' events not found", events);
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SplitShardSuggester.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SplitShardSuggester.java
index 559ff8a..8aafef8 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SplitShardSuggester.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SplitShardSuggester.java
@@ -47,7 +47,11 @@
     }
     Pair<String, String> collShard = shards.iterator().next();
     Map<String, Object> params = (Map<String, Object>)hints.getOrDefault(Hint.PARAMS, Collections.emptyMap());
+    Float splitFuzz = (Float)params.get(CommonAdminParams.SPLIT_FUZZ);
     CollectionAdminRequest.SplitShard req = CollectionAdminRequest.splitShard(collShard.first()).setShardName(collShard.second());
+    if (splitFuzz != null) {
+      req.setSplitFuzz(splitFuzz);
+    }
     String splitMethod = (String)params.get(CommonAdminParams.SPLIT_METHOD);
     if (splitMethod != null) {
       req.setSplitMethod(splitMethod);
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
index 4f26984..3769c2e 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
@@ -1154,6 +1154,7 @@
     protected String shard;
     protected String splitMethod;
     protected Integer numSubShards;
+    protected Float splitFuzz;
 
     private Properties properties;
 
@@ -1183,6 +1184,15 @@
       return splitMethod;
     }
 
+    public SplitShard setSplitFuzz(float splitFuzz) {
+      this.splitFuzz = splitFuzz;
+      return this;
+    }
+
+    public Float getSplitFuzz() {
+      return splitFuzz;
+    }
+
     public SplitShard setSplitKey(String splitKey) {
       this.splitKey = splitKey;
       return this;
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/DocRouter.java b/solr/solrj/src/java/org/apache/solr/common/cloud/DocRouter.java
index 846c25e..c1471b5 100644
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/DocRouter.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/DocRouter.java
@@ -30,6 +30,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 
 import static org.apache.solr.common.cloud.DocCollection.DOC_ROUTER;
 
@@ -153,24 +154,59 @@
   }
 
   /**
-   * Returns the range for each partition
+   * Split the range into partitions.
+   * @param partitions number of partitions
+   * @param range range to split
    */
   public List<Range> partitionRange(int partitions, Range range) {
+    return partitionRange(partitions, range, 0.0f);
+  }
+
+  /**
+   * Split the range into partitions with inexact sizes.
+   * @param partitions number of partitions
+   * @param range range to split
+   * @param fuzz value between 0 (inclusive) and 0.5 indicating inexact split, i.e. percentage
+   *        of variation in resulting ranges - odd ranges will be larger and even ranges will be smaller
+   *        by up to that percentage.
+   */
+  public List<Range> partitionRange(int partitions, Range range, float fuzz) {
     int min = range.min;
     int max = range.max;
 
     assert max >= min;
+    if (fuzz > 0.5f) {
+      throw new IllegalArgumentException("'fuzz' parameter must be <= 0.5f but was " + fuzz);
+    }
     if (partitions == 0) return Collections.EMPTY_LIST;
     long rangeSize = (long)max - (long)min;
     long rangeStep = Math.max(1, rangeSize / partitions);
+    long fuzzStep = Math.round(rangeStep * fuzz / 2.0f);
+    // use a predictable pseudo-random
+    Random r = null;
+    if (fuzzStep > 2) {
+      r = new Random(0);
+      // limit randomness to half the range
+      fuzzStep = fuzzStep / 2;
+    }
 
     List<Range> ranges = new ArrayList<>(partitions);
 
     long start = min;
     long end = start;
+    boolean odd = true;
 
     while (end < max) {
       end = start + rangeStep;
+      if (fuzzStep > 0) {
+        long currentFuzz = r != null ? fuzzStep + r.nextLong() % fuzzStep : fuzzStep;
+        if (odd) {
+          end = end + currentFuzz;
+        } else {
+          end = end - currentFuzz;
+        }
+        odd = !odd;
+      }
       // make last range always end exactly on MAX_VALUE
       if (ranges.size() == partitions - 1) {
         end = max;
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
index c080342..13d304c 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
@@ -27,8 +27,10 @@
   String IN_PLACE_MOVE = "inPlaceMove";
   /** Method to use for shard splitting. */
   String SPLIT_METHOD = "splitMethod";
-  /** **/
+  /** Number of sub-shards to create. **/
   String NUM_SUB_SHARDS = "numSubShards";
   /** Timeout for replicas to become active. */
   String TIMEOUT = "timeout";
+  /** Inexact shard splitting factor. */
+  String SPLIT_FUZZ = "splitFuzz";
 }