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:01 UTC

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

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;