Merge branch 'master' into jira/solr-15016
diff --git a/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java b/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java
index ef13d8a..0d9a183 100644
--- a/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java
+++ b/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java
@@ -25,8 +25,8 @@
*/
public interface ConfigurablePlugin<T extends MapWriter> {
- /**This is invoked soon after the Object is initialized
- *
+ /**
+ * This is invoked soon after the Object is initialized.
* @param cfg value deserialized from JSON
*/
void configure(T cfg);
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 158bcf6..9fa0261 100644
--- a/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java
+++ b/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java
@@ -143,7 +143,7 @@
}
@SuppressWarnings("unchecked")
public synchronized void refresh() {
- Map<String, Object> pluginInfos = null;
+ Map<String, Object> pluginInfos;
try {
pluginInfos = ContainerPluginsApi.plugins(coreContainer.zkClientSupplier);
} catch (IOException e) {
@@ -181,9 +181,8 @@
} else {
//ADDED or UPDATED
PluginMetaHolder info = newState.get(e.getKey());
- ApiInfo apiInfo = null;
List<String> errs = new ArrayList<>();
- apiInfo = new ApiInfo(info,errs);
+ ApiInfo apiInfo = new ApiInfo(info,errs);
if (!errs.isEmpty()) {
log.error(StrUtils.join(errs, ','));
continue;
@@ -239,8 +238,7 @@
@SuppressWarnings({"rawtypes", "unchecked"})
private static Map<String, String> getTemplateVars(PluginMeta pluginMeta) {
- Map result = makeMap("plugin-name", pluginMeta.name, "path-prefix", pluginMeta.pathPrefix);
- return result;
+ return (Map) makeMap("plugin-name", pluginMeta.name, "path-prefix", pluginMeta.pathPrefix);
}
private static class ApiHolder extends Api {
@@ -273,7 +271,7 @@
private final PluginMetaHolder holder;
@JsonProperty
- private PluginMeta info;
+ private final PluginMeta info;
@JsonProperty(value = "package")
public final String pkg;
@@ -392,8 +390,8 @@
}
if (instance instanceof ConfigurablePlugin) {
Class<? extends MapWriter> c = getConfigClass((ConfigurablePlugin<? extends MapWriter>) instance);
- if (c != null) {
- MapWriter initVal = mapper.readValue(Utils.toJSON(holder.original), c);
+ if (c != null && holder.meta.config != null) {
+ MapWriter initVal = mapper.readValue(Utils.toJSON(holder.meta.config), c);
((ConfigurablePlugin) instance).configure(initVal);
}
}
@@ -412,7 +410,8 @@
}
- /**Get the generic type of a {@link ConfigurablePlugin}
+ /**
+ * Get the generic type of a {@link ConfigurablePlugin}
*/
@SuppressWarnings("rawtypes")
public static Class getConfigClass(ConfigurablePlugin<?> o) {
@@ -422,7 +421,10 @@
for (Type type : interfaces) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
- if (parameterizedType.getRawType() == ConfigurablePlugin.class) {
+ Type rawType = parameterizedType.getRawType();
+ if (rawType == ConfigurablePlugin.class ||
+ // or if a super interface is a ConfigurablePlugin
+ ((rawType instanceof Class) && ConfigurablePlugin.class.isAssignableFrom((Class) rawType))) {
return (Class) parameterizedType.getActualTypeArguments()[0];
}
}
@@ -442,10 +444,10 @@
}
public enum Diff {
- ADDED, REMOVED, UNCHANGED, UPDATED;
+ ADDED, REMOVED, UNCHANGED, UPDATED
}
- public static Map<String, Diff> compareMaps(Map<String,? extends Object> a, Map<String,? extends Object> b) {
+ public static Map<String, Diff> compareMaps(Map<String, ?> a, Map<String, ?> b) {
if(a.isEmpty() && b.isEmpty()) return null;
Map<String, Diff> result = new HashMap<>(Math.max(a.size(), b.size()));
a.forEach((k, v) -> {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
index 5f4114a..b24e442 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
@@ -20,12 +20,7 @@
import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.CREATE_NODE_SET;
import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.SKIP_CREATE_REPLICA_IN_CLUSTER_STATE;
-import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
-import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_PROP;
-import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
-import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.*;
import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
@@ -50,6 +45,7 @@
import org.apache.solr.cloud.ActiveReplicaWatcher;
import org.apache.solr.cloud.Overseer;
import org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.ShardRequestTracker;
+import org.apache.solr.cluster.placement.PlacementPlugin;
import org.apache.solr.common.SolrCloseableLatch;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterState;
@@ -144,7 +140,8 @@
}
}
- List<CreateReplica> createReplicas = buildReplicaPositions(ocmh.cloudManager, clusterState, collectionName, message, replicaTypesVsCount)
+ List<CreateReplica> createReplicas = buildReplicaPositions(ocmh.cloudManager, clusterState, collectionName, message, replicaTypesVsCount,
+ ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance())
.stream()
.map(replicaPosition -> assignReplicaDetails(ocmh.cloudManager, clusterState, message, replicaPosition))
.collect(Collectors.toList());
@@ -304,7 +301,8 @@
public static List<ReplicaPosition> buildReplicaPositions(SolrCloudManager cloudManager, ClusterState clusterState,
String collectionName, ZkNodeProps message,
- EnumMap<Replica.Type, Integer> replicaTypeVsCount) throws IOException, InterruptedException {
+ EnumMap<Replica.Type, Integer> replicaTypeVsCount,
+ PlacementPlugin placementPlugin) throws IOException, InterruptedException {
boolean skipCreateReplicaInClusterState = message.getBool(SKIP_CREATE_REPLICA_IN_CLUSTER_STATE, false);
boolean skipNodeAssignment = message.getBool(CollectionAdminParams.SKIP_NODE_ASSIGNMENT, false);
String sliceName = message.getStr(SHARD_ID_PROP);
@@ -328,7 +326,7 @@
if (!skipCreateReplicaInClusterState && !skipNodeAssignment) {
positions = Assign.getNodesForNewReplicas(clusterState, collection.getName(), sliceName, numNrtReplicas,
- numTlogReplicas, numPullReplicas, createNodeSetStr, cloudManager);
+ numTlogReplicas, numPullReplicas, createNodeSetStr, cloudManager, placementPlugin);
}
if (positions == null) {
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 968fb92..786bfa9 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
@@ -42,7 +42,6 @@
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.impl.PlacementPluginConfigImpl;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
@@ -270,7 +269,8 @@
@SuppressWarnings({"unchecked"})
public static List<ReplicaPosition> getNodesForNewReplicas(ClusterState clusterState, String collectionName,
String shard, int nrtReplicas, int tlogReplicas, int pullReplicas,
- Object createNodeSet, SolrCloudManager cloudManager) throws IOException, InterruptedException, AssignmentException {
+ Object createNodeSet, SolrCloudManager cloudManager,
+ PlacementPlugin placementPlugin) throws IOException, InterruptedException, AssignmentException {
log.debug("getNodesForNewReplicas() shard: {} , nrtReplicas : {} , tlogReplicas: {} , pullReplicas: {} , createNodeSet {}"
, shard, nrtReplicas, tlogReplicas, pullReplicas, createNodeSet);
DocCollection coll = clusterState.getCollection(collectionName);
@@ -296,7 +296,7 @@
.assignPullReplicas(pullReplicas)
.onNodes(createNodeList)
.build();
- AssignStrategy assignStrategy = createAssignStrategy(cloudManager, clusterState, coll);
+ AssignStrategy assignStrategy = createAssignStrategy(placementPlugin, clusterState, coll);
return assignStrategy.assign(cloudManager, assignRequest);
}
@@ -492,13 +492,13 @@
/**
* Creates the appropriate instance of {@link AssignStrategy} based on how the cluster and/or individual collections are
* configured.
+ * <p>If {@link PlacementPlugin} instance is null this call will return {@link LegacyAssignStrategy}, otherwise
+ * {@link PlacementPluginAssignStrategy} will be used.</p>
*/
- public static AssignStrategy createAssignStrategy(SolrCloudManager solrCloudManager, ClusterState clusterState, DocCollection collection) {
- PlacementPlugin plugin = PlacementPluginConfigImpl.getPlacementPlugin(solrCloudManager);
-
- if (plugin != null) {
+ public static AssignStrategy createAssignStrategy(PlacementPlugin placementPlugin, ClusterState clusterState, DocCollection collection) {
+ if (placementPlugin != null) {
// If a cluster wide placement plugin is configured (and that's the only way to define a placement plugin)
- return new PlacementPluginAssignStrategy(collection, plugin);
+ return new PlacementPluginAssignStrategy(collection, placementPlugin);
} else {
return new LegacyAssignStrategy();
}
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
index 233dd4b..2e2a06c 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
@@ -41,6 +41,7 @@
import org.apache.solr.cloud.ZkController;
import org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.ShardRequestTracker;
import org.apache.solr.cloud.overseer.ClusterStateMutator;
+import org.apache.solr.cluster.placement.PlacementPlugin;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.Aliases;
@@ -168,7 +169,8 @@
List<ReplicaPosition> replicaPositions = null;
try {
- replicaPositions = buildReplicaPositions(ocmh.cloudManager, clusterState, clusterState.getCollection(collectionName), message, shardNames);
+ replicaPositions = buildReplicaPositions(ocmh.cloudManager, clusterState, clusterState.getCollection(collectionName),
+ message, shardNames, ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance());
} catch (Assign.AssignmentException e) {
ZkNodeProps deleteMessage = new ZkNodeProps("name", collectionName);
new DeleteCollectionCmd(ocmh).call(clusterState, deleteMessage, results);
@@ -286,10 +288,10 @@
}
}
- public static List<ReplicaPosition> buildReplicaPositions(SolrCloudManager cloudManager, ClusterState clusterState,
- DocCollection docCollection,
- ZkNodeProps message,
- List<String> shardNames) throws IOException, InterruptedException, Assign.AssignmentException {
+ private static List<ReplicaPosition> buildReplicaPositions(SolrCloudManager cloudManager, ClusterState clusterState,
+ DocCollection docCollection,
+ ZkNodeProps message,
+ List<String> shardNames, PlacementPlugin placementPlugin) throws IOException, InterruptedException, Assign.AssignmentException {
final String collectionName = message.getStr(NAME);
// look at the replication factor and see if it matches reality
// if it does not, find best nodes to create more cores
@@ -328,7 +330,7 @@
.assignPullReplicas(numPullReplicas)
.onNodes(nodeList)
.build();
- Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(cloudManager, clusterState, docCollection);
+ Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(placementPlugin, clusterState, docCollection);
replicaPositions = assignStrategy.assign(cloudManager, assignRequest);
}
return replicaPositions;
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
index 2267b4d..271677f 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
@@ -120,7 +120,9 @@
.assignPullReplicas(numPullReplicas)
.onNodes(new ArrayList<>(ocmh.cloudManager.getClusterStateProvider().getLiveNodes()))
.build();
- Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(ocmh.cloudManager, clusterState, clusterState.getCollection(sourceCollection));
+ Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(
+ ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance(),
+ clusterState, clusterState.getCollection(sourceCollection));
targetNode = assignStrategy.assign(ocmh.cloudManager, assignRequest).get(0).node;
}
ZkNodeProps msg = sourceReplica.plus("parallel", String.valueOf(parallel)).plus(CoreAdminParams.NODE, targetNode);
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
index db408b4..c7c941a 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
@@ -229,7 +229,9 @@
.assignPullReplicas(numPullReplicas)
.onNodes(nodeList)
.build();
- Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(ocmh.cloudManager, clusterState, restoreCollection);
+ Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(
+ ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance(),
+ clusterState, restoreCollection);
List<ReplicaPosition> replicaPositions = assignStrategy.assign(ocmh.cloudManager, assignRequest);
CountDownLatch countDownLatch = new CountDownLatch(restoreCollection.getSlices().size());
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
index 9641757..770dfac 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
@@ -434,7 +434,9 @@
.assignPullReplicas(numPull.get())
.onNodes(new ArrayList<>(clusterState.getLiveNodes()))
.build();
- Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(ocmh.cloudManager, clusterState, collection);
+ Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(
+ ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance(),
+ clusterState, collection);
List<ReplicaPosition> replicaPositions = assignStrategy.assign(ocmh.cloudManager, assignRequest);
t.stop();
diff --git a/solr/core/src/java/org/apache/solr/cluster/events/ClusterEventProducer.java b/solr/core/src/java/org/apache/solr/cluster/events/ClusterEventProducer.java
index d3b0ee7..aa36fd7 100644
--- a/solr/core/src/java/org/apache/solr/cluster/events/ClusterEventProducer.java
+++ b/solr/core/src/java/org/apache/solr/cluster/events/ClusterEventProducer.java
@@ -26,7 +26,7 @@
public interface ClusterEventProducer extends ClusterSingleton, Closeable {
/** Unique name for the registration of a plugin-based implementation. */
- String PLUGIN_NAME = "cluster-event-producer";
+ String PLUGIN_NAME = ".cluster-event-producer";
@Override
default String getName() {
diff --git a/solr/core/src/java/org/apache/solr/cluster/events/impl/ClusterEventProducerFactory.java b/solr/core/src/java/org/apache/solr/cluster/events/impl/ClusterEventProducerFactory.java
index 17f769b..e79341d 100644
--- a/solr/core/src/java/org/apache/solr/cluster/events/impl/ClusterEventProducerFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/events/impl/ClusterEventProducerFactory.java
@@ -128,13 +128,17 @@
ClusterEventListener listener = (ClusterEventListener) instance;
clusterEventProducer.registerListener(listener);
} else if (instance instanceof ClusterEventProducer) {
- // replace the existing impl
- if (cc.getClusterEventProducer() instanceof DelegatingClusterEventProducer) {
- ((DelegatingClusterEventProducer) cc.getClusterEventProducer())
- .setDelegate((ClusterEventProducer) instance);
+ if (ClusterEventProducer.PLUGIN_NAME.equals(plugin.getInfo().name)) {
+ // replace the existing impl
+ if (cc.getClusterEventProducer() instanceof DelegatingClusterEventProducer) {
+ ((DelegatingClusterEventProducer) cc.getClusterEventProducer())
+ .setDelegate((ClusterEventProducer) instance);
+ } else {
+ log.warn("Can't configure plugin-based ClusterEventProducer while CoreContainer is still loading - " +
+ " using existing implementation {}", cc.getClusterEventProducer().getClass().getName());
+ }
} else {
- log.warn("Can't configure plugin-based ClusterEventProducer while CoreContainer is still loading - " +
- " using existing implementation {}", cc.getClusterEventProducer().getClass().getName());
+ log.warn("Ignoring ClusterEventProducer config with non-standard name: {}", plugin.getInfo());
}
}
}
@@ -149,21 +153,25 @@
ClusterEventListener listener = (ClusterEventListener) instance;
clusterEventProducer.unregisterListener(listener);
} else if (instance instanceof ClusterEventProducer) {
- // replace the existing impl with NoOp
- if (cc.getClusterEventProducer() instanceof DelegatingClusterEventProducer) {
- ((DelegatingClusterEventProducer) cc.getClusterEventProducer())
- .setDelegate(new NoOpProducer(cc));
+ if (ClusterEventProducer.PLUGIN_NAME.equals(plugin.getInfo().name)) {
+ // replace the existing impl with NoOp
+ if (cc.getClusterEventProducer() instanceof DelegatingClusterEventProducer) {
+ ((DelegatingClusterEventProducer) cc.getClusterEventProducer())
+ .setDelegate(new NoOpProducer(cc));
+ } else {
+ log.warn("Can't configure plugin-based ClusterEventProducer while CoreContainer is still loading - " +
+ " using existing implementation {}", cc.getClusterEventProducer().getClass().getName());
+ }
} else {
- log.warn("Can't configure plugin-based ClusterEventProducer while CoreContainer is still loading - " +
- " using existing implementation {}", cc.getClusterEventProducer().getClass().getName());
+ log.warn("Ignoring ClusterEventProducer config with non-standard name: {}", plugin.getInfo());
}
}
}
@Override
public void modified(ContainerPluginsRegistry.ApiInfo old, ContainerPluginsRegistry.ApiInfo replacement) {
- added(replacement);
deleted(old);
+ added(replacement);
}
};
plugins.registerListener(pluginListener);
diff --git a/solr/core/src/java/org/apache/solr/cluster/events/impl/CollectionsRepairEventListener.java b/solr/core/src/java/org/apache/solr/cluster/events/impl/CollectionsRepairEventListener.java
index 48400f8..8984d1d 100644
--- a/solr/core/src/java/org/apache/solr/cluster/events/impl/CollectionsRepairEventListener.java
+++ b/solr/core/src/java/org/apache/solr/cluster/events/impl/CollectionsRepairEventListener.java
@@ -41,6 +41,8 @@
import org.apache.solr.cluster.events.ClusterEvent;
import org.apache.solr.cluster.events.ClusterEventListener;
import org.apache.solr.cluster.events.NodesDownEvent;
+import org.apache.solr.cluster.placement.PlacementPluginConfig;
+import org.apache.solr.cluster.placement.PlacementPluginFactory;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.ReplicaPosition;
@@ -76,10 +78,12 @@
private int waitForSecond = DEFAULT_WAIT_FOR_SEC;
private ScheduledThreadPoolExecutor waitForExecutor;
+ private final PlacementPluginFactory<? extends PlacementPluginConfig> placementPluginFactory;
public CollectionsRepairEventListener(CoreContainer cc) {
this.solrClient = cc.getSolrClientCache().getCloudSolrClient(cc.getZkController().getZkClient().getZkServerAddress());
this.solrCloudManager = cc.getZkController().getSolrCloudManager();
+ this.placementPluginFactory = cc.getPlacementPluginFactory();
}
@VisibleForTesting
@@ -110,7 +114,7 @@
}
}
- private Map<String, Long> nodeNameVsTimeRemoved = new ConcurrentHashMap<>();
+ private final Map<String, Long> nodeNameVsTimeRemoved = new ConcurrentHashMap<>();
private void handleNodesDown(NodesDownEvent event) {
@@ -121,9 +125,7 @@
Set<String> trackingKeySet = nodeNameVsTimeRemoved.keySet();
trackingKeySet.removeAll(solrCloudManager.getClusterStateProvider().getLiveNodes());
// add any new lost nodes (old lost nodes are skipped)
- event.getNodeNames().forEachRemaining(lostNode -> {
- nodeNameVsTimeRemoved.computeIfAbsent(lostNode, n -> solrCloudManager.getTimeSource().getTimeNs());
- });
+ event.getNodeNames().forEachRemaining(lostNode -> nodeNameVsTimeRemoved.computeIfAbsent(lostNode, n -> solrCloudManager.getTimeSource().getTimeNs()));
}
private void runRepair() {
@@ -167,7 +169,7 @@
.incrementAndGet();
}
});
- Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(solrCloudManager, clusterState, coll);
+ Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(placementPluginFactory.createPluginInstance(), clusterState, coll);
lostReplicas.forEach((shard, types) -> {
Assign.AssignRequestBuilder assignRequestBuilder = new Assign.AssignRequestBuilder()
.forCollection(coll.getName())
@@ -191,7 +193,6 @@
newPositions.put(coll.getName(), positions);
} catch (Exception e) {
log.warn("Exception computing positions for {}/{}: {}", coll.getName(), shard, e);
- return;
}
});
});
@@ -206,15 +207,13 @@
// send ADDREPLICA admin requests for each lost replica
// XXX should we use 'async' for that, to avoid blocking here?
List<CollectionAdminRequest.AddReplica> addReplicas = new ArrayList<>();
- newPositions.forEach((collection, positions) -> {
- positions.forEach(position -> {
- CollectionAdminRequest.AddReplica addReplica = CollectionAdminRequest
- .addReplicaToShard(collection, position.shard, position.type);
- addReplica.setNode(position.node);
- addReplica.setAsyncId(ASYNC_ID_PREFIX + counter.incrementAndGet());
- addReplicas.add(addReplica);
- });
- });
+ newPositions.forEach((collection, positions) -> positions.forEach(position -> {
+ CollectionAdminRequest.AddReplica addReplica = CollectionAdminRequest
+ .addReplicaToShard(collection, position.shard, position.type);
+ addReplica.setNode(position.node);
+ addReplica.setAsyncId(ASYNC_ID_PREFIX + counter.incrementAndGet());
+ addReplicas.add(addReplica);
+ }));
addReplicas.forEach(addReplica -> {
try {
solrClient.request(addReplica);
@@ -231,7 +230,7 @@
new SolrNamedThreadFactory("collectionsRepair_waitFor"));
waitForExecutor.setRemoveOnCancelPolicy(true);
waitForExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
- waitForExecutor.scheduleAtFixedRate(() -> runRepair(), 0, waitForSecond, TimeUnit.SECONDS);
+ waitForExecutor.scheduleAtFixedRate(this::runRepair, 0, waitForSecond, TimeUnit.SECONDS);
state = State.RUNNING;
}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginConfig.java b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginConfig.java
index d223dcc..fd5566d 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginConfig.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPluginConfig.java
@@ -14,112 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.solr.cluster.placement;
+import org.apache.solr.common.util.ReflectMapWriter;
+
/**
- * <p>Configuration passed by Solr to {@link PlacementPluginFactory#createPluginInstance(PlacementPluginConfig)} so that plugin instances
- * ({@link PlacementPlugin}) created by the factory can easily retrieve their configuration.</p>
- *
- * <p>A plugin writer decides the names and the types of the configurable parameters it needs. Available types are
- * {@link String}, {@link Long}, {@link Boolean}, {@link Double}. This configuration currently lives in the {@code /clusterprops.json}
- * file in Zookeeper (this could change in the future, the plugin code will not change but the way to store its configuration
- * in the cluster might). {@code clusterprops.json} also contains the name of the plugin factory class implementing
- * {@link org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory}.</p>
- *
- * <p>In order to configure a plugin to be used for placement decisions, the following {@code curl} command (or something
- * equivalent) has to be executed once the cluster is already running to set the configuration.
- * Replace {@code localhost:8983} by one of your servers' IP address and port.</p>
- *
- * <pre>
- *
- * curl -X POST -H 'Content-type:application/json' -d '{
- * "set-placement-plugin": {
- * "class": "factory.class.name$inner",
- * "myfirstString": "a text value",
- * "aLong": 50,
- * "aDoubleConfig": 3.1415928,
- * "shouldIStay": true
- * }
- * }' http://localhost:8983/api/cluster
- * </pre>
- *
- * <p>The consequence will be the creation (or replacement if it exists) of an element in the Zookeeper file
- * {@code /clusterprops.json} as follows:</p>
- *
- * <pre>
- *
- * "placement-plugin":{
- * "class":"factory.class.name$inner",
- * "myfirstString": "a text value",
- * "aLong": 50,
- * "aDoubleConfig": 3.1415928,
- * "shouldIStay": true}
- * </pre>
- *
- * <p>In order to delete the placement-plugin section from {@code /clusterprops.json} (and to fallback to either Legacy
- * or rule based placement if so configured for a collection), execute:</p>
- *
- * <pre>
- *
- * curl -X POST -H 'Content-type:application/json' -d '{
- * "set-placement-plugin" : null
- * }' http://localhost:8983/api/cluster
- * </pre>
+ * Configuration beans should use this interface to define public
+ * (mutable) configuration properties. Implementations must have a
+ * public zero-args constructor. Class fields may be optionally
+ * annotated with {@link org.apache.solr.common.annotation.JsonProperty} if needed.
*/
-public interface PlacementPluginConfig {
-
- /**
- * The key in {@code clusterprops.json} under which the plugin factory and the plugin configuration are defined.
- */
- String PLACEMENT_PLUGIN_CONFIG_KEY = "placement-plugin";
- /**
- * Name of the property containing the factory class
- */
- String FACTORY_CLASS = "class";
-
- /**
- * @return the configured {@link String} value corresponding to {@code configName} if one exists (could be the empty
- * string) and {@code null} otherwise.
- */
- String getStringConfig(String configName);
-
- /**
- * @return the configured {@link String} value corresponding to {@code configName} if one exists (could be the empty
- * string) and {@code defaultValue} otherwise.
- */
- String getStringConfig(String configName, String defaultValue);
-
- /**
- * @return the configured {@link Boolean} value corresponding to {@code configName} if one exists, {@code null} otherwise.
- */
- Boolean getBooleanConfig(String configName);
-
- /**
- * @return the configured {@link Boolean} value corresponding to {@code configName} if one exists, a boxed {@code defaultValue}
- * otherwise (this method never returns {@code null}.
- */
- Boolean getBooleanConfig(String configName, boolean defaultValue);
-
- /**
- * @return the configured {@link Long} value corresponding to {@code configName} if one exists, {@code null} otherwise.
- */
- Long getLongConfig(String configName);
-
- /**
- * @return the configured {@link Long} value corresponding to {@code configName} if one exists, a boxed {@code defaultValue}
- * otherwise (this method never returns {@code null}.
- */
- Long getLongConfig(String configName, long defaultValue);
-
- /**
- * @return the configured {@link Double} value corresponding to {@code configName} if one exists, {@code null} otherwise.
- */
- Double getDoubleConfig(String configName);
-
- /**
- * @return the configured {@link Double} value corresponding to {@code configName} if one exists, a boxed {@code defaultValue}
- * otherwise (this method never returns {@code null}.
- */
- Double getDoubleConfig(String configName, double defaultValue);
+public interface PlacementPluginConfig extends ReflectMapWriter {
}
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 7372003..3bfc0d8 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
@@ -17,15 +17,53 @@
package org.apache.solr.cluster.placement;
+import org.apache.solr.api.ConfigurablePlugin;
+
/**
- * Factory implemented by client code and configured in {@code solr.xml} allowing the creation of instances of
+ * Factory implemented by client code and configured in container plugins
+ * (see {@link org.apache.solr.handler.admin.ContainerPluginsApi#editAPI})
+ * allowing the creation of instances of
* {@link PlacementPlugin} to be used for replica placement computation.
+ * <p>Note: configurable factory implementations should also implement
+ * {@link org.apache.solr.api.ConfigurablePlugin} with the appropriate configuration
+ * bean type.</p>
*/
-public interface PlacementPluginFactory {
+public interface PlacementPluginFactory<T extends PlacementPluginConfig> extends ConfigurablePlugin<T> {
/**
- * Returns an instance of the plugin that will be repeatedly (and concurrently) be called to compute placement. Multiple
+ * The key in the plugins registry under which this plugin and its configuration are defined.
+ */
+ String PLUGIN_NAME = ".placement-plugin";
+
+ /**
+ * 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 legacy assignment strategy will be used
+ * (see {@link org.apache.solr.cloud.api.collections.Assign.LegacyAssignStrategy}).</p>
*/
- PlacementPlugin createPluginInstance(PlacementPluginConfig config);
+ PlacementPlugin createPluginInstance();
+
+ /**
+ * Default implementation is a no-op. Override to provide meaningful
+ * behavior if needed.
+ * @param cfg value deserialized from JSON, not null.
+ */
+ @Override
+ default void configure(T cfg) {
+ // no-op
+ }
+
+ /**
+ * Return the configuration of the plugin.
+ * Default implementation returns null.
+ */
+ default T getConfig() {
+ return null;
+ }
+
+ /**
+ * Useful type for plugins that don't use any configuration.
+ */
+ class NoConfig implements PlacementPluginConfig {
+ }
}
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
new file mode 100644
index 0000000..9786fd5
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/DelegatingPlacementPluginFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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.cluster.placement.impl;
+
+import org.apache.solr.cluster.placement.PlacementPlugin;
+import org.apache.solr.cluster.placement.PlacementPluginConfig;
+import org.apache.solr.cluster.placement.PlacementPluginFactory;
+
+/**
+ * Helper class to support dynamic reloading of plugin implementations.
+ */
+public final class DelegatingPlacementPluginFactory implements PlacementPluginFactory<PlacementPluginFactory.NoConfig> {
+
+ private volatile PlacementPluginFactory<? extends PlacementPluginConfig> delegate;
+ // support for tests to make sure the update is completed
+ private volatile int version;
+
+ @Override
+ public PlacementPlugin createPluginInstance() {
+ if (delegate != null) {
+ return delegate.createPluginInstance();
+ } else {
+ return null;
+ }
+ }
+
+ public void setDelegate(PlacementPluginFactory<? extends PlacementPluginConfig> delegate) {
+ this.delegate = delegate;
+ this.version++;
+ }
+
+ public PlacementPluginFactory<? extends PlacementPluginConfig> getDelegate() {
+ return delegate;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPlanFactoryImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPlanFactoryImpl.java
index 7f7f89f..35671d1 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPlanFactoryImpl.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPlanFactoryImpl.java
@@ -24,6 +24,9 @@
import java.util.Set;
+/**
+ * Simple implementation of {@link PlacementPlanFactory}.
+ */
public class PlacementPlanFactoryImpl implements PlacementPlanFactory {
@Override
public PlacementPlan createPlacementPlan(PlacementRequest request, Set<ReplicaPlacement> replicaPlacements) {
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginConfigImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginConfigImpl.java
deleted file mode 100644
index 30cb6ef..0000000
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginConfigImpl.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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.cluster.placement.impl;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.solr.client.solrj.cloud.SolrCloudManager;
-import org.apache.solr.cluster.placement.PlacementPlugin;
-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.common.SolrException;
-import org.apache.solr.common.util.Utils;
-
-/**
- * <p>This concrete class is implementing the config as visible by the placement plugins and contains the code transforming the
- * plugin configuration (currently stored in {@code clusterprops.json} into a strongly typed abstraction (that will not
- * change if internally plugin configuration is moved to some other place).</p>
- *
- * <p>This class also contains the (static) code dealing with instantiating the plugin factory config (it is config, even though
- * of a slightly different type). This code is not accessed by the plugin code but used from the
- * {@link org.apache.solr.cloud.api.collections.Assign} class.</p>
- */
-public class PlacementPluginConfigImpl implements PlacementPluginConfig {
-
- // Separating configs into typed maps based on the element names in solr.xml
- private final Map<String, String> stringConfigs;
- private final Map<String, Long> longConfigs;
- private final Map<String, Boolean> boolConfigs;
- private final Map<String, Double> doubleConfigs;
-
-
- private PlacementPluginConfigImpl(Map<String, String> stringConfigs,
- Map<String, Long> longConfigs,
- Map<String, Boolean> boolConfigs,
- Map<String, Double> doubleConfigs) {
- this.stringConfigs = stringConfigs;
- this.longConfigs = longConfigs;
- this.boolConfigs = boolConfigs;
- this.doubleConfigs = doubleConfigs;
- }
-
- @Override
- public String getStringConfig(String configName) {
- return stringConfigs.get(configName);
- }
-
- @Override
- public String getStringConfig(String configName, String defaultValue) {
- String retval = stringConfigs.get(configName);
- return retval != null ? retval : defaultValue;
- }
-
- @Override
- public Boolean getBooleanConfig(String configName) {
- return boolConfigs.get(configName);
- }
-
- @Override
- public Boolean getBooleanConfig(String configName, boolean defaultValue) {
- Boolean retval = boolConfigs.get(configName);
- return retval != null ? retval : defaultValue;
- }
-
- @Override
- public Long getLongConfig(String configName) {
- return longConfigs.get(configName);
- }
-
- @Override
- public Long getLongConfig(String configName, long defaultValue) {
- Long retval = longConfigs.get(configName);
- return retval != null ? retval : defaultValue;
- }
-
- @Override
- public Double getDoubleConfig(String configName) {
- return doubleConfigs.get(configName);
- }
-
- @Override
- public Double getDoubleConfig(String configName, double defaultValue) {
- Double retval = doubleConfigs.get(configName);
- return retval != null ? retval : defaultValue;
- }
-
- /**
- * <p>Parses the {@link Map} obtained as the value for key {@link #PLACEMENT_PLUGIN_CONFIG_KEY} from
- * the {@code clusterprops.json} configuration {@link Map} (obtained by calling
- * {@link org.apache.solr.client.solrj.impl.ClusterStateProvider#getClusterProperties()}) and translates it into a
- * configuration consumable by the plugin (and that will not change as Solr changes internally how and where it stores
- * configuration).</p>
- *
- * <p>Configuration properties {@code class} and {@code name} are reserved: for defining the plugin factory class and
- * a human readable plugin name. All other properties are plugin specific.</p>
- *
- * <p>See configuration example and how-to in {@link AffinityPlacementFactory}.</p>
- */
- public static PlacementPluginConfig createConfigFromProperties(Map<String, Object> pluginConfig) {
- final Map<String, String> stringConfigs = new HashMap<>();
- final Map<String, Long> longConfigs = new HashMap<>();
- final Map<String, Boolean> boolConfigs = new HashMap<>();
- final Map<String, Double> doubleConfigs = new HashMap<>();
-
- for (Map.Entry<String, Object> e : pluginConfig.entrySet()) {
- String key = e.getKey();
- if (PlacementPluginConfig.FACTORY_CLASS.equals(key)) {
- continue;
- }
-
- if (key == null) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Missing config name attribute in parameter of " + PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY);
- }
-
- Object value = e.getValue();
-
- if (value == null) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Missing config value for parameter " + key + " of " + PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY);
- }
-
- if (value instanceof String) {
- stringConfigs.put(key, (String) value);
- } else if (value instanceof Long) {
- longConfigs.put(key, (Long) value);
- } else if (value instanceof Boolean) {
- boolConfigs.put(key, (Boolean) value);
- } else if (value instanceof Double) {
- doubleConfigs.put(key, (Double) value);
- } else {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unsupported config type " + value.getClass().getName() +
- " for parameter " + key + " of " + PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY);
- }
- }
-
- return new PlacementPluginConfigImpl(stringConfigs, longConfigs, boolConfigs, doubleConfigs);
- }
-
- /**
- * <p>This is where the plugin configuration is being read (from wherever in Solr it lives, and this will likely change with time),
- * a {@link org.apache.solr.cluster.placement.PlacementPluginFactory} (as configured) instantiated and a plugin instance
- * created from this factory.</p>
- *
- * <p>The initial implementation you see here is crude! the configuration is read anew each time and the factory class
- * as well as the plugin class instantiated each time.
- * This has to be changed once the code is accepted overall, to register a listener that is notified when the configuration
- * changes (see {@link org.apache.solr.common.cloud.ZkStateReader#registerClusterPropertiesListener})
- * and that will either create a new instance of the plugin with new configuration using the existing factory (if the factory
- * class has not changed - we need to keep track of this one) of create a new factory altogether (then a new plugin instance).</p>
- */
- @SuppressWarnings({"unchecked"})
- public static PlacementPlugin getPlacementPlugin(SolrCloudManager solrCloudManager) {
- Map<String, Object> props = solrCloudManager.getClusterStateProvider().getClusterProperties();
- Map<String, Object> pluginConfigMap = (Map<String, Object>) props.get(PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY);
-
- if (pluginConfigMap == null) {
- return null;
- }
-
- String pluginFactoryClassName = (String) pluginConfigMap.get(PlacementPluginConfig.FACTORY_CLASS);
-
- // Get the configured plugin factory class. Is there a way to load a resource in Solr without being in the context of
- // CoreContainer? Here the placement code is unrelated to the presence of cores (and one can imagine it running on
- // specialized nodes not having a CoreContainer). I guess the loading code below is not totally satisfying (although
- // it's not the only place in Solr doing it that way), but I didn't find more satisfying alternatives. Open to suggestions.
- PlacementPluginFactory placementPluginFactory;
- try {
- Class<? extends PlacementPluginFactory> factoryClazz =
- Class.forName(pluginFactoryClassName, true, PlacementPluginConfigImpl.class.getClassLoader())
- .asSubclass(PlacementPluginFactory.class);
-
- placementPluginFactory = factoryClazz.getConstructor().newInstance(); // no args constructor - that's why we introduced a factory...
- } catch (Exception e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unable to instantiate placement-plugin factory: " +
- Utils.toJSONString(pluginConfigMap) + " please review /clusterprops.json config for " + PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY, e);
- }
-
- // Translate the config from the properties where they are defined into the abstraction seen by the plugin
- PlacementPluginConfig pluginConfig = createConfigFromProperties(pluginConfigMap);
-
- return placementPluginFactory.createPluginInstance(pluginConfig);
- }
-}
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
new file mode 100644
index 0000000..be534b3
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginFactoryLoader.java
@@ -0,0 +1,80 @@
+/*
+ * 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.cluster.placement.impl;
+
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.invoke.MethodHandles;
+
+/**
+ * Utility class to load the configured {@link PlacementPluginFactory} plugin and
+ * then keep it up to date as the plugin configuration changes.
+ */
+public class PlacementPluginFactoryLoader {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public static void load(DelegatingPlacementPluginFactory pluginFactory, ContainerPluginsRegistry plugins) {
+ ContainerPluginsRegistry.ApiInfo pluginFactoryInfo = plugins.getPlugin(PlacementPluginFactory.PLUGIN_NAME);
+ if (pluginFactoryInfo != null && (pluginFactoryInfo.getInstance() instanceof PlacementPluginFactory)) {
+ pluginFactory.setDelegate((PlacementPluginFactory<? extends PlacementPluginConfig>) pluginFactoryInfo.getInstance());
+ }
+ ContainerPluginsRegistry.PluginRegistryListener pluginListener = new ContainerPluginsRegistry.PluginRegistryListener() {
+ @Override
+ public void added(ContainerPluginsRegistry.ApiInfo plugin) {
+ if (plugin == null || plugin.getInstance() == null) {
+ return;
+ }
+ Object instance = plugin.getInstance();
+ if (instance instanceof PlacementPluginFactory) {
+ setDelegate(plugin.getInfo(), (PlacementPluginFactory<? extends PlacementPluginConfig>) instance);
+ }
+ }
+
+ @Override
+ public void deleted(ContainerPluginsRegistry.ApiInfo plugin) {
+ if (plugin == null || plugin.getInstance() == null) {
+ return;
+ }
+ Object instance = plugin.getInstance();
+ if (instance instanceof PlacementPluginFactory) {
+ setDelegate(plugin.getInfo(), null);
+ }
+ }
+
+ @Override
+ public void modified(ContainerPluginsRegistry.ApiInfo old, ContainerPluginsRegistry.ApiInfo replacement) {
+ added(replacement);
+ }
+
+ private void setDelegate(PluginMeta pluginMeta, PlacementPluginFactory<? extends PlacementPluginConfig> factory) {
+ if (PlacementPluginFactory.PLUGIN_NAME.equals(pluginMeta.name)) {
+ pluginFactory.setDelegate(factory);
+ } else {
+ log.warn("Ignoring PlacementPluginFactory plugin with non-standard name: {}", pluginMeta);
+ }
+ }
+ };
+ plugins.registerListener(pluginListener);
+ }
+
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
new file mode 100644
index 0000000..bbf8dc8
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cluster.placement.plugins;
+
+import org.apache.solr.cluster.placement.PlacementPluginConfig;
+import org.apache.solr.common.annotation.JsonProperty;
+
+/**
+ * Configuration bean for {@link AffinityPlacementFactory}.
+ */
+public class AffinityPlacementConfig implements PlacementPluginConfig {
+
+ public static final AffinityPlacementConfig DEFAULT = new AffinityPlacementConfig();
+
+ /**
+ * If a node has strictly less GB of free disk than this value, the node is excluded from assignment decisions.
+ * Set to 0 or less to disable.
+ */
+ @JsonProperty
+ public long minimalFreeDiskGB;
+
+ /**
+ * Replica allocation will assign replicas to nodes with at least this number of GB of free disk space regardless
+ * of the number of cores on these nodes rather than assigning replicas to nodes with less than this amount of free
+ * disk space if that's an option (if that's not an option, replicas can still be assigned to nodes with less than this
+ * amount of free space).
+ */
+ @JsonProperty
+ public long prioritizedFreeDiskGB;
+
+ // no-arg public constructor required for deserialization
+ public AffinityPlacementConfig() {
+ minimalFreeDiskGB = 20L;
+ prioritizedFreeDiskGB = 100L;
+ }
+
+ public AffinityPlacementConfig(long minimalFreeDiskGB, long prioritizedFreeDiskGB) {
+ this.minimalFreeDiskGB = minimalFreeDiskGB;
+ this.prioritizedFreeDiskGB = prioritizedFreeDiskGB;
+ }
+}
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 06bdda7..be72190 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
@@ -115,7 +115,7 @@
* make it relatively easy to adapt it to (somewhat) different assumptions. Configuration options could be introduced
* to allow configuration base option selection as well...</p>
*/
-public class AffinityPlacementFactory implements PlacementPluginFactory {
+public class AffinityPlacementFactory implements PlacementPluginFactory<AffinityPlacementConfig> {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
@@ -140,19 +140,7 @@
*/
public static final String UNDEFINED_AVAILABILITY_ZONE = "uNd3f1NeD";
- /**
- * If a node has strictly less GB of free disk than this value, the node is excluded from assignment decisions.
- * Set to 0 or less to disable.
- */
- public static final String MINIMAL_FREE_DISK_GB = "minimalFreeDiskGB";
-
- /**
- * Replica allocation will assign replicas to nodes with at least this number of GB of free disk space regardless
- * of the number of cores on these nodes rather than assigning replicas to nodes with less than this amount of free
- * disk space if that's an option (if that's not an option, replicas can still be assigned to nodes with less than this
- * amount of free space).
- */
- public static final String PRIORITIZED_FREE_DISK_GB = "prioritizedFreeDiskGB";
+ private AffinityPlacementConfig config = AffinityPlacementConfig.DEFAULT;
/**
* Empty public constructor is used to instantiate this factory. Using a factory pattern to allow the factory to do one
@@ -164,10 +152,19 @@
}
@Override
- public PlacementPlugin createPluginInstance(PlacementPluginConfig config) {
- final long minimalFreeDiskGB = config.getLongConfig(MINIMAL_FREE_DISK_GB, 20L);
- final long prioritizedFreeDiskGB = config.getLongConfig(PRIORITIZED_FREE_DISK_GB, 100L);
- return new AffinityPlacementPlugin(minimalFreeDiskGB, prioritizedFreeDiskGB);
+ public PlacementPlugin createPluginInstance() {
+ return new AffinityPlacementPlugin(config.minimalFreeDiskGB, config.prioritizedFreeDiskGB);
+ }
+
+ @Override
+ public void configure(AffinityPlacementConfig cfg) {
+ Objects.requireNonNull(cfg, "configuration must never be null");
+ this.config = cfg;
+ }
+
+ @Override
+ public AffinityPlacementConfig getConfig() {
+ return config;
}
/**
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
index b73b692..5038ddd 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
@@ -40,10 +40,10 @@
*
* <p>See {@link AffinityPlacementFactory} for a more realistic example and documentation.</p>
*/
-public class MinimizeCoresPlacementFactory implements PlacementPluginFactory {
+public class MinimizeCoresPlacementFactory implements PlacementPluginFactory<PlacementPluginFactory.NoConfig> {
@Override
- public PlacementPlugin createPluginInstance(PlacementPluginConfig config) {
+ public PlacementPlugin createPluginInstance() {
return new MinimizeCoresPlacementPlugin();
}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java
new file mode 100644
index 0000000..0b27d21
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java
@@ -0,0 +1,93 @@
+/*
+ * 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.cluster.placement.plugins;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.solr.cluster.Cluster;
+import org.apache.solr.cluster.Node;
+import org.apache.solr.cluster.Replica;
+import org.apache.solr.cluster.SolrCollection;
+import org.apache.solr.cluster.placement.*;
+
+/**
+ * <p>Factory for creating {@link RandomPlacementPlugin}, a placement plugin implementing random placement for new
+ * collection creation while preventing two replicas of same shard from being placed on same node..</p>
+ *
+ * <p>See {@link AffinityPlacementFactory} for a more realistic example and documentation.</p>
+ */
+public class RandomPlacementFactory implements PlacementPluginFactory<PlacementPluginFactory.NoConfig> {
+
+ @Override
+ public PlacementPlugin createPluginInstance() {
+ return new RandomPlacementPlugin();
+ }
+
+ public static class RandomPlacementPlugin implements PlacementPlugin {
+ private final Random replicaPlacementRandom = new Random(); // ok even if random sequence is predictable.
+
+ private RandomPlacementPlugin() {
+ // We make things reproducible in tests by using test seed if any
+ String seed = System.getProperty("tests.seed");
+ if (seed != null) {
+ replicaPlacementRandom.setSeed(seed.hashCode());
+ }
+ }
+
+ public PlacementPlan computePlacement(Cluster cluster, PlacementRequest request, AttributeFetcher attributeFetcher,
+ PlacementPlanFactory placementPlanFactory) throws PlacementException {
+ int totalReplicasPerShard = 0;
+ for (Replica.ReplicaType rt : Replica.ReplicaType.values()) {
+ totalReplicasPerShard += request.getCountReplicasToCreate(rt);
+ }
+
+ if (cluster.getLiveNodes().size() < totalReplicasPerShard) {
+ throw new PlacementException("Cluster size too small for number of replicas per shard");
+ }
+
+ Set<ReplicaPlacement> replicaPlacements = new HashSet<>(totalReplicasPerShard * request.getShardNames().size());
+
+ // Now place randomly all replicas of all shards on available nodes
+ for (String shardName : request.getShardNames()) {
+ // Shuffle the nodes for each shard so that replicas for a shard are placed on distinct yet random nodes
+ ArrayList<Node> nodesToAssign = new ArrayList<>(cluster.getLiveNodes());
+ Collections.shuffle(nodesToAssign, replicaPlacementRandom);
+
+ for (Replica.ReplicaType rt : Replica.ReplicaType.values()) {
+ placeForReplicaType(request.getCollection(), nodesToAssign, placementPlanFactory, replicaPlacements, shardName, request, rt);
+ }
+ }
+
+ return placementPlanFactory.createPlacementPlan(request, replicaPlacements);
+ }
+
+ private void placeForReplicaType(SolrCollection solrCollection, ArrayList<Node> nodesToAssign, PlacementPlanFactory placementPlanFactory,
+ Set<ReplicaPlacement> replicaPlacements,
+ String shardName, PlacementRequest request, Replica.ReplicaType replicaType) {
+ for (int replica = 0; replica < request.getCountReplicasToCreate(replicaType); replica++) {
+ Node node = nodesToAssign.remove(0);
+
+ replicaPlacements.add(placementPlanFactory.createReplicaPlacement(solrCollection, shardName, node, replicaType));
+ }
+ }
+ }
+}
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 7331cef..45bffd1 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -74,6 +74,10 @@
import org.apache.solr.cloud.ZkController;
import org.apache.solr.cluster.events.ClusterEventProducer;
import org.apache.solr.cluster.events.impl.ClusterEventProducerFactory;
+import org.apache.solr.cluster.placement.PlacementPluginConfig;
+import org.apache.solr.cluster.placement.PlacementPluginFactory;
+import org.apache.solr.cluster.placement.impl.DelegatingPlacementPluginFactory;
+import org.apache.solr.cluster.placement.impl.PlacementPluginFactoryLoader;
import org.apache.solr.common.AlreadyClosedException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
@@ -255,8 +259,8 @@
!getZkController().getOverseer().isClosed(),
(r) -> this.runAsync(r));
- // initially these are the same to collect the plugin-based listeners during init
- private ClusterEventProducer clusterEventProducer;
+ private volatile ClusterEventProducer clusterEventProducer;
+ private final DelegatingPlacementPluginFactory placementPluginFactory = new DelegatingPlacementPluginFactory();
private PackageStoreAPI packageStoreAPI;
private PackageLoader packageLoader;
@@ -896,6 +900,10 @@
containerHandlers.getApiBag().registerObject(containerPluginsApi.readAPI);
containerHandlers.getApiBag().registerObject(containerPluginsApi.editAPI);
+ // initialize the placement plugin factory wrapper
+ // with the plugin configuration from the registry
+ PlacementPluginFactoryLoader.load(placementPluginFactory, containerPluginsRegistry);
+
// create target ClusterEventProducer (possibly from plugins)
clusterEventProducer = clusterEventProducerFactory.create(containerPluginsRegistry);
@@ -2180,6 +2188,10 @@
return clusterEventProducer;
}
+ public PlacementPluginFactory<? extends PlacementPluginConfig> getPlacementPluginFactory() {
+ return placementPluginFactory;
+ }
+
static {
ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider());
}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index 605dbb6..ee77e3d 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -27,8 +27,6 @@
import org.apache.solr.client.solrj.request.beans.CreateConfigInfo;
import org.apache.solr.client.solrj.request.beans.RateLimiterMeta;
import org.apache.solr.cloud.OverseerConfigSetMessageHandler;
-import org.apache.solr.cluster.placement.PlacementPluginConfig;
-import org.apache.solr.common.MapWriterMap;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.cloud.ClusterProperties;
@@ -243,26 +241,6 @@
collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), m), obj.getResponse());
}
- @Command(name = "set-placement-plugin")
- public void setPlacementPlugin(PayloadObj<Map<String, Object>> obj) {
- Map<String, Object> placementPluginConfig = obj.getDataMap();
- if(placementPluginConfig.isEmpty()) placementPluginConfig = null;
- ClusterProperties clusterProperties = new ClusterProperties(getCoreContainer().getZkController().getZkClient());
- // When the json contains { "set-placement-plugin" : null }, the map is empty, not null.
- // Very basic sanity check. Real validation will be done when the config is used...
- if (!(placementPluginConfig == null) && !placementPluginConfig.containsKey(PlacementPluginConfig.FACTORY_CLASS)) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Must contain " + PlacementPluginConfig.FACTORY_CLASS + " attribute (or be null)");
- }
- try {
- clusterProperties.update(placementPluginConfig == null?
- null:
- new MapWriterMap(placementPluginConfig),
- PlacementPluginConfig.PLACEMENT_PLUGIN_CONFIG_KEY);
- } catch (Exception e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in API", e);
- }
- }
-
@Command(name = "set-ratelimiter")
public void setRateLimiters(PayloadObj<RateLimiterMeta> payLoad) {
RateLimiterMeta rateLimiterConfig = payLoad.get();
diff --git a/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java b/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
index 5b18ab0..6127e59 100644
--- a/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
@@ -42,6 +42,7 @@
import org.apache.solr.cloud.Overseer.LeaderStatus;
import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
import org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler;
+import org.apache.solr.cluster.placement.PlacementPluginFactory;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
@@ -122,8 +123,10 @@
private static CoreContainer coreContainerMock;
private static UpdateShardHandler updateShardHandlerMock;
private static HttpClient httpClientMock;
+ @SuppressWarnings("rawtypes")
+ private static PlacementPluginFactory placementPluginFactoryMock;
private static SolrMetricsContext solrMetricsContextMock;
-
+
private static ObjectCache objectCache;
private Map<String, byte[]> zkClientData = new HashMap<>();
private final Map<String, ClusterState.CollectionRef> collectionsSet = new HashMap<>();
@@ -183,6 +186,7 @@
coreContainerMock = mock(CoreContainer.class);
updateShardHandlerMock = mock(UpdateShardHandler.class);
httpClientMock = mock(HttpClient.class);
+ placementPluginFactoryMock = mock(PlacementPluginFactory.class);
solrMetricsContextMock = mock(SolrMetricsContext.class);
}
@@ -208,6 +212,7 @@
coreContainerMock = null;
updateShardHandlerMock = null;
httpClientMock = null;
+ placementPluginFactoryMock = null;
solrMetricsContextMock = null;
}
@@ -238,6 +243,7 @@
reset(coreContainerMock);
reset(updateShardHandlerMock);
reset(httpClientMock);
+ reset(placementPluginFactoryMock);
reset(solrMetricsContextMock);
zkClientData.clear();
@@ -250,7 +256,8 @@
stopComponentUnderTest();
super.tearDown();
}
-
+
+ @SuppressWarnings("unchecked")
protected Set<String> commonMocks(int liveNodesCount) throws Exception {
when(shardHandlerFactoryMock.getShardHandler()).thenReturn(shardHandlerMock);
when(workQueueMock.peekTopN(anyInt(), any(), anyLong())).thenAnswer(invocation -> {
@@ -367,6 +374,7 @@
when(overseerMock.getSolrCloudManager()).thenReturn(cloudDataProviderMock);
when(overseerMock.getCoreContainer()).thenReturn(coreContainerMock);
when(coreContainerMock.getUpdateShardHandler()).thenReturn(updateShardHandlerMock);
+ when(coreContainerMock.getPlacementPluginFactory()).thenReturn(placementPluginFactoryMock);
when(updateShardHandlerMock.getDefaultHttpClient()).thenReturn(httpClientMock);
when(zkControllerMock.getSolrCloudManager()).thenReturn(cloudDataProviderMock);
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 ac325b1..676c70f 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
@@ -20,20 +20,35 @@
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.V2Request;
+import org.apache.solr.client.solrj.request.beans.PluginMeta;
import org.apache.solr.client.solrj.response.CollectionAdminResponse;
-import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.client.solrj.response.V2Response;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.cluster.placement.PlacementPluginConfig;
+import org.apache.solr.cluster.placement.PlacementPluginFactory;
+import org.apache.solr.cluster.placement.plugins.AffinityPlacementConfig;
+import org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cluster.placement.plugins.MinimizeCoresPlacementFactory;
-import org.apache.solr.common.cloud.ClusterProperties;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.util.TimeSource;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.util.LogLevel;
+import org.apache.solr.util.TimeOut;
+
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.Collections.singletonMap;
@@ -41,12 +56,14 @@
/**
* Test for {@link MinimizeCoresPlacementFactory} using a {@link MiniSolrCloudCluster}.
*/
+@LogLevel("org.apache.solr.cluster.placement.impl=DEBUG")
public class PlacementPluginIntegrationTest extends SolrCloudTestCase {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String COLLECTION = PlacementPluginIntegrationTest.class.getName() + "_collection";
- private static ClusterProperties clusterProperties;
private static SolrCloudManager cloudManager;
+ private static CoreContainer cc;
@BeforeClass
public static void setupCluster() throws Exception {
@@ -55,29 +72,37 @@
configureCluster(3)
.addConfig("conf", configset("cloud-minimal"))
.configure();
- cloudManager = cluster.getJettySolrRunner(0).getCoreContainer().getZkController().getSolrCloudManager();
- clusterProperties = new ClusterProperties(cluster.getZkClient());
+ cc = cluster.getJettySolrRunner(0).getCoreContainer();
+ cloudManager = cc.getZkController().getSolrCloudManager();
}
@After
public void cleanup() throws Exception {
cluster.deleteAllCollections();
- V2Request req = new V2Request.Builder("/cluster")
+ V2Request req = new V2Request.Builder("/cluster/plugin")
.forceV2(true)
- .POST()
- .withPayload(singletonMap("set-placement-plugin", Map.of()))
+ .GET()
.build();
- req.process(cluster.getSolrClient());
-
+ V2Response rsp = req.process(cluster.getSolrClient());
+ if (rsp._get(Arrays.asList("plugin", PlacementPluginFactory.PLUGIN_NAME), null) != null) {
+ req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload("{remove: '" + PlacementPluginFactory.PLUGIN_NAME + "'}")
+ .build();
+ req.process(cluster.getSolrClient());
+ }
}
@Test
public void testMinimizeCores() throws Exception {
- Map<String, Object> config = Map.of(PlacementPluginConfig.FACTORY_CLASS, MinimizeCoresPlacementFactory.class.getName());
- V2Request req = new V2Request.Builder("/cluster")
+ PluginMeta plugin = new PluginMeta();
+ plugin.name = PlacementPluginFactory.PLUGIN_NAME;
+ plugin.klass = MinimizeCoresPlacementFactory.class.getName();
+ V2Request req = new V2Request.Builder("/cluster/plugin")
.forceV2(true)
.POST()
- .withPayload(singletonMap("set-placement-plugin", config))
+ .withPayload(singletonMap("add", plugin))
.build();
req.process(cluster.getSolrClient());
@@ -90,9 +115,7 @@
DocCollection collection = clusterState.getCollectionOrNull(COLLECTION);
assertNotNull(collection);
Map<String, AtomicInteger> coresByNode = new HashMap<>();
- collection.forEachReplica((shard, replica) -> {
- coresByNode.computeIfAbsent(replica.getNodeName(), n -> new AtomicInteger()).incrementAndGet();
- });
+ collection.forEachReplica((shard, replica) -> coresByNode.computeIfAbsent(replica.getNodeName(), n -> new AtomicInteger()).incrementAndGet());
int maxCores = 0;
int minCores = Integer.MAX_VALUE;
for (Map.Entry<String, AtomicInteger> entry : coresByNode.entrySet()) {
@@ -109,4 +132,108 @@
assertEquals("min cores too low", 1, minCores);
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testDynamicReconfiguration() throws Exception {
+ PlacementPluginFactory<? extends PlacementPluginConfig> pluginFactory = cc.getPlacementPluginFactory();
+ assertTrue("wrong type " + pluginFactory.getClass().getName(), pluginFactory instanceof DelegatingPlacementPluginFactory);
+ DelegatingPlacementPluginFactory wrapper = (DelegatingPlacementPluginFactory) pluginFactory;
+
+ int version = wrapper.getVersion();
+ log.debug("--initial version={}", version);
+
+ PluginMeta plugin = new PluginMeta();
+ plugin.name = PlacementPluginFactory.PLUGIN_NAME;
+ plugin.klass = MinimizeCoresPlacementFactory.class.getName();
+ V2Request req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload(singletonMap("add", plugin))
+ .build();
+ req.process(cluster.getSolrClient());
+
+ version = waitForVersionChange(version, wrapper, 10);
+
+ assertTrue("wrong version " + version, version > 0);
+ PlacementPluginFactory<? extends PlacementPluginConfig> factory = wrapper.getDelegate();
+ assertTrue("wrong type " + factory.getClass().getName(), factory instanceof MinimizeCoresPlacementFactory);
+
+ // reconfigure
+ plugin.klass = AffinityPlacementFactory.class.getName();
+ plugin.config = new AffinityPlacementConfig(1, 2);
+ req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload(singletonMap("update", plugin))
+ .build();
+ req.process(cluster.getSolrClient());
+
+ version = waitForVersionChange(version, wrapper, 10);
+
+ factory = wrapper.getDelegate();
+ assertTrue("wrong type " + factory.getClass().getName(), factory instanceof AffinityPlacementFactory);
+ AffinityPlacementConfig config = ((AffinityPlacementFactory) factory).getConfig();
+ assertEquals("minimalFreeDiskGB", 1, config.minimalFreeDiskGB);
+ assertEquals("prioritizedFreeDiskGB", 2, config.prioritizedFreeDiskGB);
+
+ // change plugin config
+ plugin.config = new AffinityPlacementConfig(3, 4);
+ req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload(singletonMap("update", plugin))
+ .build();
+ req.process(cluster.getSolrClient());
+
+ version = waitForVersionChange(version, wrapper, 10);
+ factory = wrapper.getDelegate();
+ assertTrue("wrong type " + factory.getClass().getName(), factory instanceof AffinityPlacementFactory);
+ config = ((AffinityPlacementFactory) factory).getConfig();
+ assertEquals("minimalFreeDiskGB", 3, config.minimalFreeDiskGB);
+ assertEquals("prioritizedFreeDiskGB", 4, config.prioritizedFreeDiskGB);
+
+ // add plugin of the right type but with the wrong name
+ plugin.name = "myPlugin";
+ req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload(singletonMap("add", plugin))
+ .build();
+ req.process(cluster.getSolrClient());
+ try {
+ int newVersion = waitForVersionChange(version, wrapper, 5);
+ if (newVersion != version) {
+ fail("factory configuration updated but plugin name was wrong: " + plugin);
+ }
+ } catch (TimeoutException te) {
+ // expected
+ }
+ // remove plugin
+ req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .POST()
+ .withPayload("{remove: '" + PlacementPluginFactory.PLUGIN_NAME + "'}")
+ .build();
+ req.process(cluster.getSolrClient());
+ waitForVersionChange(version, wrapper, 10);
+ factory = wrapper.getDelegate();
+ assertNull("no factory should be present", factory);
+ }
+
+ private int waitForVersionChange(int currentVersion, DelegatingPlacementPluginFactory wrapper, int timeoutSec) throws Exception {
+ TimeOut timeout = new TimeOut(timeoutSec, TimeUnit.SECONDS, TimeSource.NANO_TIME);
+
+ while (!timeout.hasTimedOut()) {
+ int newVersion = wrapper.getVersion();
+ if (newVersion < currentVersion) {
+ throw new Exception("Invalid version - went back! currentVersion=" + currentVersion +
+ " newVersion=" + newVersion);
+ } else if (currentVersion < newVersion) {
+ log.debug("--current version was {}, new version is {}", currentVersion, newVersion);
+ return newVersion;
+ }
+ timeout.sleep(200);
+ }
+ throw new TimeoutException("version didn't change in time, currentVersion=" + currentVersion);
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
index 7e240b6..e048617 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
@@ -26,7 +26,6 @@
import org.apache.solr.cluster.placement.*;
import org.apache.solr.cluster.placement.Builders;
import org.apache.solr.cluster.placement.impl.PlacementPlanFactoryImpl;
-import org.apache.solr.cluster.placement.impl.PlacementPluginConfigImpl;
import org.apache.solr.cluster.placement.impl.PlacementRequestImpl;
import org.apache.solr.common.util.Pair;
import org.junit.BeforeClass;
@@ -54,9 +53,10 @@
@BeforeClass
public static void setupPlugin() {
- PlacementPluginConfig config = PlacementPluginConfigImpl.createConfigFromProperties(
- Map.of("minimalFreeDiskGB", MINIMAL_FREE_DISK_GB, "prioritizedFreeDiskGB", PRIORITIZED_FREE_DISK_GB));
- plugin = new AffinityPlacementFactory().createPluginInstance(config);
+ AffinityPlacementConfig config = new AffinityPlacementConfig(MINIMAL_FREE_DISK_GB, PRIORITIZED_FREE_DISK_GB);
+ AffinityPlacementFactory factory = new AffinityPlacementFactory();
+ factory.configure(config);
+ plugin = factory.createPluginInstance();
}
@Test
diff --git a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
index 01ab39f..224caf7 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
@@ -193,12 +193,14 @@
assertEquals( CConfig.class, ContainerPluginsRegistry.getConfigClass(new CC1()));
assertEquals( CConfig.class, ContainerPluginsRegistry.getConfigClass(new CC2()));
- CConfig p = new CConfig();
- p.boolVal = Boolean.TRUE;
- p.strVal = "Something";
- p.longVal = 1234L;
+ CConfig cfg = new CConfig();
+ cfg.boolVal = Boolean.TRUE;
+ cfg.strVal = "Something";
+ cfg.longVal = 1234L;
+ PluginMeta p = new PluginMeta();
p.name = "hello";
p.klass = CC.class.getName();
+ p.config = cfg;
new V2Request.Builder("/cluster/plugin")
.forceV2(true)
@@ -213,7 +215,7 @@
.build().process(cluster.getSolrClient()),
ImmutableMap.of("/config/boolVal", "true", "/config/strVal", "Something","/config/longVal", "1234" ));
- p.strVal = "Something else";
+ cfg.strVal = "Something else";
new V2Request.Builder("/cluster/plugin")
.forceV2(true)
.POST()
@@ -226,7 +228,7 @@
.forceV2(true)
.GET()
.build().process(cluster.getSolrClient()),
- ImmutableMap.of("/config/boolVal", "true", "/config/strVal", p.strVal,"/config/longVal", "1234" ));
+ ImmutableMap.of("/config/boolVal", "true", "/config/strVal", cfg.strVal,"/config/longVal", "1234" ));
// kill the Overseer leader
for (JettySolrRunner jetty : cluster.getJettySolrRunners()) {
@@ -391,12 +393,6 @@
@JsonProperty
public Boolean boolVal;
-
- @JsonProperty
- public String name;
-
- @JsonProperty(value = "class", required = true)
- public String klass;
}
public static class C6 implements ClusterSingleton {
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java
index 80098ca..bab68b2 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java
@@ -26,24 +26,33 @@
* POJO for a plugin metadata used in container plugins
*/
public class PluginMeta implements ReflectMapWriter {
+ /** Unique plugin name, required. */
@JsonProperty(required = true)
public String name;
+ /** Plugin implementation class, required. */
@JsonProperty(value = "class", required = true)
public String klass;
+ /** Plugin version. */
@JsonProperty
public String version;
+ /** Plugin API path prefix, optional. */
@JsonProperty("path-prefix")
public String pathPrefix;
+ /** Plugin configuration object, optional. */
+ @JsonProperty
+ public Object config;
+
public PluginMeta copy() {
PluginMeta result = new PluginMeta();
result.name = name;
result.klass = klass;
result.version = version;
+ result.config = config;
return result;
}
@@ -53,7 +62,8 @@
PluginMeta that = (PluginMeta) obj;
return Objects.equals(this.name, that.name) &&
Objects.equals(this.klass, that.klass) &&
- Objects.equals(this.version, that.version);
+ Objects.equals(this.version, that.version) &&
+ Objects.equals(this.config, that.config);
}
return false;
}
@@ -61,4 +71,9 @@
public int hashCode() {
return Objects.hash(name, version, klass);
}
+
+ @Override
+ public String toString() {
+ return jsonStr();
+ }
}