You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by tf...@apache.org on 2019/10/28 23:26:00 UTC

[lucene-solr] branch branch_8x updated (8489b0f -> 220faa5)

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

tflobbe pushed a change to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git.


    from 8489b0f  a simple perf test to check serialization/deserialization perf
     new bf26279  SOLR-13865: Migrate replica routing code to SolrJ (#974)
     new 220faa5  SOLR-13865: Add missing package docs

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 solr/CHANGES.txt                                   |   2 +
 .../java/org/apache/solr/cloud/ZkController.java   |   1 +
 .../apache/solr/handler/admin/MetricsHandler.java  |   4 +-
 .../solr/handler/component/HttpShardHandler.java   |   1 +
 .../handler/component/HttpShardHandlerFactory.java | 357 +++------------------
 .../java/org/apache/solr/util/TestInjection.java   |   8 -
 .../cloud/RoutingToNodesWithPropertiesTest.java    |   9 +-
 .../component/TestHttpShardHandlerFactory.java     | 211 ------------
 .../routing}/AffinityReplicaListTransformer.java   |  14 +-
 .../AffinityReplicaListTransformerFactory.java     |  18 +-
 .../routing/NodePreferenceRulesComparator.java     | 179 +++++++++++
 .../solrj/{impl => routing}/PreferenceRule.java    |   2 +-
 .../solrj/routing}/ReplicaListTransformer.java     |   2 +-
 .../routing}/ReplicaListTransformerFactory.java    |   8 +-
 .../RequestReplicaListTransformerGenerator.java    | 168 ++++++++++
 .../routing}/ShufflingReplicaListTransformer.java  |   4 +-
 .../solr/client/solrj/routing/package-info.java}   |  26 +-
 .../solr/common}/cloud/NodesSysPropsCacher.java    |  18 +-
 .../solr/common/util/CommonTestInjection.java}     |  31 +-
 .../routing/NodePreferenceRulesComparatorTest.java | 155 +++++++++
 .../solrj/routing}/ReplicaListTransformerTest.java |   3 +-
 ...RequestReplicaListTransformerGeneratorTest.java | 152 +++++++++
 .../ShufflingReplicaListTransformerTest.java       |   2 +-
 23 files changed, 770 insertions(+), 605 deletions(-)
 rename solr/{core/src/java/org/apache/solr/handler/component => solrj/src/java/org/apache/solr/client/solrj/routing}/AffinityReplicaListTransformer.java (86%)
 rename solr/{core/src/java/org/apache/solr/handler/component => solrj/src/java/org/apache/solr/client/solrj/routing}/AffinityReplicaListTransformerFactory.java (88%)
 create mode 100644 solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
 rename solr/solrj/src/java/org/apache/solr/client/solrj/{impl => routing}/PreferenceRule.java (97%)
 rename solr/{core/src/java/org/apache/solr/handler/component => solrj/src/java/org/apache/solr/client/solrj/routing}/ReplicaListTransformer.java (96%)
 rename solr/{core/src/java/org/apache/solr/handler/component => solrj/src/java/org/apache/solr/client/solrj/routing}/ReplicaListTransformerFactory.java (80%)
 create mode 100644 solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
 copy solr/{core/src/java/org/apache/solr/handler/component => solrj/src/java/org/apache/solr/client/solrj/routing}/ShufflingReplicaListTransformer.java (89%)
 copy solr/{core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java => solrj/src/java/org/apache/solr/client/solrj/routing/package-info.java} (65%)
 rename solr/{core/src/java/org/apache/solr => solrj/src/java/org/apache/solr/common}/cloud/NodesSysPropsCacher.java (93%)
 rename solr/{core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java => solrj/src/java/org/apache/solr/common/util/CommonTestInjection.java} (56%)
 create mode 100644 solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
 rename solr/{core/src/test/org/apache/solr/handler/component => solrj/src/test/org/apache/solr/client/solrj/routing}/ReplicaListTransformerTest.java (97%)
 create mode 100644 solr/solrj/src/test/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGeneratorTest.java
 rename solr/{core/src/test/org/apache/solr/handler/component => solrj/src/test/org/apache/solr/client/solrj/routing}/ShufflingReplicaListTransformerTest.java (98%)


[lucene-solr] 01/02: SOLR-13865: Migrate replica routing code to SolrJ (#974)

Posted by tf...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tflobbe pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit bf2627969940a9173ada83336898af1c5f95bfa5
Author: Houston Putman <hp...@bloomberg.net>
AuthorDate: Mon Oct 28 18:49:21 2019 -0400

    SOLR-13865: Migrate replica routing code to SolrJ (#974)
    
    * [SOLR-13865] Migrate replica routing code to  solrJ
    
    * Added a CommonTestInjection class.
    
    * Fixing imports.
    
    * Reverted extraneous streaming changes.
    
    * Fix precommit errors.
    
    * Changing name of the RLTManager.
    
    * Splitting up existing tests.
    
    * Updated documentation.
    
    * Added solr/CHANGES.txt entry
---
 solr/CHANGES.txt                                   |   2 +
 .../java/org/apache/solr/cloud/ZkController.java   |   1 +
 .../apache/solr/handler/admin/MetricsHandler.java  |   4 +-
 .../solr/handler/component/HttpShardHandler.java   |   1 +
 .../handler/component/HttpShardHandlerFactory.java | 357 +++------------------
 .../java/org/apache/solr/util/TestInjection.java   |   8 -
 .../cloud/RoutingToNodesWithPropertiesTest.java    |   9 +-
 .../component/TestHttpShardHandlerFactory.java     | 211 ------------
 .../routing}/AffinityReplicaListTransformer.java   |  14 +-
 .../AffinityReplicaListTransformerFactory.java     |  18 +-
 .../routing/NodePreferenceRulesComparator.java     | 179 +++++++++++
 .../solrj/{impl => routing}/PreferenceRule.java    |   2 +-
 .../solrj/routing}/ReplicaListTransformer.java     |   2 +-
 .../routing}/ReplicaListTransformerFactory.java    |   8 +-
 .../RequestReplicaListTransformerGenerator.java    | 168 ++++++++++
 .../routing}/ShufflingReplicaListTransformer.java  |   4 +-
 .../solr/common}/cloud/NodesSysPropsCacher.java    |  18 +-
 .../solr/common/util/CommonTestInjection.java}     |  31 +-
 .../routing/NodePreferenceRulesComparatorTest.java | 155 +++++++++
 .../solrj/routing}/ReplicaListTransformerTest.java |   3 +-
 ...RequestReplicaListTransformerGeneratorTest.java | 152 +++++++++
 .../ShufflingReplicaListTransformerTest.java       |   2 +-
 22 files changed, 765 insertions(+), 584 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 2c6d4d2..76a5bdf 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -43,6 +43,8 @@ Improvements
 
 * SOLR-13831: Support defining arbitrary autoscaling simulation scenarios. (ab)
 
+* SOLR-13865: Move replica routing code to SolrJ. (Houston Putman via Tomas Fernandez-Lobbe)
+
 
 Optimizations
 ---------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkController.java b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
index 8364b11..9d7d9be 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -75,6 +75,7 @@ import org.apache.solr.common.cloud.DefaultZkCredentialsProvider;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.DocCollectionWatcher;
 import org.apache.solr.common.cloud.LiveNodesListener;
+import org.apache.solr.common.cloud.NodesSysPropsCacher;
 import org.apache.solr.common.cloud.OnReconnect;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.Type;
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 f61d799..f3b20ad 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
@@ -38,6 +38,7 @@ import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.Timer;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CommonTestInjection;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
@@ -48,7 +49,6 @@ import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
-import org.apache.solr.util.TestInjection;
 import org.apache.solr.util.stats.MetricUtils;
 
 /**
@@ -70,7 +70,7 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName
 
   private static final Pattern KEY_REGEX = Pattern.compile("(?<!" + Pattern.quote("\\") + ")" + Pattern.quote(":"));
   private CoreContainer cc;
-  private final Map<String, String> injectedSysProps = TestInjection.injectAdditionalProps();
+  private final Map<String, String> injectedSysProps = CommonTestInjection.injectAdditionalProps();
 
   public MetricsHandler() {
     this.metricManager = null;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
index 8cf2fbf..69da394 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
@@ -43,6 +43,7 @@ import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.BinaryResponseParser;
 import org.apache.solr.client.solrj.impl.Http2SolrClient;
 import org.apache.solr.client.solrj.impl.LBSolrClient;
+import org.apache.solr.client.solrj.routing.ReplicaListTransformer;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.cloud.CloudDescriptor;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
index 844acf3..c6c6d4f 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
@@ -16,20 +16,13 @@
  */
 package org.apache.solr.handler.component;
 
-import static org.apache.solr.util.stats.InstrumentedHttpListenerFactory.KNOWN_METRIC_NAME_STRATEGIES;
-
-import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Random;
 import java.util.Set;
@@ -41,6 +34,8 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.client.HttpClient;
 import org.apache.solr.client.solrj.SolrClient;
@@ -51,16 +46,16 @@ import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.client.solrj.impl.HttpSolrClient;
 import org.apache.solr.client.solrj.impl.LBHttp2SolrClient;
 import org.apache.solr.client.solrj.impl.LBSolrClient;
