Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOLR-17096: Cluster Singleton plugin support in solr.xml #2126

Merged
merged 11 commits into from
Jan 17, 2024
Merged
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Improvements

* SOLR-16536: Replace OpenTracing instrumentation with OpenTelemetry (Alex Deparvu, janhoy)

* SOLR-17096: Support for Cluster Singleton plugins with immutable deployments (Paul McArthur)
dsmiley marked this conversation as resolved.
Show resolved Hide resolved

Optimizations
---------------------
(No changes)
Expand Down
59 changes: 59 additions & 0 deletions solr/core/src/java/org/apache/solr/api/ClusterPluginsSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.api;

import java.io.IOException;
import java.util.Map;
import java.util.function.Function;
import org.apache.solr.client.solrj.request.beans.PluginMeta;
import org.apache.solr.handler.admin.ContainerPluginsApi;

/** A source for Container Plugin configurations */
dsmiley marked this conversation as resolved.
Show resolved Hide resolved
public interface ClusterPluginsSource {

/**
* Get the Container Plugins Read Api for this plugin source
*
* @return A {@link ContainerPluginsApi} Read Api for this plugin source
*/
ContainerPluginsApi.Read getReadApi();

/**
* Get the Container Plugins Edit Api for this plugin source, if it supports edit operations
*
* @return A {@link ContainerPluginsApi} Edit Api for this plugin source, or null if the plugin
* source does not support editing the plugin configs
*/
ContainerPluginsApi.Edit getEditApi();

/**
* Get a map of container plugin configurations from this source, where keys are plugin names and
* values are {@link PluginMeta} plugin metadata.
*
* @return An immutable map of plugin configurations
*/
Map<String, Object> plugins() throws IOException;

/**
* Persist the updated set of plugin configs
*
* @param modifier A function providing the map of plugin configs to be persisted
*/
void persistPlugins(Function<Map<String, Object>, Map<String, Object>> modifier)
throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.api;

import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.NodeConfig;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.util.plugin.NamedListInitializedPlugin;

/**
* Loads the {@link ClusterPluginsSource} depending on the declared implementation. The default
* implementation is {@link ZkClusterPluginsSource}, but can be overridden by a system property, or
* by declaring the implementation class in solr.xml
*/
public class ClusterPluginsSourceConfigurator implements NamedListInitializedPlugin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending NamedListInitializedPlugin means we have an init() method. But what becomes of those args?


/**
* Resolves the name of the class that will be used to provide cluster plugins.
*
* @return The name of the class to use as the {@link ClusterPluginsSource}
*/
public static String resolveClassName() {
return NodeConfig.isImmutableConfigSet()
? NodeConfigClusterPluginsSource.class.getName()
: ZkClusterPluginsSource.class.getName();
}

public static ClusterPluginsSource loadClusterPluginsSource(
CoreContainer cc, SolrResourceLoader loader) {
return loader.newInstance(
resolveClassName(),
ClusterPluginsSource.class,
new String[0],
new Class<?>[] {CoreContainer.class},
new Object[] {cc});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Phaser;
import java.util.function.Supplier;
import org.apache.lucene.util.ResourceLoaderAware;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.beans.PluginMeta;
Expand All @@ -55,7 +54,6 @@
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.handler.admin.ContainerPluginsApi;
import org.apache.solr.pkg.SolrPackageLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
Expand All @@ -66,7 +64,7 @@
/**
* This class manages the container-level plugins and their Api-s. It is responsible for adding /
* removing / replacing the plugins according to the updated configuration obtained from {@link
* ContainerPluginsApi#plugins(Supplier)}.
* ClusterPluginsSource#plugins()}.
*
* <p>Plugins instantiated by this class may implement zero or more {@link Api}-s, which are then
* registered in the CoreContainer {@link ApiBag}. They may be also post-processed for additional
Expand All @@ -85,6 +83,8 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW
private final CoreContainer coreContainer;
private final ApiBag containerApiBag;

private final ClusterPluginsSource pluginsSource;

private final Map<String, ApiInfo> currentPlugins = new HashMap<>();

private Phaser phaser;
Expand Down Expand Up @@ -117,9 +117,11 @@ public void unregisterListener(PluginRegistryListener listener) {
listeners.remove(listener);
}

public ContainerPluginsRegistry(CoreContainer coreContainer, ApiBag apiBag) {
public ContainerPluginsRegistry(
CoreContainer coreContainer, ApiBag apiBag, ClusterPluginsSource pluginsSource) {
this.coreContainer = coreContainer;
this.containerApiBag = apiBag;
this.pluginsSource = pluginsSource;
}

@Override
Expand Down Expand Up @@ -171,7 +173,7 @@ public int hashCode() {
public synchronized void refresh() {
Map<String, Object> pluginInfos;
try {
pluginInfos = ContainerPluginsApi.plugins(coreContainer.zkClientSupplier);
pluginInfos = pluginsSource.plugins();
} catch (IOException e) {
log.error("Could not read plugins data", e);
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.api;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.NodeConfig;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.handler.admin.ContainerPluginsApi;

/**
* Plugin configurations that are defined in solr.xml. This supports immutable deployments, and the
* /cluster/plugin Edit APIs are not available
*/
public class NodeConfigClusterPluginsSource implements ClusterPluginsSource {

private final Map<String, Object> plugins;

private final ContainerPluginsApi api;

public NodeConfigClusterPluginsSource(final CoreContainer cc) {
api = new ContainerPluginsApi(cc, this);
plugins = Map.copyOf(readPlugins(cc.getNodeConfig()));
}

@Override
public ContainerPluginsApi.Read getReadApi() {
return api.readAPI;
}

@Override
public ContainerPluginsApi.Edit getEditApi() {
return null;
}

@Override
public Map<String, Object> plugins() throws IOException {
return plugins;
}

/**
* This method should never be invoked because the Edit Apis are not made available by the plugin
*
* @throws UnsupportedOperationException always
*/
@Override
public void persistPlugins(Function<Map<String, Object>, Map<String, Object>> modifier) {
throw new UnsupportedOperationException(
"The NodeConfigContainerPluginsSource does not support updates to plugin configurations");
}

private static Map<String, Object> readPlugins(final NodeConfig cfg) {
Map<String, Object> pluginInfos = new HashMap<>();
PluginInfo[] clusterSingletons = cfg.getClusterSingletonPlugins();
if (clusterSingletons != null) {
Arrays.stream(clusterSingletons)
.forEach(
p -> {
Map<String, Object> pluginMap = new HashMap<>();
pluginMap.put("name", p.name);
pluginMap.put("class", p.className);

if (p.initArgs.size() > 0) {
Map<String, Object> config = new HashMap<>();
p.initArgs.toMap(config);
pluginMap.put("config", config);
}

pluginInfos.put(p.name, pluginMap);
});
}
return pluginInfos;
}
}
114 changes: 114 additions & 0 deletions solr/core/src/java/org/apache/solr/api/ZkClusterPluginsSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.api;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.solr.client.solrj.request.beans.PluginMeta;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.admin.ContainerPluginsApi;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;

/**
* The plugin configurations are stored and retrieved from the ZooKeeper cluster properties, stored
* at the {@link ZkStateReader#CONTAINER_PLUGINS} location This supports mutable configurations, and
* management via the /cluster/plugin APIs
*/
public class ZkClusterPluginsSource implements ClusterPluginsSource {

private final Supplier<SolrZkClient> zkClientSupplier;

private final ContainerPluginsApi api;

public ZkClusterPluginsSource(CoreContainer coreContainer) {
this.zkClientSupplier = coreContainer.zkClientSupplier;
this.api = new ContainerPluginsApi(coreContainer, this);
}

@Override
public ContainerPluginsApi.Read getReadApi() {
return api.readAPI;
}

@Override
public ContainerPluginsApi.Edit getEditApi() {
return api.editAPI;
}

/**
* Retrieve the current plugin configurations.
*
* @return current plugin configurations, where keys are plugin names and values are {@link
* PluginMeta} plugin metadata.
* @throws IOException on IO errors
*/
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> plugins() throws IOException {
SolrZkClient zkClient = zkClientSupplier.get();
try {
Map<String, Object> clusterPropsJson =
(Map<String, Object>)
Utils.fromJSON(zkClient.getData(ZkStateReader.CLUSTER_PROPS, null, new Stat(), true));
return Map.copyOf(
(Map<String, Object>)
clusterPropsJson.computeIfAbsent(
ZkStateReader.CONTAINER_PLUGINS, o -> new LinkedHashMap<>()));
} catch (KeeperException.NoNodeException e) {
return new LinkedHashMap<>();
} catch (KeeperException | InterruptedException e) {
throw new IOException("Error reading cluster property", SolrZkClient.checkInterrupted(e));
}
}

@Override
public void persistPlugins(Function<Map<String, Object>, Map<String, Object>> modifier)
throws IOException {
try {
zkClientSupplier
.get()
.atomicUpdate(
ZkStateReader.CLUSTER_PROPS,
bytes -> {
@SuppressWarnings("unchecked")
Map<String, Object> rawJson =
bytes == null
? new LinkedHashMap<>()
: (Map<String, Object>) Utils.fromJSON(bytes);
@SuppressWarnings("unchecked")
Map<String, Object> pluginsModified =
modifier.apply(
(Map<String, Object>)
rawJson.computeIfAbsent(
ZkStateReader.CONTAINER_PLUGINS, o -> new LinkedHashMap<>()));
if (pluginsModified == null) return null;
rawJson.put(ZkStateReader.CONTAINER_PLUGINS, pluginsModified);
return Utils.toJSON(rawJson);
});
} catch (KeeperException | InterruptedException e) {
throw new IOException("Error reading cluster property", SolrZkClient.checkInterrupted(e));
}
}
}