You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2023/11/20 04:44:52 UTC

(solr) branch main updated: SOLR-17079: Allow to declare replica placement plugins in solr.xml (#2071)

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

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new aca557a4e68 SOLR-17079: Allow to declare replica placement plugins in solr.xml (#2071)
aca557a4e68 is described below

commit aca557a4e687d4bad475171306e4aa79967990df
Author: Vincent P <vi...@gmail.com>
AuthorDate: Mon Nov 20 05:44:41 2023 +0100

    SOLR-17079: Allow to declare replica placement plugins in solr.xml (#2071)
    
    
    
    Co-authored-by: Vincent Primault <vp...@salesforce.com>
---
 solr/CHANGES.txt                                   |  2 +
 .../apache/solr/api/ContainerPluginsRegistry.java  | 31 +++++---
 .../apache/solr/cloud/api/collections/Assign.java  | 37 ---------
 .../cluster/placement/PlacementPluginFactory.java  |  4 -
 .../impl/DelegatingPlacementPluginFactory.java     | 24 +++++-
 .../impl/PlacementPluginFactoryLoader.java         | 87 +++++++++++++++++++++-
 .../plugins/AffinityPlacementFactory.java          |  2 +-
 .../java/org/apache/solr/core/CoreContainer.java   |  7 +-
 .../src/java/org/apache/solr/core/NodeConfig.java  | 16 +++-
 .../java/org/apache/solr/core/SolrXmlConfig.java   |  2 +
 solr/core/src/test-files/solr/solr-50-all.xml      |  5 ++
 .../impl/PlacementPluginIntegrationTest.java       | 47 ++++++++++++
 .../src/test/org/apache/solr/core/TestSolrXml.java |  8 ++
 .../pages/configuring-solr-xml.adoc                | 15 ++++
 .../pages/replica-placement-plugins.adoc           |  3 +-
 15 files changed, 228 insertions(+), 62 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index fca3e1fecfa..327e4003244 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -82,6 +82,8 @@ New Features
 * SOLR-17006: Collection creation & adding replicas: User-defined properties are persisted to state.json and
   applied to new replicas, available for use as property substitution in configuration files.  (Vincent Primault)
 
+* SOLR-17079: Allow to declare replica placement plugins in solr.xml  (Vincent Primault)
+
 Improvements
 ---------------------
 * SOLR-16924: RESTORECORE now sets the UpdateLog to ACTIVE state instead of requiring a separate
diff --git a/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java b/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java
index 3fea96c2ba8..fbfa6719e8e 100644
--- a/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java
+++ b/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java
@@ -420,16 +420,9 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW
       } else {
         throw new RuntimeException("Must have a no-arg constructor or CoreContainer constructor ");
       }
-      if (instance instanceof ConfigurablePlugin) {
-        Class<? extends MapWriter> c =
-            getConfigClass((ConfigurablePlugin<? extends MapWriter>) instance);
-        if (c != null) {
-          Map<String, Object> original =
-              (Map<String, Object>) holder.original.getOrDefault("config", Collections.emptyMap());
-          holder.meta.config = mapper.readValue(Utils.toJSON(original), c);
-          ((ConfigurablePlugin<MapWriter>) instance).configure(holder.meta.config);
-        }
-      }
+      Map<String, Object> config =
+          (Map<String, Object>) holder.original.getOrDefault("config", Collections.emptyMap());
+      configure(instance, config, holder.meta);
       if (instance instanceof ResourceLoaderAware) {
         try {
           ((ResourceLoaderAware) instance).inform(pkgVersion.getLoader());
@@ -444,6 +437,24 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW
     }
   }
 
+  @SuppressWarnings("unchecked")
+  public static MapWriter configure(Object instance, Map<String, Object> config, PluginMeta meta)
+      throws IOException {
+    if (instance instanceof ConfigurablePlugin) {
+      Class<? extends MapWriter> c =
+          getConfigClass((ConfigurablePlugin<? extends MapWriter>) instance);
+      if (c != null) {
+        MapWriter configObj = mapper.readValue(Utils.toJSON(config), c);
+        if (null != meta) {
+          meta.config = configObj;
+        }
+        ((ConfigurablePlugin<MapWriter>) instance).configure(configObj);
+        return configObj;
+      }
+    }
+    return null;
+  }
+
   /** Get the generic type of a {@link ConfigurablePlugin} */
   @SuppressWarnings("unchecked")
   public static <T extends MapWriter> Class<T> getConfigClass(ConfigurablePlugin<T> o) {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
index 4f5f2482dcf..1d7d3a4087d 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
@@ -41,9 +41,6 @@ import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.cloud.VersionedData;
 import org.apache.solr.cluster.placement.PlacementPlugin;
 import org.apache.solr.cluster.placement.impl.PlacementPluginAssignStrategy;
-import org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory;
-import org.apache.solr.cluster.placement.plugins.MinimizeCoresPlacementFactory;
-import org.apache.solr.cluster.placement.plugins.RandomPlacementFactory;
 import org.apache.solr.cluster.placement.plugins.SimplePlacementFactory;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
@@ -561,40 +558,6 @@ public class Assign {
     // placement plugin)
     PlacementPlugin placementPlugin =
         coreContainer.getPlacementPluginFactory().createPluginInstance();
-    if (placementPlugin == null) {
-      // Otherwise use the default
-      String defaultPluginId = System.getProperty(PLACEMENTPLUGIN_DEFAULT_SYSPROP);
-      if (defaultPluginId != null) {
-        switch (defaultPluginId.toLowerCase(Locale.ROOT)) {
-          case "simple":
-            placementPlugin = (new SimplePlacementFactory()).createPluginInstance();
-            break;
-          case "affinity":
-            placementPlugin = (new AffinityPlacementFactory()).createPluginInstance();
-            break;
-          case "minimizecores":
-            placementPlugin = (new MinimizeCoresPlacementFactory()).createPluginInstance();
-            break;
-          case "random":
-            placementPlugin = (new RandomPlacementFactory()).createPluginInstance();
-            break;
-          default:
-            throw new SolrException(
-                SolrException.ErrorCode.SERVER_ERROR,
-                "Invalid value for system property '"
-                    + PLACEMENTPLUGIN_DEFAULT_SYSPROP
-                    + "'. Supported values are 'simple', 'random', 'affinity' and 'minimizecores'");
-        }
-        log.info(
-            "Default replica placement plugin set in {} to {}",
-            PLACEMENTPLUGIN_DEFAULT_SYSPROP,
-            defaultPluginId);
-      } else {
-        // TODO: Consider making the ootb default AffinityPlacementFactory, see
-        // https://issues.apache.org/jira/browse/SOLR-16492
-        placementPlugin = (new SimplePlacementFactory()).createPluginInstance();
-      }
-    }
     return new PlacementPluginAssignStrategy(placementPlugin);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginFactory.java
index 212c92906dd..da9fac5c3da 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginFactory.java
@@ -18,7 +18,6 @@
 package org.apache.solr.cluster.placement;
 
 import org.apache.solr.api.ConfigurablePlugin;
-import org.apache.solr.cluster.placement.plugins.SimplePlacementFactory;
 
 /**
  * Factory implemented by client code and configured in container plugins (see {@link
@@ -37,9 +36,6 @@ public interface PlacementPluginFactory<T extends PlacementPluginConfig>
    * Returns an instance of the plugin that will be repeatedly (and concurrently) called to compute
    * placement. Multiple instances of a plugin can be used in parallel (for example if configuration
    * has to change, but plugin instances with the previous configuration are still being used).
-   *
-   * <p>If this method returns null then a simple default assignment strategy will be used (see
-   * {@link SimplePlacementFactory}).
    */
   PlacementPlugin createPluginInstance();
 
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/DelegatingPlacementPluginFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/DelegatingPlacementPluginFactory.java
index 2817ded862c..0fbc6ae27a4 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/DelegatingPlacementPluginFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/DelegatingPlacementPluginFactory.java
@@ -24,17 +24,37 @@ import org.apache.solr.cluster.placement.PlacementPluginFactory;
 
 /** Helper class to support dynamic reloading of plugin implementations. */
 public final class DelegatingPlacementPluginFactory
-    implements PlacementPluginFactory<PlacementPluginFactory.NoConfig> {
+    implements PlacementPluginFactory<PlacementPluginConfig> {
   private volatile PlacementPluginFactory<? extends PlacementPluginConfig> delegate;
   // support for tests to make sure the update is completed
   private volatile Phaser phaser;
+  private final PlacementPluginFactory<?> defaultPlacementPluginFactory;
+
+  /**
+   * Constructor.
+   *
+   * @param defaultPlacementPluginFactory A {@link PlacementPluginFactory} to use when no delegate
+   *     is defined.
+   */
+  public DelegatingPlacementPluginFactory(PlacementPluginFactory<?> defaultPlacementPluginFactory) {
+    this.defaultPlacementPluginFactory = defaultPlacementPluginFactory;
+  }
 
   @Override
   public PlacementPlugin createPluginInstance() {
     if (delegate != null) {
       return delegate.createPluginInstance();
     } else {
-      return null;
+      return defaultPlacementPluginFactory.createPluginInstance();
+    }
+  }
+
+  @Override
+  public PlacementPluginConfig getConfig() {
+    if (delegate != null) {
+      return delegate.getConfig();
+    } else {
+      return defaultPlacementPluginFactory.getConfig();
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginFactoryLoader.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginFactoryLoader.java
index 0728c8c221c..45d10012028 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginFactoryLoader.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginFactoryLoader.java
@@ -17,21 +17,38 @@
 
 package org.apache.solr.cluster.placement.impl;
 
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 import org.apache.solr.api.ContainerPluginsRegistry;
 import org.apache.solr.client.solrj.request.beans.PluginMeta;
 import org.apache.solr.cluster.placement.PlacementPluginConfig;
 import org.apache.solr.cluster.placement.PlacementPluginFactory;
+import org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory;
+import org.apache.solr.cluster.placement.plugins.MinimizeCoresPlacementFactory;
+import org.apache.solr.cluster.placement.plugins.RandomPlacementFactory;
+import org.apache.solr.cluster.placement.plugins.SimplePlacementFactory;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.NodeConfig;
+import org.apache.solr.core.PluginInfo;
+import org.apache.solr.core.SolrResourceLoader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/**
- * Utility class to load the configured {@link PlacementPluginFactory} plugin and then keep it up to
- * date as the plugin configuration changes.
- */
+/** Utility class to work with {@link PlacementPluginFactory} plugins. */
 public class PlacementPluginFactoryLoader {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+  @VisibleForTesting
+  static final String PLACEMENTPLUGIN_DEFAULT_SYSPROP = "solr.placementplugin.default";
+
+  /**
+   * Loads the {@link PlacementPluginFactory} configured in cluster plugins and then keep it up to
+   * date as the plugin configuration changes.
+   */
   public static void load(
       DelegatingPlacementPluginFactory pluginFactory, ContainerPluginsRegistry plugins) {
     ContainerPluginsRegistry.ApiInfo pluginFactoryInfo =
@@ -87,4 +104,66 @@ public class PlacementPluginFactoryLoader {
         };
     plugins.registerListener(pluginListener);
   }
+
+  /** Returns the default {@link PlacementPluginFactory} configured in solr.xml. */
+  public static PlacementPluginFactory<?> getDefaultPlacementPluginFactory(
+      NodeConfig nodeConfig, SolrResourceLoader loader) {
+    PluginInfo pluginInfo = nodeConfig.getReplicaPlacementFactoryConfig();
+    if (null != pluginInfo) {
+      return getPlacementPluginFactory(pluginInfo, loader);
+    } else {
+      return getDefaultPlacementPluginFactory();
+    }
+  }
+
+  private static PlacementPluginFactory<?> getPlacementPluginFactory(
+      PluginInfo pluginInfo, SolrResourceLoader loader) {
+    // Load placement plugin factory from solr.xml.
+    PlacementPluginFactory<?> placementPluginFactory =
+        loader.newInstance(pluginInfo, PlacementPluginFactory.class, false);
+    if (null != pluginInfo.initArgs) {
+      Map<String, Object> config = new HashMap<>();
+      pluginInfo.initArgs.toMap(config);
+      try {
+        ContainerPluginsRegistry.configure(placementPluginFactory, config, null);
+      } catch (IOException e) {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR,
+            "Invalid " + pluginInfo.type + " configuration",
+            e);
+      }
+    }
+    return placementPluginFactory;
+  }
+
+  private static PlacementPluginFactory<?> getDefaultPlacementPluginFactory() {
+    // Otherwise use the default provided by system properties.
+    String defaultPluginId = System.getProperty(PLACEMENTPLUGIN_DEFAULT_SYSPROP);
+    if (defaultPluginId != null) {
+      log.info(
+          "Default replica placement plugin set in {} to {}",
+          PLACEMENTPLUGIN_DEFAULT_SYSPROP,
+          defaultPluginId);
+      switch (defaultPluginId.toLowerCase(Locale.ROOT)) {
+        case "simple":
+          return new SimplePlacementFactory();
+        case "affinity":
+          return new AffinityPlacementFactory();
+        case "minimizecores":
+          return new MinimizeCoresPlacementFactory();
+        case "random":
+          return new RandomPlacementFactory();
+        default:
+          throw new SolrException(
+              SolrException.ErrorCode.SERVER_ERROR,
+              "Invalid value for system property '"
+                  + PLACEMENTPLUGIN_DEFAULT_SYSPROP
+                  + "'. Supported values are 'simple', 'random', 'affinity' and 'minimizecores'");
+      }
+    } else {
+      // TODO: Consider making the ootb default AffinityPlacementFactory, see
+      // https://issues.apache.org/jira/browse/SOLR-16492
+      return new SimplePlacementFactory();
+    }
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
index b397cab6722..6c339aa7094 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
@@ -150,7 +150,7 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
    * See {@link AffinityPlacementFactory} for instructions on how to configure a cluster to use this
    * plugin and details on what the plugin does.
    */
-  static class AffinityPlacementPlugin extends OrderedNodePlacementPlugin {
+  public static class AffinityPlacementPlugin extends OrderedNodePlacementPlugin {
 
     private final long minimalFreeDiskGB;
 
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 6e1a417a63d..c8749f4c9e0 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -290,8 +290,7 @@ public class CoreContainer {
           (r) -> this.runAsync(r));
 
   private volatile ClusterEventProducer clusterEventProducer;
-  private final DelegatingPlacementPluginFactory placementPluginFactory =
-      new DelegatingPlacementPluginFactory();
+  private DelegatingPlacementPluginFactory placementPluginFactory;
 
   private FileStoreAPI fileStoreAPI;
   private SolrPackageLoader packageLoader;
@@ -776,6 +775,10 @@ public class CoreContainer {
     ClusterEventProducerFactory clusterEventProducerFactory = new ClusterEventProducerFactory(this);
     clusterEventProducer = clusterEventProducerFactory;
 
+    placementPluginFactory =
+        new DelegatingPlacementPluginFactory(
+            PlacementPluginFactoryLoader.getDefaultPlacementPluginFactory(cfg, loader));
+
     containerPluginsRegistry.registerListener(clusterSingletons.getPluginRegistryListener());
     containerPluginsRegistry.registerListener(
         clusterEventProducerFactory.getPluginRegistryListener());
diff --git a/solr/core/src/java/org/apache/solr/core/NodeConfig.java b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
index 0d83279c1c1..3745a922e4d 100644
--- a/solr/core/src/java/org/apache/solr/core/NodeConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
@@ -77,8 +77,8 @@ public class NodeConfig {
   private final Predicate<String> hiddenSysPropPattern;
 
   private final PluginInfo shardHandlerFactoryConfig;
-
   private final UpdateShardHandlerConfig updateShardHandlerConfig;
+  private final PluginInfo replicaPlacementFactoryConfig;
 
   private final String configSetServiceClass;
 
@@ -128,6 +128,7 @@ public class NodeConfig {
       String sharedLibDirectory,
       PluginInfo shardHandlerFactoryConfig,
       UpdateShardHandlerConfig updateShardHandlerConfig,
+      PluginInfo replicaPlacementFactoryConfig,
       String coreAdminHandlerClass,
       Map<String, String> coreAdminHandlerActions,
       String collectionsAdminHandlerClass,
@@ -165,6 +166,7 @@ public class NodeConfig {
     this.sharedLibDirectory = sharedLibDirectory;
     this.shardHandlerFactoryConfig = shardHandlerFactoryConfig;
     this.updateShardHandlerConfig = updateShardHandlerConfig;
+    this.replicaPlacementFactoryConfig = replicaPlacementFactoryConfig;
     this.coreAdminHandlerClass = coreAdminHandlerClass;
     this.coreAdminHandlerActions = coreAdminHandlerActions;
     this.collectionsAdminHandlerClass = collectionsAdminHandlerClass;
@@ -299,6 +301,10 @@ public class NodeConfig {
     return updateShardHandlerConfig;
   }
 
+  public PluginInfo getReplicaPlacementFactoryConfig() {
+    return replicaPlacementFactoryConfig;
+  }
+
   public int getCoreLoadThreadCount(boolean zkAware) {
     return coreLoadThreads == null
         ? (zkAware
@@ -557,6 +563,7 @@ public class NodeConfig {
     private String hiddenSysProps;
     private PluginInfo shardHandlerFactoryConfig;
     private UpdateShardHandlerConfig updateShardHandlerConfig = UpdateShardHandlerConfig.DEFAULT;
+    private PluginInfo replicaPlacementFactoryConfig;
     private String configSetServiceClass;
     private String coreAdminHandlerClass = DEFAULT_ADMINHANDLERCLASS;
     private Map<String, String> coreAdminHandlerActions = Collections.emptyMap();
@@ -668,6 +675,12 @@ public class NodeConfig {
       return this;
     }
 
+    public NodeConfigBuilder setReplicaPlacementFactoryConfig(
+        PluginInfo replicaPlacementFactoryConfig) {
+      this.replicaPlacementFactoryConfig = replicaPlacementFactoryConfig;
+      return this;
+    }
+
     public NodeConfigBuilder setCoreAdminHandlerClass(String coreAdminHandlerClass) {
       this.coreAdminHandlerClass = coreAdminHandlerClass;
       return this;
@@ -852,6 +865,7 @@ public class NodeConfig {
           sharedLibDirectory,
           shardHandlerFactoryConfig,
           updateShardHandlerConfig,
+          replicaPlacementFactoryConfig,
           coreAdminHandlerClass,
           coreAdminHandlerActions,
           collectionsAdminHandlerClass,
diff --git a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
index 989fa50a77f..f9d5fecd7ec 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
@@ -157,6 +157,8 @@ public class SolrXmlConfig {
     configBuilder.setSolrResourceLoader(loader);
     configBuilder.setUpdateShardHandlerConfig(updateConfig);
     configBuilder.setShardHandlerFactoryConfig(getPluginInfo(root.get("shardHandlerFactory")));
+    configBuilder.setReplicaPlacementFactoryConfig(
+        getPluginInfo(root.get("replicaPlacementFactory")));
     configBuilder.setTracerConfig(getPluginInfo(root.get("tracerConfig")));
     configBuilder.setLogWatcherConfig(loadLogWatcherConfig(root.get("logging")));
     configBuilder.setSolrProperties(loadProperties(root, substituteProperties));
diff --git a/solr/core/src/test-files/solr/solr-50-all.xml b/solr/core/src/test-files/solr/solr-50-all.xml
index 4680027ecd9..c0677d06977 100644
--- a/solr/core/src/test-files/solr/solr-50-all.xml
+++ b/solr/core/src/test-files/solr/solr-50-all.xml
@@ -26,6 +26,7 @@
   <str name="sharedLib">testSharedLib</str>
   <str name="allowPaths">${solr.allowPaths:}</str>
   <str name="shareSchema">${shareSchema:true}</str>
+  <str name="coresLocator">testCoresLocator</str>
   <int name="transientCacheSize">66</int>
   <int name="replayUpdatesThreads">100</int>
   <int name="maxBooleanClauses">42</int>
@@ -66,6 +67,10 @@
     <int name="connTimeout">${connTimeout:110}</int>
   </shardHandlerFactory>
 
+  <replicaPlacementFactory class="org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory">
+    <int name="minimalFreeDiskGB">10</int>
+  </replicaPlacementFactory>
+
   <backup>
     <repository name="local" class="a.b.C" default="true"/>
   </backup>
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
index ecbf3fb5de4..bcac9882e2c 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
@@ -18,6 +18,7 @@
 package org.apache.solr.cluster.placement.impl;
 
 import static java.util.Collections.singletonMap;
+import static org.hamcrest.Matchers.instanceOf;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -29,6 +30,7 @@ import java.util.concurrent.Phaser;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.lucene.tests.util.TestRuleRestoreSystemProperties;
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.V2Request;
@@ -51,13 +53,18 @@ import org.apache.solr.cluster.placement.ShardMetrics;
 import org.apache.solr.cluster.placement.plugins.AffinityPlacementConfig;
 import org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory;
 import org.apache.solr.cluster.placement.plugins.MinimizeCoresPlacementFactory;
+import org.apache.solr.cluster.placement.plugins.RandomPlacementFactory;
+import org.apache.solr.cluster.placement.plugins.SimplePlacementFactory;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.util.LogLevel;
+import org.hamcrest.MatcherAssert;
 import org.junit.After;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 /** Test for {@link MinimizeCoresPlacementFactory} using a {@link MiniSolrCloudCluster}. */
 @LogLevel("org.apache.solr.cluster.placement.impl=DEBUG")
@@ -65,6 +72,11 @@ public class PlacementPluginIntegrationTest extends SolrCloudTestCase {
   private static final String COLLECTION =
       PlacementPluginIntegrationTest.class.getSimpleName() + "_collection";
 
+  @Rule
+  public TestRule sysPropRestore =
+      new TestRuleRestoreSystemProperties(
+          PlacementPluginFactoryLoader.PLACEMENTPLUGIN_DEFAULT_SYSPROP);
+
   private static SolrCloudManager cloudManager;
   private static CoreContainer cc;
 
@@ -93,6 +105,41 @@ public class PlacementPluginIntegrationTest extends SolrCloudTestCase {
     }
   }
 
+  @Test
+  public void testDefaultConfiguration() {
+    CoreContainer cc = createCoreContainer(TEST_PATH(), "<solr></solr>");
+    MatcherAssert.assertThat(
+        cc.getPlacementPluginFactory().createPluginInstance(),
+        instanceOf(SimplePlacementFactory.SimplePlacementPlugin.class));
+  }
+
+  @Test
+  public void testConfigurationInSystemProps() {
+    System.setProperty(PlacementPluginFactoryLoader.PLACEMENTPLUGIN_DEFAULT_SYSPROP, "random");
+    CoreContainer cc = createCoreContainer(TEST_PATH(), "<solr></solr>");
+    MatcherAssert.assertThat(
+        cc.getPlacementPluginFactory().createPluginInstance(),
+        instanceOf(RandomPlacementFactory.RandomPlacementPlugin.class));
+  }
+
+  @Test
+  public void testConfigurationInSolrXml() {
+    String solrXml =
+        "<solr><replicaPlacementFactory class=\"org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory\"><int name=\"minimalFreeDiskGB\">10</int><int name=\"prioritizedFreeDiskGB\">200</int></replicaPlacementFactory></solr>";
+    CoreContainer cc = createCoreContainer(TEST_PATH(), solrXml);
+
+    MatcherAssert.assertThat(
+        cc.getPlacementPluginFactory().createPluginInstance(),
+        instanceOf(AffinityPlacementFactory.AffinityPlacementPlugin.class));
+    MatcherAssert.assertThat(
+        cc.getPlacementPluginFactory().getConfig(), instanceOf(AffinityPlacementConfig.class));
+
+    AffinityPlacementConfig config =
+        (AffinityPlacementConfig) cc.getPlacementPluginFactory().getConfig();
+    assertEquals(config.minimalFreeDiskGB, 10);
+    assertEquals(config.prioritizedFreeDiskGB, 200);
+  }
+
   @Test
   public void testMinimizeCores() throws Exception {
     PluginMeta plugin = new PluginMeta();
diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
index c3761cd7b4b..94a66a4c5c6 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
@@ -78,6 +78,7 @@ public class TestSolrXml extends SolrTestCaseJ4 {
     assertEquals("info handler class", "testInfoHandler", cfg.getInfoHandlerClass());
     assertEquals(
         "config set handler class", "testConfigSetsHandler", cfg.getConfigSetsHandlerClass());
+    assertEquals("cores locator class", "testCoresLocator", cfg.getCoresLocatorClass());
     assertEquals("core load threads", 11, cfg.getCoreLoadThreadCount(false));
     assertEquals("replay update threads", 100, cfg.getReplayUpdatesThreads());
     MatcherAssert.assertThat(
@@ -137,6 +138,13 @@ public class TestSolrXml extends SolrTestCaseJ4 {
                         .collect(Collectors.toSet())));
     assertTrue("hideStackTrace", cfg.hideStackTraces());
     System.clearProperty("solr.allowPaths");
+
+    PluginInfo replicaPlacementFactoryConfig = cfg.getReplicaPlacementFactoryConfig();
+    assertEquals(
+        "org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory",
+        replicaPlacementFactoryConfig.className);
+    assertEquals(1, replicaPlacementFactoryConfig.initArgs.size());
+    assertEquals(10, replicaPlacementFactoryConfig.initArgs.get("minimalFreeDiskGB"));
   }
 
   // Test  a few property substitutions that happen to be in solr-50-all.xml.
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
index bc398b44e34..83c29c670f9 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
@@ -642,6 +642,21 @@ For configuring `stable` routing, the `hash` parameter implicitly defaults to a
 The `dividend` parameter must be configured explicitly; there is no implicit default.
 If only `dividend` routing is desired, `hash` may be explicitly set to the empty string, entirely disabling implicit hash-based routing.
 
+=== The <replicaPlacementFactory> Element
+
+A default xref:replica-placement-plugins.adoc[replica placement plugin] can be defined in `solr.xml`.
+
+[source,xml]
+----
+<replicaPlacementFactory class="org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory">
+  <int name="minimalFreeDiskGB">10</int>
+  <int name="prioritizedFreeDiskGB">200</int>
+</replicaPlacementFactory>
+----
+
+The `class` attribute should be set to the FQN (fully qualified name) of a class that extends `PlacementPluginFactory`.
+Sub-elements are specific to the implementation.
+
 === The <metrics> Element
 
 The `<metrics>` element in `solr.xml` allows you to customize the metrics reported by Solr.
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/replica-placement-plugins.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/replica-placement-plugins.adoc
index 4556574ea51..0ce84efaf68 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/replica-placement-plugins.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/replica-placement-plugins.adoc
@@ -25,7 +25,8 @@ It can also enforce additional constraints on operations such as collection or r
 In earlier versions of Solr, this functionality was provided using either per-collection rules, or with the autoscaling framework.
 
 == Plugin Configuration
-Replica placement plugin configurations are maintained using the `/cluster/plugin` API.
+Replica placement plugin configurations are configured either xref:configuring-solr-xml.adoc#the-replicaplacementfactory-element[ in the `solr.xml` file], or using the `/cluster/plugin` API.
+Any plugin configured in `solr.xml` will be used as long as no replica placement plugin is defined as a cluster plugin.
 There can be only one cluster-wide plugin configuration at a time, and it uses a pre-defined plugin name: `.placement-plugin`.
 
 There are several placement plugins included in the Solr distribution.