-import org.apache.solr.client.solrj.impl.PreferenceRule;
+import org.apache.solr.client.solrj.routing.AffinityReplicaListTransformerFactory;
+import org.apache.solr.client.solrj.routing.ReplicaListTransformer;
+import org.apache.solr.client.solrj.routing.ReplicaListTransformerFactory;
+import org.apache.solr.client.solrj.routing.RequestReplicaListTransformerGenerator;
 import org.apache.solr.client.solrj.request.QueryRequest;
-import org.apache.solr.cloud.NodesSysPropsCacher;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.ClusterState;
-import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.ExecutorUtil;
@@ -82,10 +77,12 @@ import org.apache.solr.util.stats.MetricUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.apache.solr.util.stats.InstrumentedHttpListenerFactory.KNOWN_METRIC_NAME_STRATEGIES;
+
 public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.apache.solr.util.plugin.PluginInfoInitialized, SolrMetricProducer {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   private static final String DEFAULT_SCHEME = "http";
-  
+
   // We want an executor that doesn't take up any resources if
   // it's not used, so it could be created statically for
   // the distributed search component if desired.
@@ -122,7 +119,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
 
   protected final Random r = new Random();
 
-  private final ReplicaListTransformer shufflingReplicaListTransformer = new ShufflingReplicaListTransformer(r);
+  private RequestReplicaListTransformerGenerator requestReplicaListTransformerGenerator = new RequestReplicaListTransformerGenerator();
 
   // URL scheme to be used in distributed search.
   static final String INIT_URL_SCHEME = "urlScheme";
@@ -222,6 +219,8 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
 
   private void initReplicaListTransformers(NamedList routingConfig) {
     String defaultRouting = null;
+    ReplicaListTransformerFactory stableRltFactory = null;
+    ReplicaListTransformerFactory defaultRltFactory;
     if (routingConfig != null && routingConfig.size() > 0) {
       Iterator<Entry<String,?>> iter = routingConfig.iterator();
       do {
@@ -238,21 +237,22 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
           case ShardParams.REPLICA_STABLE:
             NamedList<?> c = getNamedList(e.getValue());
             defaultRouting = checkDefaultReplicaListTransformer(c, key, defaultRouting);
-            this.stableRltFactory = new AffinityReplicaListTransformerFactory(c);
+            stableRltFactory = new AffinityReplicaListTransformerFactory(c);
             break;
           default:
             throw new IllegalArgumentException("invalid replica routing spec name: " + key);
         }
       } while (iter.hasNext());
     }
-    if (this.stableRltFactory == null) {
-      this.stableRltFactory = new AffinityReplicaListTransformerFactory();
+    if (stableRltFactory == null) {
+      stableRltFactory = new AffinityReplicaListTransformerFactory();
     }
     if (ShardParams.REPLICA_STABLE.equals(defaultRouting)) {
-      this.defaultRltFactory = this.stableRltFactory;
+      defaultRltFactory = stableRltFactory;
     } else {
-      this.defaultRltFactory = this.randomRltFactory;
+      defaultRltFactory = RequestReplicaListTransformerGenerator.RANDOM_RLTF;
     }
+    this.requestReplicaListTransformerGenerator = new RequestReplicaListTransformerGenerator(defaultRltFactory, stableRltFactory);
   }
 
   @Override
@@ -267,7 +267,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
     String strategy = getParameter(args, "metricNameStrategy", UpdateShardHandlerConfig.DEFAULT_METRICNAMESTRATEGY, sb);
     this.metricNameStrategy = KNOWN_METRIC_NAME_STRATEGIES.get(strategy);
     if (this.metricNameStrategy == null)  {
-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+      throw new SolrException(ErrorCode.SERVER_ERROR,
           "Unknown metricNameStrategy: " + strategy + " found. Must be one of: " + KNOWN_METRIC_NAME_STRATEGIES.keySet());
     }
 
@@ -288,7 +288,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
     this.accessPolicy = getParameter(args, INIT_FAIRNESS_POLICY, accessPolicy,sb);
     this.whitelistHostChecker = new WhitelistHostChecker(args == null? null: (String) args.get(INIT_SHARDS_WHITELIST), !getDisableShardsWhitelist());
     log.info("Host whitelist initialized: {}", this.whitelistHostChecker);
-    
+
     // magic sysprop to make tests reproducible: set by SolrTestCaseJ4.
     String v = System.getProperty("tests.shardhandler.randomSeed");
     if (v != null) {
@@ -352,7 +352,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
         if (loadbalancer != null) {
           loadbalancer.close();
         }
-      } finally { 
+      } finally {
         if (defaultClient != null) {
           IOUtils.closeQuietly(defaultClient);
         }
@@ -397,280 +397,23 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
     return urls;
   }
 
-  /**
-   * A distributed request is made via {@link LBSolrClient} to the first live server in the URL list.
-   * This means it is just as likely to choose current host as any of the other hosts.
-   * This function makes sure that the cores are sorted according to the given list of preferences.
-   * E.g. If all nodes prefer local cores then a bad/heavily-loaded node will receive less requests from 
-   * healthy nodes. This will help prevent a distributed deadlock or timeouts in all the healthy nodes due 
-   * to one bad node.
-   * 
-   * Optional final preferenceRule is *not* used for pairwise sorting, but instead defines how "equivalent"
-   * replicas will be ordered (the base ordering). Defaults to "random"; may specify "stable".
-   */
-  static class NodePreferenceRulesComparator implements Comparator<Object> {
-
-    private final SolrQueryRequest request;
-    private final NodesSysPropsCacher sysPropsCache;
-    private final String nodeName;
-    private final List<PreferenceRule> sortRules;
-    private final List<PreferenceRule> preferenceRules;
-    private String localHostAddress = null;
-    private final ReplicaListTransformer baseReplicaListTransformer;
-
-    public NodePreferenceRulesComparator(final List<PreferenceRule> preferenceRules, final SolrQueryRequest request,
-        final ReplicaListTransformerFactory defaultRltFactory, final ReplicaListTransformerFactory randomRltFactory,
-        final ReplicaListTransformerFactory stableRltFactory) {
-      this.request = request;
-      final SolrCore core; // explicit check for null core (temporary?, for tests)
-      if (request != null && (core = request.getCore()) != null && core.getCoreContainer().getZkController() != null) {
-        ZkController zkController = request.getCore().getCoreContainer().getZkController();
-        sysPropsCache = zkController.getSysPropsCacher();
-        nodeName = zkController.getNodeName();
-      } else {
-        sysPropsCache = null;
-        nodeName = null;
-      }
-      this.preferenceRules = preferenceRules;
-      final int maxIdx = preferenceRules.size() - 1;
-      final PreferenceRule lastRule = preferenceRules.get(maxIdx);
-      if (!ShardParams.SHARDS_PREFERENCE_REPLICA_BASE.equals(lastRule.name)) {
-        this.sortRules = preferenceRules;
-        this.baseReplicaListTransformer = defaultRltFactory.getInstance(null, request, randomRltFactory);
-      } else {
-        if (maxIdx == 0) {
-          this.sortRules = null;
-        } else {
-          this.sortRules = preferenceRules.subList(0, maxIdx);
-        }
-        String[] parts = lastRule.value.split(":", 2);
-        switch (parts[0]) {
-          case ShardParams.REPLICA_RANDOM:
-            this.baseReplicaListTransformer = randomRltFactory.getInstance(parts.length == 1 ? null : parts[1], request, null);
-            break;
-          case ShardParams.REPLICA_STABLE:
-            this.baseReplicaListTransformer = stableRltFactory.getInstance(parts.length == 1 ? null : parts[1], request, randomRltFactory);
-            break;
-          default:
-            throw new IllegalArgumentException("Invalid base replica order spec");
-        }
-      }
-    }
-    private static final ReplicaListTransformer NOOP_RLT = (List<?> choices) -> { /* noop */ };
-    private static final ReplicaListTransformerFactory NOOP_RLTF = (String configSpec, SolrQueryRequest request,
-        ReplicaListTransformerFactory fallback) -> NOOP_RLT;
-    /**
-     * For compatibility with tests, which expect this constructor to have no effect on the *base* order.
-     */
-    NodePreferenceRulesComparator(final List<PreferenceRule> sortRules, final SolrQueryRequest request) {
-      this(sortRules, request, NOOP_RLTF, null, null);
-    }
-
-    public ReplicaListTransformer getBaseReplicaListTransformer() {
-      return baseReplicaListTransformer;
-    }
-
-    @Override
-    public int compare(Object left, Object right) {
-      if (this.sortRules != null) {
-        for (PreferenceRule preferenceRule: this.sortRules) {
-          final boolean lhs;
-          final boolean rhs;
-          switch (preferenceRule.name) {
-            case ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE:
-              lhs = hasReplicaType(left, preferenceRule.value);
-              rhs = hasReplicaType(right, preferenceRule.value);
-              break;
-            case ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION:
-              lhs = hasCoreUrlPrefix(left, preferenceRule.value);
-              rhs = hasCoreUrlPrefix(right, preferenceRule.value);
-              break;
-            case ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP:
-              if (sysPropsCache == null) {
-                throw new IllegalArgumentException("Unable to get the NodesSysPropsCacher" +
-                    " on sorting replicas by preference:"+ preferenceRule.value);
-              }
-              lhs = hasSameMetric(left, preferenceRule.value);
-              rhs = hasSameMetric(right, preferenceRule.value);
-              break;
-            case ShardParams.SHARDS_PREFERENCE_REPLICA_BASE:
-              throw new IllegalArgumentException("only one base replica order may be specified in "
-                  + ShardParams.SHARDS_PREFERENCE + ", and it must be specified last");
-            default:
-              throw new IllegalArgumentException("Invalid " + ShardParams.SHARDS_PREFERENCE + " type: " + preferenceRule.name);
-          }
-          if (lhs != rhs) {
-            return lhs ? -1 : +1;
-          }
-        }
-      }
-      return 0;
-    }
-
-    private boolean hasSameMetric(Object o, String metricTag) {
-      if (!(o instanceof Replica)) {
-        return false;
-      }
-
-      Collection<String> tags = Collections.singletonList(metricTag);
-      String otherNodeName = ((Replica) o).getNodeName();
-      Map<String, Object> currentNodeMetric = sysPropsCache.getSysProps(nodeName, tags);
-      Map<String, Object> otherNodeMetric = sysPropsCache.getSysProps(otherNodeName, tags);
-      return currentNodeMetric.equals(otherNodeMetric);
-    }
-
-    private boolean hasCoreUrlPrefix(Object o, String prefix) {
-      final String s;
-      if (o instanceof String) {
-        s = (String)o;
-      }
-      else if (o instanceof Replica) {
-        s = ((Replica)o).getCoreUrl();
-      } else {
-        return false;
-      }
-      if (prefix.equals(ShardParams.REPLICA_LOCAL)) {
-        if (null == localHostAddress) {
-          final ZkController zkController = this.request.getCore().getCoreContainer().getZkController();
-          localHostAddress = zkController != null ? zkController.getBaseUrl() : "";
-          if (localHostAddress.isEmpty()) {
-            log.warn("Couldn't determine current host address for sorting of local replicas");
-          }
-        }
-        if (!localHostAddress.isEmpty()) {
-          if (s.startsWith(localHostAddress)) {
-            return true;
-          }
-        }
-      } else {
-        if (s.startsWith(prefix)) {
-          return true;
-        }
-      }
-      return false;
-    }
-    private static boolean hasReplicaType(Object o, String preferred) {
-      if (!(o instanceof Replica)) {
-        return false;
-      }
-      final String s = ((Replica)o).getType().toString();
-      return s.equals(preferred);
-    }
-  }
-
-  private final ReplicaListTransformerFactory randomRltFactory = (String configSpec, SolrQueryRequest request,
-      ReplicaListTransformerFactory fallback) -> shufflingReplicaListTransformer;
-  private ReplicaListTransformerFactory stableRltFactory;
-  private ReplicaListTransformerFactory defaultRltFactory;
-
-  /**
-   * Private class responsible for applying pairwise sort based on inherent replica attributes,
-   * and subsequently reordering any equivalent replica sets according to behavior specified
-   * by the baseReplicaListTransformer.
-   */
-  private static final class TopLevelReplicaListTransformer implements ReplicaListTransformer {
-
-    private final NodePreferenceRulesComparator replicaComp;
-    private final ReplicaListTransformer baseReplicaListTransformer;
-
-    public TopLevelReplicaListTransformer(NodePreferenceRulesComparator replicaComp, ReplicaListTransformer baseReplicaListTransformer) {
-      this.replicaComp = replicaComp;
-      this.baseReplicaListTransformer = baseReplicaListTransformer;
-    }
-
-    @Override
-    public void transform(List<?> choices) {
-      if (choices.size() > 1) {
-        if (log.isDebugEnabled()) {
-          log.debug("Applying the following sorting preferences to replicas: {}",
-              Arrays.toString(replicaComp.preferenceRules.toArray()));
-        }
-
-        // First, sort according to comparator rules.
-        try {
-          choices.sort(replicaComp);
-        } catch (IllegalArgumentException iae) {
-          throw new SolrException(
-              SolrException.ErrorCode.BAD_REQUEST,
-              iae.getMessage()
-          );
-        }
-
-        // Next determine all boundaries between replicas ranked as "equivalent" by the comparator
-        Iterator<?> iter = choices.iterator();
-        Object prev = iter.next();
-        Object current;
-        int idx = 1;
-        int boundaryCount = 0;
-        int[] boundaries = new int[choices.size() - 1];
-        do {
-          current = iter.next();
-          if (replicaComp.compare(prev, current) != 0) {
-            boundaries[boundaryCount++] = idx;
-          }
-          prev = current;
-          idx++;
-        } while (iter.hasNext());
-
-        // Finally inspect boundaries to apply base transformation, where necessary (separate phase to avoid ConcurrentModificationException)
-        int startIdx = 0;
-        int endIdx;
-        for (int i = 0; i < boundaryCount; i++) {
-          endIdx = boundaries[i];
-          if (endIdx - startIdx > 1) {
-            baseReplicaListTransformer.transform(choices.subList(startIdx, endIdx));
-          }
-          startIdx = endIdx;
-        }
-
-        if (log.isDebugEnabled()) {
-          log.debug("Applied sorting preferences to replica list: {}",
-              Arrays.toString(choices.toArray()));
-        }
-      }
-    }
-  }
-
   protected ReplicaListTransformer getReplicaListTransformer(final SolrQueryRequest req) {
     final SolrParams params = req.getParams();
     final SolrCore core = req.getCore(); // explicit check for null core (temporary?, for tests)
     ZkController zkController = core == null ? null : core.getCoreContainer().getZkController();
-    String defaultShardPreference = "";
     if (zkController != null) {
-      defaultShardPreference = zkController.getZkStateReader().getClusterProperties()
-          .getOrDefault(ZkStateReader.DEFAULT_SHARD_PREFERENCES, "")
-          .toString();
-    }
-
-
-    @SuppressWarnings("deprecation")
-    final boolean preferLocalShards = params.getBool(CommonParams.PREFER_LOCAL_SHARDS, false);
-    final String shardsPreferenceSpec = params.get(ShardParams.SHARDS_PREFERENCE, defaultShardPreference);
-
-    if (preferLocalShards || !shardsPreferenceSpec.isEmpty()) {
-      if (preferLocalShards && !shardsPreferenceSpec.isEmpty()) {
-        throw new SolrException(
-          SolrException.ErrorCode.BAD_REQUEST,
-          "preferLocalShards is deprecated and must not be used with shards.preference" 
-        );
-      }
-      List<PreferenceRule> preferenceRules = PreferenceRule.from(shardsPreferenceSpec);
-      if (preferLocalShards) {
-        preferenceRules.add(new PreferenceRule(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION, ShardParams.REPLICA_LOCAL));
-      }
-
-      NodePreferenceRulesComparator replicaComp = new NodePreferenceRulesComparator(preferenceRules, req,
-          defaultRltFactory, randomRltFactory, stableRltFactory);
-      ReplicaListTransformer baseReplicaListTransformer = replicaComp.getBaseReplicaListTransformer();
-      if (replicaComp.sortRules == null) {
-        // only applying base transformation
-        return baseReplicaListTransformer;
-      } else {
-        return new TopLevelReplicaListTransformer(replicaComp, baseReplicaListTransformer);
-      }
+      return requestReplicaListTransformerGenerator.getReplicaListTransformer(
+          params,
+          zkController.getZkStateReader().getClusterProperties()
+              .getOrDefault(ZkStateReader.DEFAULT_SHARD_PREFERENCES, "")
+              .toString(),
+          zkController.getNodeName(),
+          zkController.getBaseUrl(),
+          zkController.getSysPropsCacher()
+      );
+    } else {
+      return requestReplicaListTransformerGenerator.getReplicaListTransformer(params);
     }
-
-    return defaultRltFactory.getInstance(null, req, randomRltFactory);
   }
 
   /**
@@ -679,7 +422,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
   public CompletionService newCompletionService() {
     return new ExecutorCompletionService<ShardResponse>(commExecutor);
   }
-  
+
   /**
    * Rebuilds the URL replacing the URL scheme of the passed URL with the
    * configured scheme replacement.If no scheme was configured, the passed URL's
@@ -691,7 +434,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
     } else if(StringUtils.isNotEmpty(scheme)) {
       return scheme + "://" + URLUtil.removeScheme(url);
     }
-    
+
     return url;
   }
 
@@ -703,28 +446,28 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
         manager.registry(registry),
         SolrMetricManager.mkName("httpShardExecutor", expandedScope, "threadPool"));
   }
-  
+
   /**
    * Class used to validate the hosts in the "shards" parameter when doing a distributed
    * request
    */
   public static class WhitelistHostChecker {
-    
+
     /**
      * List of the whitelisted hosts. Elements in the list will be host:port (no protocol or context)
      */
     private final Set<String> whitelistHosts;
-    
+
     /**
-     * Indicates whether host checking is enabled 
+     * Indicates whether host checking is enabled
      */
     private final boolean whitelistHostCheckingEnabled;
-    
+
     public WhitelistHostChecker(String whitelistStr, boolean enabled) {
       this.whitelistHosts = implGetShardsWhitelist(whitelistStr);
       this.whitelistHostCheckingEnabled = enabled;
     }
-    
+
     final static Set<String> implGetShardsWhitelist(final String shardsWhitelist) {
       if (shardsWhitelist != null && !shardsWhitelist.isEmpty()) {
         return StrUtils.splitSmart(shardsWhitelist, ',')
@@ -740,32 +483,32 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
                   url = new URL(hostUrl);
                 }
               } catch (MalformedURLException e) {
-                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid URL syntax in \"" + INIT_SHARDS_WHITELIST + "\": " + shardsWhitelist, e);
+                throw new SolrException(ErrorCode.SERVER_ERROR, "Invalid URL syntax in \"" + INIT_SHARDS_WHITELIST + "\": " + shardsWhitelist, e);
               }
               if (url.getHost() == null || url.getPort() < 0) {
-                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid URL syntax in \"" + INIT_SHARDS_WHITELIST + "\": " + shardsWhitelist);
+                throw new SolrException(ErrorCode.SERVER_ERROR, "Invalid URL syntax in \"" + INIT_SHARDS_WHITELIST + "\": " + shardsWhitelist);
               }
               return url.getHost() + ":" + url.getPort();
             }).collect(Collectors.toSet());
       }
       return null;
     }
-    
-    
+
+
     /**
      * @see #checkWhitelist(ClusterState, String, List)
      */
     protected void checkWhitelist(String shardsParamValue, List<String> shardUrls) {
       checkWhitelist(null, shardsParamValue, shardUrls);
     }
-    
+
     /**
      * Checks that all the hosts for all the shards requested in shards parameter exist in the configured whitelist
      * or in the ClusterState (in case of cloud mode)
-     * 
+     *
      * @param clusterState The up to date ClusterState, can be null in case of non-cloud mode
      * @param shardsParamValue The original shards parameter
-     * @param shardUrls The list of cores generated from the shards parameter. 
+     * @param shardUrls The list of cores generated from the shards parameter.
      */
     protected void checkWhitelist(ClusterState clusterState, String shardsParamValue, List<String> shardUrls) {
       if (!whitelistHostCheckingEnabled) {
@@ -780,7 +523,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
       } else {
         localWhitelistHosts = Collections.emptySet();
       }
-      
+
       shardUrls.stream().map(String::trim).forEach((shardUrl) -> {
         URL url;
         try {
@@ -791,10 +534,10 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
             url = new URL(shardUrl);
           }
         } catch (MalformedURLException e) {
-          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid URL syntax in \"shards\" parameter: " + shardsParamValue, e);
+          throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid URL syntax in \"shards\" parameter: " + shardsParamValue, e);
         }
         if (url.getHost() == null || url.getPort() < 0) {
-          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid URL syntax in \"shards\" parameter: " + shardsParamValue);
+          throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid URL syntax in \"shards\" parameter: " + shardsParamValue);
         }
         if (!localWhitelistHosts.contains(url.getHost() + ":" + url.getPort())) {
           log.warn("The '"+ShardParams.SHARDS+"' parameter value '"+shardsParamValue+"' contained value(s) not on the shards whitelist ("+localWhitelistHosts+"), shardUrl:" + shardUrl);
diff --git a/solr/core/src/java/org/apache/solr/util/TestInjection.java b/solr/core/src/java/org/apache/solr/util/TestInjection.java
index ef140d0..39c849b 100644
--- a/solr/core/src/java/org/apache/solr/util/TestInjection.java
+++ b/solr/core/src/java/org/apache/solr/util/TestInjection.java
@@ -20,7 +20,6 @@ import java.lang.invoke.MethodHandles;
 import java.lang.reflect.Method;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import java.util.Timer;
@@ -147,8 +146,6 @@ public class TestInjection {
 
   public volatile static boolean uifOutOfMemoryError = false;
 
-  public volatile static Map<String, String> additionalSystemProps = null;
-
   private volatile static CountDownLatch notifyPauseForeverDone = new CountDownLatch(1);
   
   public static void notifyPauseForeverDone() {
@@ -157,7 +154,6 @@ public class TestInjection {
   }
 
   public static void reset() {
-    additionalSystemProps = null;
     nonGracefullClose = null;
     failReplicaRequests = null;
     failUpdateRequests = null;
@@ -494,10 +490,6 @@ public class TestInjection {
     return true;
   }
 
-  public static Map<String,String> injectAdditionalProps() {
-    return additionalSystemProps;
-  }
-
   public static boolean injectUIFOutOfMemoryError() {
     if (uifOutOfMemoryError ) {
       throw new OutOfMemoryError("Test Injection");
diff --git a/solr/core/src/test/org/apache/solr/cloud/RoutingToNodesWithPropertiesTest.java b/solr/core/src/test/org/apache/solr/cloud/RoutingToNodesWithPropertiesTest.java
index 95503ae..14150d0 100644
--- a/solr/core/src/test/org/apache/solr/cloud/RoutingToNodesWithPropertiesTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/RoutingToNodesWithPropertiesTest.java
@@ -41,6 +41,7 @@ import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.CommonTestInjection;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.TimeSource;
@@ -67,14 +68,14 @@ public class RoutingToNodesWithPropertiesTest extends SolrCloudTestCase {
 
   @Before
   public void setupCluster() throws Exception {
-    TestInjection.additionalSystemProps = ImmutableMap.of("zone", "us-west1");
+    CommonTestInjection.setAdditionalProps(ImmutableMap.of("zone", "us-west1"));
     configureCluster(2)
         .withSolrXml(TEST_PATH().resolve("solr-trackingshardhandler.xml"))
         .addConfig("config", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
         .configure();
 
     zone1Nodes.addAll(cluster.getJettySolrRunners().stream().map(JettySolrRunner::getNodeName).collect(Collectors.toSet()));
-    TestInjection.additionalSystemProps = ImmutableMap.of("zone", "us-west2");
+    CommonTestInjection.setAdditionalProps(ImmutableMap.of("zone", "us-west2"));
     zone2Nodes.add(cluster.startJettySolrRunner().getNodeName());
     zone2Nodes.add(cluster.startJettySolrRunner().getNodeName());
 
@@ -114,9 +115,9 @@ public class RoutingToNodesWithPropertiesTest extends SolrCloudTestCase {
       assertEquals("us-west1", map.get(PROP_NAME));
     }
 
-    for (String zone1Node: zone2Nodes) {
+    for (String zone2Node: zone2Nodes) {
       NodeStateProvider nodeStateProvider = cloudManager.getNodeStateProvider();
-      Map<String, Object> map = nodeStateProvider.getNodeValues(zone1Node, Collections.singletonList(PROP_NAME));
+      Map<String, Object> map = nodeStateProvider.getNodeValues(zone2Node, Collections.singletonList(PROP_NAME));
       assertEquals("us-west2", map.get(PROP_NAME));
     }
 
diff --git a/solr/core/src/test/org/apache/solr/handler/component/TestHttpShardHandlerFactory.java b/solr/core/src/test/org/apache/solr/handler/component/TestHttpShardHandlerFactory.java
index f995a7f..f486dbd 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/TestHttpShardHandlerFactory.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/TestHttpShardHandlerFactory.java
@@ -20,7 +20,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -28,19 +27,11 @@ import java.util.Set;
 
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.impl.LBSolrClient;
-import org.apache.solr.client.solrj.impl.PreferenceRule;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
-import org.apache.solr.common.cloud.Replica;
-import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.ShardParams;
-import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.PluginInfo;
-import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.component.HttpShardHandlerFactory.WhitelistHostChecker;
-import org.apache.solr.request.SolrQueryRequestBase;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -121,208 +112,6 @@ public class TestHttpShardHandlerFactory extends SolrTestCaseJ4 {
     }
   }
 
-  @SuppressWarnings("unchecked")
-  public void testNodePreferenceRulesBase() throws Exception {
-    SolrCore testCore = null;
-    HttpShardHandlerFactory fac = new HttpShardHandlerFactory();
-    fac.init(new PluginInfo(null, Collections.EMPTY_MAP));
-    SolrQueryRequestBase req;
-    NamedList<String> params = new NamedList<>();
-    List<Replica> replicas = getBasicReplicaList();
-
-    String rulesParam = ShardParams.SHARDS_PREFERENCE_REPLICA_BASE + ":stable:dividend:routingPreference";
-
-    params.add("routingPreference", "0");
-    params.add(ShardParams.SHARDS_PREFERENCE, rulesParam);
-
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    ReplicaListTransformer rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node2", replicas.get(1).getNodeName());
-    assertEquals("node3", replicas.get(2).getNodeName());
-    req.close();
-
-    params.setVal(0, "1");
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node2", replicas.get(0).getNodeName());
-    assertEquals("node3", replicas.get(1).getNodeName());
-    assertEquals("node1", replicas.get(2).getNodeName());
-    req.close();
-
-    params.setVal(0, "2");
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node3", replicas.get(0).getNodeName());
-    assertEquals("node1", replicas.get(1).getNodeName());
-    assertEquals("node2", replicas.get(2).getNodeName());
-    req.close();
-
-    params.setVal(0, "3");
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node2", replicas.get(1).getNodeName());
-    assertEquals("node3", replicas.get(2).getNodeName());
-    req.close();
-
-    // Add a replica so that sorting by replicaType:TLOG can cause a tie
-    replicas.add(
-      new Replica(
-        "node4",
-        map(
-          ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
-          ZkStateReader.NODE_NAME_PROP, "node4",
-          ZkStateReader.CORE_NAME_PROP, "collection1",
-          ZkStateReader.REPLICA_TYPE, "TLOG"
-        )
-      )
-    );
-
-    // replicaType and replicaBase combined rule param
-    rulesParam = ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," + 
-      ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," + 
-      ShardParams.SHARDS_PREFERENCE_REPLICA_BASE + ":stable:dividend:routingPreference";
-
-    params.setVal(0, "0");
-    params.setVal(1, rulesParam);
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node2", replicas.get(1).getNodeName());
-    assertEquals("node4", replicas.get(2).getNodeName());
-    assertEquals("node3", replicas.get(3).getNodeName());
-    req.close();
-
-    params.setVal(0, "1");
-    req = new SolrQueryRequestBase(testCore, params.toSolrParams()) {};
-    rlt = fac.getReplicaListTransformer(req);
-    rlt.transform(replicas);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node4", replicas.get(1).getNodeName());
-    assertEquals("node2", replicas.get(2).getNodeName());
-    assertEquals("node3", replicas.get(3).getNodeName());
-    req.close();
-    fac.close();
-  }
-
-  @SuppressWarnings("unchecked")
-  private static List<Replica> getBasicReplicaList() {
-    List<Replica> replicas = new ArrayList<Replica>();
-    replicas.add(
-      new Replica(
-        "node1",
-        map(
-          ZkStateReader.BASE_URL_PROP, "http://host1:8983/solr",
-          ZkStateReader.NODE_NAME_PROP, "node1",
-          ZkStateReader.CORE_NAME_PROP, "collection1",
-          ZkStateReader.REPLICA_TYPE, "NRT"
-        )
-      )
-    );
-    replicas.add(
-      new Replica(
-        "node2",
-        map(
-          ZkStateReader.BASE_URL_PROP, "http://host2:8983/solr",
-          ZkStateReader.NODE_NAME_PROP, "node2",
-          ZkStateReader.CORE_NAME_PROP, "collection1",
-          ZkStateReader.REPLICA_TYPE, "TLOG"
-        )
-      )
-    );
-    replicas.add(
-      new Replica(
-        "node3",
-        map(
-          ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
-          ZkStateReader.NODE_NAME_PROP, "node3",
-          ZkStateReader.CORE_NAME_PROP, "collection1",
-          ZkStateReader.REPLICA_TYPE, "PULL"
-        )
-      )
-    );
-    return replicas;
-  }
-
-  @SuppressWarnings("unchecked")
-  public void testNodePreferenceRulesComparator() throws Exception {
-    List<Replica> replicas = getBasicReplicaList();
-
-    // Simple replica type rule
-    List<PreferenceRule> rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," +
-        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG");
-    HttpShardHandlerFactory.NodePreferenceRulesComparator comparator = 
-      new HttpShardHandlerFactory.NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node2", replicas.get(1).getNodeName());
-
-    // Another simple replica type rule
-    rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," +
-        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT");
-    comparator = new HttpShardHandlerFactory.NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
-    assertEquals("node2", replicas.get(0).getNodeName());
-    assertEquals("node1", replicas.get(1).getNodeName());
-
-    // replicaLocation rule
-    rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":http://host2:8983");
-    comparator = new HttpShardHandlerFactory.NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
-    assertEquals("node2", replicas.get(0).getNodeName());
-    assertEquals("node1", replicas.get(1).getNodeName());
-
-    // Add a replica so that sorting by replicaType:TLOG can cause a tie
-    replicas.add(
-      new Replica(
-        "node4",
-        map(
-          ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
-          ZkStateReader.NODE_NAME_PROP, "node4",
-          ZkStateReader.CORE_NAME_PROP, "collection1",
-          ZkStateReader.REPLICA_TYPE, "TLOG"
-        )
-      )
-    );
-
-    // replicaType and replicaLocation combined rule
-    rules = PreferenceRule.from(
-      ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," + 
-      ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," + 
-      ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":http://host2_2");
-    comparator = new HttpShardHandlerFactory.NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
-    assertEquals("node1", replicas.get(0).getNodeName());
-    assertEquals("node4", replicas.get(1).getNodeName());
-    assertEquals("node2", replicas.get(2).getNodeName());
-    assertEquals("node3", replicas.get(3).getNodeName());
-
-    // Bad rule
-
-    try {
-      rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE);
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertEquals("Invalid shards.preference rule: " + ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE, e.getMessage());
-    }
-
-    // Unknown rule
-    rules = PreferenceRule.from("badRule:test");
-    try {
-      comparator = new HttpShardHandlerFactory.NodePreferenceRulesComparator(rules, null);
-      replicas.sort(comparator);
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertEquals("Invalid shards.preference type: badRule", e.getMessage());
-    }
-  }
-
   @Test
   public void getShardsWhitelist() throws Exception {
     System.setProperty(SHARDS_WHITELIST, "http://abc:8983/,http://def:8984/,");
diff --git a/solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformer.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformer.java
similarity index 86%
rename from solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformer.java
rename to solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformer.java
index 420c111..f6dbf61 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformer.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.Arrays;
 import java.util.Comparator;
@@ -24,14 +24,13 @@ import java.util.ListIterator;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.Hash;
-import org.apache.solr.request.SolrQueryRequest;
 
 /**
  * Allows better caching by establishing deterministic evenly-distributed replica routing preferences according to
  * either explicitly configured hash routing parameter, or the hash of a query parameter (configurable, usually related
  * to the main query).
  */
-class AffinityReplicaListTransformer implements ReplicaListTransformer {
+public class AffinityReplicaListTransformer implements ReplicaListTransformer {
 
   private final int routingDividend;
 
@@ -47,17 +46,16 @@ class AffinityReplicaListTransformer implements ReplicaListTransformer {
    *
    * @param dividendParam int param to be used directly for mod-based routing
    * @param hashParam String param to be hashed into an int for mod-based routing
-   * @param req the request from which param values will be drawn
+   * @param requestParams the parameters of the Solr request
    * @return null if specified routing vals are not able to be parsed properly
    */
-  public static ReplicaListTransformer getInstance(String dividendParam, String hashParam, SolrQueryRequest req) {
-    SolrParams params = req.getOriginalParams();
+  public static ReplicaListTransformer getInstance(String dividendParam, String hashParam, SolrParams requestParams) {
     Integer dividendVal;
-    if (dividendParam != null && (dividendVal = params.getInt(dividendParam)) != null) {
+    if (dividendParam != null && (dividendVal = requestParams.getInt(dividendParam)) != null) {
       return new AffinityReplicaListTransformer(dividendVal);
     }
     String hashVal;
-    if (hashParam != null && (hashVal = params.get(hashParam)) != null && !hashVal.isEmpty()) {
+    if (hashParam != null && (hashVal = requestParams.get(hashParam)) != null && !hashVal.isEmpty()) {
       return new AffinityReplicaListTransformer(hashVal);
     } else {
       return null;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformerFactory.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformerFactory.java
similarity index 88%
rename from solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformerFactory.java
rename to solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformerFactory.java
index 1e97c25..4cfa659 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/AffinityReplicaListTransformerFactory.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/AffinityReplicaListTransformerFactory.java
@@ -14,12 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.request.SolrQueryRequest;
 
 /**
  * Factory for constructing an {@link AffinityReplicaListTransformer} that reorders replica routing
@@ -27,12 +27,12 @@ import org.apache.solr.request.SolrQueryRequest;
  *
  * Default names of params that contain the values by which routing is determined may be configured
  * at the time of {@link AffinityReplicaListTransformerFactory} construction, and may be
- * overridden by the config spec passed to {@link #getInstance(String, SolrQueryRequest, ReplicaListTransformerFactory)}
+ * overridden by the config spec passed to {@link #getInstance(String, SolrParams, ReplicaListTransformerFactory)}
  *
  * If no defaultHashParam name is specified at time of factory construction, the routing dividend will
  * be derived by hashing the {@link String} value of the {@link CommonParams#Q} param.
  */
-class AffinityReplicaListTransformerFactory implements ReplicaListTransformerFactory {
+public class AffinityReplicaListTransformerFactory implements ReplicaListTransformerFactory {
   private final String defaultDividendParam;
   private final String defaultHashParam;
 
@@ -68,24 +68,24 @@ class AffinityReplicaListTransformerFactory implements ReplicaListTransformerFac
   }
 
   @Override
-  public ReplicaListTransformer getInstance(String configSpec, SolrQueryRequest request, ReplicaListTransformerFactory fallback) {
+  public ReplicaListTransformer getInstance(String configSpec, SolrParams requestParams, ReplicaListTransformerFactory fallback) {
     ReplicaListTransformer rlt;
     if (configSpec == null) {
-      rlt = AffinityReplicaListTransformer.getInstance(defaultDividendParam, defaultHashParam, request);
+      rlt = AffinityReplicaListTransformer.getInstance(defaultDividendParam, defaultHashParam, requestParams);
     } else {
       String[] parts = configSpec.split(":", 2);
       switch (parts[0]) {
         case ShardParams.ROUTING_DIVIDEND:
-          rlt = AffinityReplicaListTransformer.getInstance(parts.length == 1 ? defaultDividendParam : parts[1], defaultHashParam, request);
+          rlt = AffinityReplicaListTransformer.getInstance(parts.length == 1 ? defaultDividendParam : parts[1], defaultHashParam, requestParams);
           break;
         case ShardParams.ROUTING_HASH:
-          rlt = AffinityReplicaListTransformer.getInstance(null, parts.length == 1 ? defaultHashParam : parts[1], request);
+          rlt = AffinityReplicaListTransformer.getInstance(null, parts.length == 1 ? defaultHashParam : parts[1], requestParams);
           break;
         default:
           throw new IllegalArgumentException("Invalid routing spec: \"" + configSpec + '"');
       }
     }
-    return rlt != null ? rlt : fallback.getInstance(null, request, null);
+    return rlt != null ? rlt : fallback.getInstance(null, requestParams, null);
   }
 
 }
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
new file mode 100644
index 0000000..4fdab0f
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.client.solrj.routing;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.cloud.NodesSysPropsCacher;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+
+/**
+   * This comparator makes sure that the given replicas are sorted according to the given list of preferences.
+   * E.g. If all nodes prefer local cores then a bad/heavily-loaded node will receive less requests from
+   * healthy nodes. This will help prevent a distributed deadlock or timeouts in all the healthy nodes due
+   * to one bad node.
+   *
+   * Optional final preferenceRule is *not* used for pairwise sorting, but instead defines how "equivalent"
+   * replicas will be ordered (the base ordering). Defaults to "random"; may specify "stable".
+   */
+public class NodePreferenceRulesComparator implements Comparator<Object> {
+
+  private final NodesSysPropsCacher sysPropsCache;
+  private final String nodeName;
+  private final List<PreferenceRule> sortRules;
+  private final List<PreferenceRule> preferenceRules;
+  private final String localHostAddress;
+  private final ReplicaListTransformer baseReplicaListTransformer;
+
+  public NodePreferenceRulesComparator(final List<PreferenceRule> preferenceRules, final SolrParams requestParams,
+  final ReplicaListTransformerFactory defaultRltFactory, final ReplicaListTransformerFactory stableRltFactory) {
+    this(preferenceRules, requestParams, null, null, null, defaultRltFactory, stableRltFactory);
+  }
+
+  public NodePreferenceRulesComparator(final List<PreferenceRule> preferenceRules, final SolrParams requestParams,
+      final String nodeName, final String localHostAddress, final NodesSysPropsCacher sysPropsCache,
+      final ReplicaListTransformerFactory defaultRltFactory, final ReplicaListTransformerFactory stableRltFactory) {
+    this.sysPropsCache = sysPropsCache;
+    this.preferenceRules = preferenceRules;
+    this.nodeName = nodeName;
+    this.localHostAddress = localHostAddress;
+    final int maxIdx = preferenceRules.size() - 1;
+    final PreferenceRule lastRule = preferenceRules.get(maxIdx);
+    if (!ShardParams.SHARDS_PREFERENCE_REPLICA_BASE.equals(lastRule.name)) {
+      this.sortRules = preferenceRules;
+      this.baseReplicaListTransformer = defaultRltFactory.getInstance(null, requestParams, RequestReplicaListTransformerGenerator.RANDOM_RLTF);
+    } else {
+      if (maxIdx == 0) {
+        this.sortRules = null;
+      } else {
+        this.sortRules = preferenceRules.subList(0, maxIdx);
+      }
+      String[] parts = lastRule.value.split(":", 2);
+      switch (parts[0]) {
+        case ShardParams.REPLICA_RANDOM:
+          this.baseReplicaListTransformer = RequestReplicaListTransformerGenerator.RANDOM_RLTF.getInstance(parts.length == 1 ? null : parts[1], requestParams, null);
+          break;
+        case ShardParams.REPLICA_STABLE:
+          this.baseReplicaListTransformer = stableRltFactory.getInstance(parts.length == 1 ? null : parts[1], requestParams, RequestReplicaListTransformerGenerator.RANDOM_RLTF);
+          break;
+        default:
+          throw new IllegalArgumentException("Invalid base replica order spec");
+      }
+    }
+  }
+  private static final ReplicaListTransformer NOOP_RLT = (List<?> choices) -> { /* noop */ };
+  private static final ReplicaListTransformerFactory NOOP_RLTF =
+      (String configSpec, SolrParams requestParams, ReplicaListTransformerFactory fallback) -> NOOP_RLT;
+  /**
+   * For compatibility with tests, which expect this constructor to have no effect on the *base* order.
+   */
+  NodePreferenceRulesComparator(final List<PreferenceRule> sortRules, final SolrParams requestParams) {
+    this(sortRules, requestParams, NOOP_RLTF, null);
+  }
+
+  public ReplicaListTransformer getBaseReplicaListTransformer() {
+    return baseReplicaListTransformer;
+  }
+
+  @Override
+  public int compare(Object left, Object right) {
+    if (this.sortRules != null) {
+      for (PreferenceRule preferenceRule: this.sortRules) {
+        final boolean lhs;
+        final boolean rhs;
+        switch (preferenceRule.name) {
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE:
+            lhs = hasReplicaType(left, preferenceRule.value);
+            rhs = hasReplicaType(right, preferenceRule.value);
+            break;
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION:
+            lhs = hasCoreUrlPrefix(left, preferenceRule.value);
+            rhs = hasCoreUrlPrefix(right, preferenceRule.value);
+            break;
+          case ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP:
+            if (sysPropsCache == null) {
+              throw new IllegalArgumentException("Unable to get the NodesSysPropsCacher on sorting replicas by preference:"+ preferenceRule.value);
+            }
+            lhs = hasSameMetric(left, preferenceRule.value);
+            rhs = hasSameMetric(right, preferenceRule.value);
+            break;
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_BASE:
+            throw new IllegalArgumentException("only one base replica order may be specified in "
+                + ShardParams.SHARDS_PREFERENCE + ", and it must be specified last");
+          default:
+            throw new IllegalArgumentException("Invalid " + ShardParams.SHARDS_PREFERENCE + " type: " + preferenceRule.name);
+        }
+        if (lhs != rhs) {
+          return lhs ? -1 : +1;
+        }
+      }
+    }
+    return 0;
+  }
+
+  private boolean hasSameMetric(Object o, String metricTag) {
+    if (!(o instanceof Replica)) {
+      return false;
+    }
+
+    Collection<String> tags = Collections.singletonList(metricTag);
+    String otherNodeName = ((Replica) o).getNodeName();
+    Map<String, Object> currentNodeMetric = sysPropsCache.getSysProps(nodeName, tags);
+    Map<String, Object> otherNodeMetric = sysPropsCache.getSysProps(otherNodeName, tags);
+    return currentNodeMetric.equals(otherNodeMetric);
+  }
+
+  private boolean hasCoreUrlPrefix(Object o, String prefix) {
+    final String s;
+    if (o instanceof String) {
+      s = (String)o;
+    }
+    else if (o instanceof Replica) {
+      s = ((Replica)o).getCoreUrl();
+    } else {
+      return false;
+    }
+    if (prefix.equals(ShardParams.REPLICA_LOCAL)) {
+      return !StringUtils.isEmpty(localHostAddress) && s.startsWith(localHostAddress);
+    } else {
+      return s.startsWith(prefix);
+    }
+  }
+  private static boolean hasReplicaType(Object o, String preferred) {
+    if (!(o instanceof Replica)) {
+      return false;
+    }
+    final String s = ((Replica)o).getType().toString();
+    return s.equals(preferred);
+  }
+
+  public List<PreferenceRule> getSortRules() {
+    return sortRules;
+  }
+
+  public List<PreferenceRule> getPreferenceRules() {
+    return preferenceRules;
+  }
+}
\ No newline at end of file
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreferenceRule.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
similarity index 97%
rename from solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreferenceRule.java
rename to solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
index 6ada81d..91ea962 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreferenceRule.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.solr.client.solrj.impl;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformer.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformer.java
similarity index 96%
rename from solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformer.java
rename to solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformer.java
index b7784e8..e2344c6 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformer.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.List;
 
diff --git a/solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformerFactory.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformerFactory.java
similarity index 80%
rename from solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformerFactory.java
rename to solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformerFactory.java
index af09a69..3f640ab 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ReplicaListTransformerFactory.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ReplicaListTransformerFactory.java
@@ -14,21 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
-import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.common.params.SolrParams;
 
 public interface ReplicaListTransformerFactory {
 
   /**
    * 
    * @param configSpec spec for dynamic configuration of ReplicaListTransformer
-   * @param request the request for which the ReplicaListTransformer is being generated
+   * @param requestParams the request parameters for which the ReplicaListTransformer is being generated
    * @param fallback used to generate fallback value; the getInstance() method of the specified fallback must not
    * return null; The fallback value itself may be null if this implementation is known to never return null (i.e., if
    * fallback will never be needed)
    * @return ReplicaListTransformer to be used for routing this request
    */
-  ReplicaListTransformer getInstance(String configSpec, SolrQueryRequest request, ReplicaListTransformerFactory fallback);
+  ReplicaListTransformer getInstance(String configSpec, SolrParams requestParams, ReplicaListTransformerFactory fallback);
 
 }
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
new file mode 100644
index 0000000..58c8b2e
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.client.solrj.routing;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.cloud.NodesSysPropsCacher;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RequestReplicaListTransformerGenerator {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final Random r = new Random();
+
+  private static final ReplicaListTransformer shufflingReplicaListTransformer = new ShufflingReplicaListTransformer(r);
+  public static final ReplicaListTransformerFactory RANDOM_RLTF =
+      (String configSpec, SolrParams requestParams, ReplicaListTransformerFactory fallback) -> shufflingReplicaListTransformer;
+  private final ReplicaListTransformerFactory stableRltFactory;
+  private final ReplicaListTransformerFactory defaultRltFactory;
+
+  public RequestReplicaListTransformerGenerator() {
+    this(RANDOM_RLTF);
+  }
+
+  public RequestReplicaListTransformerGenerator(ReplicaListTransformerFactory defaultRltFactory) {
+    this(defaultRltFactory, null);
+  }
+
+  public RequestReplicaListTransformerGenerator(ReplicaListTransformerFactory defaultRltFactory, ReplicaListTransformerFactory stableRltFactory) {
+    this.defaultRltFactory = defaultRltFactory;
+    if (stableRltFactory == null) {
+      this.stableRltFactory = new AffinityReplicaListTransformerFactory();
+    } else {
+      this.stableRltFactory = stableRltFactory;
+    }
+  }
+
+  public ReplicaListTransformer getReplicaListTransformer(final SolrParams requestParams) {
+    return getReplicaListTransformer(requestParams, "");
+  }
+
+  public ReplicaListTransformer getReplicaListTransformer(final SolrParams requestParams, String defaultShardPreferences) {
+    return getReplicaListTransformer(requestParams, defaultShardPreferences, null, null, null);
+  }
+
+  public ReplicaListTransformer getReplicaListTransformer(final SolrParams requestParams, String defaultShardPreferences, String nodeName, String localHostAddress, NodesSysPropsCacher sysPropsCacher) {
+    @SuppressWarnings("deprecation")
+    final boolean preferLocalShards = requestParams.getBool(CommonParams.PREFER_LOCAL_SHARDS, false);
+    final String shardsPreferenceSpec = requestParams.get(ShardParams.SHARDS_PREFERENCE, defaultShardPreferences);
+
+    if (preferLocalShards || !shardsPreferenceSpec.isEmpty()) {
+      if (preferLocalShards && !shardsPreferenceSpec.isEmpty()) {
+        throw new SolrException(
+            ErrorCode.BAD_REQUEST,
+            "preferLocalShards is deprecated and must not be used with shards.preference"
+        );
+      }
+      List<PreferenceRule> preferenceRules = PreferenceRule.from(shardsPreferenceSpec);
+      if (preferLocalShards) {
+        preferenceRules.add(new PreferenceRule(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION, ShardParams.REPLICA_LOCAL));
+      }
+
+      NodePreferenceRulesComparator replicaComp = new NodePreferenceRulesComparator(preferenceRules, requestParams, nodeName, localHostAddress, sysPropsCacher, defaultRltFactory, stableRltFactory);
+      ReplicaListTransformer baseReplicaListTransformer = replicaComp.getBaseReplicaListTransformer();
+      if (replicaComp.getSortRules() == null) {
+        // only applying base transformation
+        return baseReplicaListTransformer;
+      } else {
+        return new TopLevelReplicaListTransformer(replicaComp, baseReplicaListTransformer);
+      }
+    }
+
+    return defaultRltFactory.getInstance(null, requestParams, RANDOM_RLTF);
+  }
+
+  /**
+   * Private class responsible for applying pairwise sort based on inherent replica attributes,
+   * and subsequently reordering any equivalent replica sets according to behavior specified
+   * by the baseReplicaListTransformer.
+   */
+  private static final class TopLevelReplicaListTransformer implements ReplicaListTransformer {
+
+    private final NodePreferenceRulesComparator replicaComp;
+    private final ReplicaListTransformer baseReplicaListTransformer;
+
+    public TopLevelReplicaListTransformer(NodePreferenceRulesComparator replicaComp, ReplicaListTransformer baseReplicaListTransformer) {
+      this.replicaComp = replicaComp;
+      this.baseReplicaListTransformer = baseReplicaListTransformer;
+    }
+
+    @Override
+    public void transform(List<?> choices) {
+      if (choices.size() > 1) {
+        if (log.isDebugEnabled()) {
+          log.debug("Applying the following sorting preferences to replicas: {}",
+              Arrays.toString(replicaComp.getPreferenceRules().toArray()));
+        }
+
+        // First, sort according to comparator rules.
+        try {
+          choices.sort(replicaComp);
+        } catch (IllegalArgumentException iae) {
+          throw new SolrException(
+              ErrorCode.BAD_REQUEST,
+              iae.getMessage()
+          );
+        }
+
+        // Next determine all boundaries between replicas ranked as "equivalent" by the comparator
+        Iterator<?> iter = choices.iterator();
+        Object prev = iter.next();
+        Object current;
+        int idx = 1;
+        int boundaryCount = 0;
+        int[] boundaries = new int[choices.size() - 1];
+        do {
+          current = iter.next();
+          if (replicaComp.compare(prev, current) != 0) {
+            boundaries[boundaryCount++] = idx;
+          }
+          prev = current;
+          idx++;
+        } while (iter.hasNext());
+
+        // Finally inspect boundaries to apply base transformation, where necessary (separate phase to avoid ConcurrentModificationException)
+        int startIdx = 0;
+        int endIdx;
+        for (int i = 0; i < boundaryCount; i++) {
+          endIdx = boundaries[i];
+          if (endIdx - startIdx > 1) {
+            baseReplicaListTransformer.transform(choices.subList(startIdx, endIdx));
+          }
+          startIdx = endIdx;
+        }
+
+        if (log.isDebugEnabled()) {
+          log.debug("Applied sorting preferences to replica list: {}",
+              Arrays.toString(choices.toArray()));
+        }
+      }
+    }
+  }
+  
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformer.java
similarity index 89%
copy from solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java
copy to solr/solrj/src/java/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformer.java
index 428e348..7c37129 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformer.java
@@ -14,13 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Random;
 
-class ShufflingReplicaListTransformer implements ReplicaListTransformer {
+public class ShufflingReplicaListTransformer implements ReplicaListTransformer {
 
   private final Random r;
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/NodesSysPropsCacher.java b/solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java
similarity index 93%
rename from solr/core/src/java/org/apache/solr/cloud/NodesSysPropsCacher.java
rename to solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java
index acd765d..186a32c 100644
--- a/solr/core/src/java/org/apache/solr/cloud/NodesSysPropsCacher.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.solr.cloud;
+package org.apache.solr.common.cloud;
 
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -29,13 +29,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
-import com.google.common.annotations.VisibleForTesting;
 import org.apache.solr.client.solrj.cloud.NodeStateProvider;
-import org.apache.solr.client.solrj.impl.PreferenceRule;
+import org.apache.solr.client.solrj.routing.PreferenceRule;
 import org.apache.solr.common.SolrCloseable;
-import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.ShardParams;
-import org.apache.solr.util.TestInjection;
+import org.apache.solr.common.util.CommonTestInjection;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,7 +53,7 @@ public class NodesSysPropsCacher implements SolrCloseable {
 
   private final AtomicBoolean isRunning = new AtomicBoolean(false);
   private final NodeStateProvider nodeStateProvider;
-  private final Map<String, String> additionalProps = TestInjection.injectAdditionalProps();
+  private Map<String, String> additionalProps = CommonTestInjection.injectAdditionalProps();
   private final String currentNode;
   private final ConcurrentHashMap<String, Map<String, Object>> cache = new ConcurrentHashMap<>();
   private final AtomicInteger fetchCounting = new AtomicInteger(0);
@@ -63,9 +61,9 @@ public class NodesSysPropsCacher implements SolrCloseable {
   private volatile boolean isClosed;
   private volatile Collection<String> tags = new ArrayList<>();
 
-  NodesSysPropsCacher(NodeStateProvider nodeStateProvider,
-                      String currentNode,
-                      ZkStateReader stateReader) {
+  public NodesSysPropsCacher(NodeStateProvider nodeStateProvider,
+                             String currentNode,
+                             ZkStateReader stateReader) {
     this.nodeStateProvider = nodeStateProvider;
     this.currentNode = currentNode;
 
@@ -177,12 +175,10 @@ public class NodesSysPropsCacher implements SolrCloseable {
     return result;
   }
 
-  @VisibleForTesting
   public int getCacheSize() {
     return cache.size();
   }
 
-  @VisibleForTesting
   public boolean isRunning() {
     return isRunning.get();
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java b/solr/solrj/src/java/org/apache/solr/common/util/CommonTestInjection.java
similarity index 56%
rename from solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java
rename to solr/solrj/src/java/org/apache/solr/common/util/CommonTestInjection.java
index 428e348..eed7175 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ShufflingReplicaListTransformer.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/CommonTestInjection.java
@@ -14,26 +14,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Random;
+package org.apache.solr.common.util;
 
-class ShufflingReplicaListTransformer implements ReplicaListTransformer {
+import java.util.Map;
 
-  private final Random r;
+/**
+ * Allows random faults to be injected in running code during test runs across all solr packages.
+ *
+ * @lucene.internal
+ */
+public class CommonTestInjection {
 
-  public ShufflingReplicaListTransformer(Random r)
-  {
-    this.r = r;
+  private volatile static Map<String, String> additionalSystemProps = null;
+
+  public static void reset() {
+    additionalSystemProps = null;
   }
 
-  public void transform(List<?> choices)
-  {
-    if (choices.size() > 1) {
-      Collections.shuffle(choices, r);
-    }
+  public static void setAdditionalProps(Map<String, String> additionalSystemProps) {
+    CommonTestInjection.additionalSystemProps = additionalSystemProps;
   }
 
+  public static Map<String,String> injectAdditionalProps() {
+    return additionalSystemProps;
+  }
 }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
new file mode 100644
index 0000000..245947a
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.client.solrj.routing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.ShardParams;
+import org.junit.Test;
+
+public class NodePreferenceRulesComparatorTest extends SolrTestCaseJ4 {
+
+  @Test
+  public void replicaLocationTest() {
+    List<Replica> replicas = getBasicReplicaList();
+
+    // replicaLocation rule
+    List<PreferenceRule> rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":http://host2:8983");
+    NodePreferenceRulesComparator comparator = new NodePreferenceRulesComparator(rules, null);
+    replicas.sort(comparator);
+    assertEquals("node2", replicas.get(0).getNodeName());
+    assertEquals("node1", replicas.get(1).getNodeName());
+
+  }
+
+  public void replicaTypeTest() {
+    List<Replica> replicas = getBasicReplicaList();
+
+    List<PreferenceRule> rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," +
+        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG");
+    NodePreferenceRulesComparator comparator = new NodePreferenceRulesComparator(rules, null);
+
+    replicas.sort(comparator);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node2", replicas.get(1).getNodeName());
+
+    // reversed rule
+    rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," +
+        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT");
+    comparator = new NodePreferenceRulesComparator(rules, null);
+
+    replicas.sort(comparator);
+    assertEquals("node2", replicas.get(0).getNodeName());
+    assertEquals("node1", replicas.get(1).getNodeName());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Test
+  public void replicaTypeAndReplicaLocationTest() {
+    List<Replica> replicas = getBasicReplicaList();
+    // Add a replica so that sorting by replicaType:TLOG can cause a tie
+    replicas.add(
+        new Replica(
+            "node4",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node4",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "TLOG"
+            )
+        )
+    );
+
+    List<PreferenceRule> rules = PreferenceRule.from(
+        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," +
+            ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," +
+            ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":http://host2_2");
+    NodePreferenceRulesComparator comparator = new NodePreferenceRulesComparator(rules, null);
+
+    replicas.sort(comparator);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node4", replicas.get(1).getNodeName());
+    assertEquals("node2", replicas.get(2).getNodeName());
+    assertEquals("node3", replicas.get(3).getNodeName());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void badRuleTest() {
+    try {
+      PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE);
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid shards.preference rule: " + ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE, e.getMessage());
+      throw e;
+    }
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void unknownRuleTest() {
+    List<Replica> replicas = getBasicReplicaList();
+    List<PreferenceRule> rules = PreferenceRule.from("badRule:test");
+    try {
+      replicas.sort(new NodePreferenceRulesComparator(rules, null));
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid shards.preference type: badRule", e.getMessage());
+      throw e;
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<Replica> getBasicReplicaList() {
+    List<Replica> replicas = new ArrayList<Replica>();
+    replicas.add(
+        new Replica(
+            "node1",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host1:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node1",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "NRT"
+            )
+        )
+    );
+    replicas.add(
+        new Replica(
+            "node2",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node2",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "TLOG"
+            )
+        )
+    );
+    replicas.add(
+        new Replica(
+            "node3",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node3",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "PULL"
+            )
+        )
+    );
+    return replicas;
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/component/ReplicaListTransformerTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/ReplicaListTransformerTest.java
similarity index 97%
rename from solr/core/src/test/org/apache/solr/handler/component/ReplicaListTransformerTest.java
rename to solr/solrj/src/test/org/apache/solr/client/solrj/routing/ReplicaListTransformerTest.java
index da14071..7fcd04a 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/ReplicaListTransformerTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/ReplicaListTransformerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -28,6 +28,7 @@ import org.apache.solr.SolrTestCase;
 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.handler.component.HttpShardHandlerFactory;
 import org.apache.solr.request.LocalSolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequest;
 import org.junit.Test;
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGeneratorTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGeneratorTest.java
new file mode 100644
index 0000000..f8f2e68
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGeneratorTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.client.solrj.routing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.ShardParams;
+import org.junit.Test;
+
+public class RequestReplicaListTransformerGeneratorTest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testNodePreferenceRulesBase() {
+    RequestReplicaListTransformerGenerator generator = new RequestReplicaListTransformerGenerator();
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    List<Replica> replicas = getBasicReplicaList();
+
+    String rulesParam = ShardParams.SHARDS_PREFERENCE_REPLICA_BASE + ":stable:dividend:routingPreference";
+
+    params.add("routingPreference", "0");
+    params.add(ShardParams.SHARDS_PREFERENCE, rulesParam);
+
+    ReplicaListTransformer rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node2", replicas.get(1).getNodeName());
+    assertEquals("node3", replicas.get(2).getNodeName());
+
+    params.set("routingPreference", "1");
+    rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node2", replicas.get(0).getNodeName());
+    assertEquals("node3", replicas.get(1).getNodeName());
+    assertEquals("node1", replicas.get(2).getNodeName());
+
+    params.set("routingPreference", "2");
+    rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node3", replicas.get(0).getNodeName());
+    assertEquals("node1", replicas.get(1).getNodeName());
+    assertEquals("node2", replicas.get(2).getNodeName());
+
+    params.set("routingPreference", "3");
+    rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node2", replicas.get(1).getNodeName());
+    assertEquals("node3", replicas.get(2).getNodeName());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Test
+  public void replicaTypeAndReplicaBase() {
+    RequestReplicaListTransformerGenerator generator = new RequestReplicaListTransformerGenerator();
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    List<Replica> replicas = getBasicReplicaList();
+
+    // Add a replica so that sorting by replicaType:TLOG can cause a tie
+    replicas.add(
+        new Replica(
+            "node4",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node4",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "TLOG"
+            )
+        )
+    );
+
+    // replicaType and replicaBase combined rule param
+    String rulesParam = ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":NRT," +
+        ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG," +
+        ShardParams.SHARDS_PREFERENCE_REPLICA_BASE + ":stable:dividend:routingPreference";
+
+    params.add("routingPreference", "0");
+    params.add(ShardParams.SHARDS_PREFERENCE, rulesParam);
+    ReplicaListTransformer rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node2", replicas.get(1).getNodeName());
+    assertEquals("node4", replicas.get(2).getNodeName());
+    assertEquals("node3", replicas.get(3).getNodeName());
+
+    params.set("routingPreference", "1");
+    rlt = generator.getReplicaListTransformer(params);
+    rlt.transform(replicas);
+    assertEquals("node1", replicas.get(0).getNodeName());
+    assertEquals("node4", replicas.get(1).getNodeName());
+    assertEquals("node2", replicas.get(2).getNodeName());
+    assertEquals("node3", replicas.get(3).getNodeName());
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<Replica> getBasicReplicaList() {
+    List<Replica> replicas = new ArrayList<Replica>();
+    replicas.add(
+        new Replica(
+            "node1",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host1:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node1",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "NRT"
+            )
+        )
+    );
+    replicas.add(
+        new Replica(
+            "node2",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node2",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "TLOG"
+            )
+        )
+    );
+    replicas.add(
+        new Replica(
+            "node3",
+            map(
+                ZkStateReader.BASE_URL_PROP, "http://host2_2:8983/solr",
+                ZkStateReader.NODE_NAME_PROP, "node3",
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "PULL"
+            )
+        )
+    );
+    return replicas;
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/component/ShufflingReplicaListTransformerTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformerTest.java
similarity index 98%
rename from solr/core/src/test/org/apache/solr/handler/component/ShufflingReplicaListTransformerTest.java
rename to solr/solrj/src/test/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformerTest.java
index 7fc9514..7e4be97 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/ShufflingReplicaListTransformerTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/ShufflingReplicaListTransformerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.handler.component;
+package org.apache.solr.client.solrj.routing;
 
 import java.util.ArrayList;
 import java.util.Collections;


[lucene-solr] 02/02: SOLR-13865: Add missing package docs

Posted by tf...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tflobbe pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit 220faa5cbfaa135cdd73a2b6f1824b8076e0e654
Author: Tomas Fernandez Lobbe <tf...@apache.org>
AuthorDate: Mon Oct 28 16:25:01 2019 -0700

    SOLR-13865: Add missing package docs
---
 .../solr/client/solrj/routing/package-info.java    | 23 ++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/package-info.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/package-info.java
new file mode 100644
index 0000000..443a274
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+ */
+ 
+/** 
+ * Classes to handle query routing preferences
+ */
+package org.apache.solr.client.solrj.routing;
+
+