org.apache.solr.handler.SolrConfigHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of solr-core Show documentation
Show all versions of solr-core Show documentation
Apache Solr (module: core)
/*
* 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.handler;
import static java.util.Collections.singletonList;
import static org.apache.solr.common.params.CoreAdminParams.NAME;
import static org.apache.solr.common.util.StrUtils.formatString;
import static org.apache.solr.core.ConfigOverlay.NOT_EDITABLE;
import static org.apache.solr.core.ConfigOverlay.ZNODEVER;
import static org.apache.solr.core.ConfigSetProperties.IMMUTABLE_CONFIGSET_ARG;
import static org.apache.solr.core.PluginInfo.APPENDS;
import static org.apache.solr.core.PluginInfo.DEFAULTS;
import static org.apache.solr.core.PluginInfo.INVARIANTS;
import static org.apache.solr.core.RequestParams.USEPARAM;
import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_CLASS;
import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME;
import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY;
import static org.apache.solr.schema.FieldType.CLASS_NAME;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
import org.apache.solr.api.ApiBag;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.io.stream.expr.Expressible;
import org.apache.solr.client.solrj.request.CollectionRequiringSolrRequest;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.cloud.ZkSolrResourceLoader;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.ConfigOverlay;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.core.RequestParams;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.handler.admin.api.GetConfigAPI;
import org.apache.solr.handler.admin.api.ModifyConfigComponentAPI;
import org.apache.solr.handler.admin.api.ModifyParamSetAPI;
import org.apache.solr.pkg.PackageAPI;
import org.apache.solr.pkg.PackageListeners;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.schema.SchemaManager;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.util.RTimer;
import org.apache.solr.util.SolrPluginUtils;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SolrConfigHandler extends RequestHandlerBase
implements SolrCoreAware, PermissionNameProvider {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String CONFIGSET_EDITING_DISABLED_ARG = "disable.configEdit";
public static final boolean configEditing_disabled =
Boolean.getBoolean(CONFIGSET_EDITING_DISABLED_ARG);
private static final Map namedPlugins;
private final Lock reloadLock = new ReentrantLock(true);
public Lock getReloadLock() {
return reloadLock;
}
private boolean isImmutableConfigSet = false;
static {
Map map = new HashMap<>();
for (SolrConfig.SolrPluginInfo plugin : SolrConfig.plugins) {
if (plugin.options.contains(REQUIRE_NAME)
|| plugin.options.contains(REQUIRE_NAME_IN_OVERLAY)) {
map.put(plugin.getCleanTag().toLowerCase(Locale.ROOT), plugin);
}
}
namedPlugins = Collections.unmodifiableMap(map);
}
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
RequestHandlerUtils.setWt(req, CommonParams.JSON);
String httpMethod = (String) req.getContext().get("httpMethod");
Command command = new Command(req, rsp, httpMethod);
if ("POST".equals(httpMethod)) {
if (configEditing_disabled || isImmutableConfigSet) {
final String reason =
configEditing_disabled
? "due to " + CONFIGSET_EDITING_DISABLED_ARG
: "because ConfigSet is immutable";
throw new SolrException(
SolrException.ErrorCode.FORBIDDEN, " solrconfig editing is not enabled " + reason);
}
try {
command.handlePOST();
} finally {
RequestHandlerUtils.addExperimentalFormatWarning(rsp);
}
} else {
command.handleGET();
}
}
@Override
public void inform(SolrCore core) {
isImmutableConfigSet = getImmutable(core);
}
public static boolean getImmutable(SolrCore core) {
NamedList configSetProperties = core.getConfigSetProperties();
if (configSetProperties == null) return false;
Object immutable = configSetProperties.get(IMMUTABLE_CONFIGSET_ARG);
return immutable != null && Boolean.parseBoolean(immutable.toString());
}
private class Command {
private final SolrQueryRequest req;
private final SolrQueryResponse resp;
private final String method;
private String path;
List parts;
private Command(SolrQueryRequest req, SolrQueryResponse resp, String httpMethod) {
this.req = req;
this.resp = resp;
this.method = httpMethod;
path = (String) req.getContext().get("path");
if (path == null) path = getDefaultPath();
parts = StrUtils.splitSmart(path, '/', true);
}
private String getDefaultPath() {
return "/config";
}
@SuppressWarnings({"unchecked"})
private void handleGET() {
if (parts.size() == 1) {
// this is the whole config. sent out the whole payload
resp.add("config", getConfigDetails(null, req));
} else {
if (ConfigOverlay.NAME.equals(parts.get(1))) {
resp.add(ConfigOverlay.NAME, req.getCore().getSolrConfig().getOverlay());
} else if (RequestParams.NAME.equals(parts.get(1))) {
if (parts.size() == 3) {
RequestParams params = req.getCore().getSolrConfig().getRequestParams();
RequestParams.ParamSet p = params.getParams(parts.get(2));
Map m = new LinkedHashMap<>();
m.put(ZNODEVER, params.getZnodeVersion());
if (p != null) {
m.put(RequestParams.NAME, Map.of(parts.get(2), p.toMap(new LinkedHashMap<>())));
}
resp.add(SolrQueryResponse.NAME, m);
} else {
resp.add(SolrQueryResponse.NAME, req.getCore().getSolrConfig().getRequestParams());
}
} else {
if (ZNODEVER.equals(parts.get(1))) {
resp.add(
ZNODEVER,
Map.of(
ConfigOverlay.NAME,
req.getCore().getSolrConfig().getOverlay().getVersion(),
RequestParams.NAME,
req.getCore().getSolrConfig().getRequestParams().getZnodeVersion()));
boolean isStale = false;
int expectedVersion = req.getParams().getInt(ConfigOverlay.NAME, -1);
int actualVersion = req.getCore().getSolrConfig().getOverlay().getVersion();
if (expectedVersion > actualVersion) {
log.info(
"expecting overlay version {} but my version is {}",
expectedVersion,
actualVersion);
isStale = true;
} else if (expectedVersion != -1) {
log.info("I already have the expected version {} of config", expectedVersion);
}
expectedVersion = req.getParams().getInt(RequestParams.NAME, -1);
actualVersion = req.getCore().getSolrConfig().getRequestParams().getZnodeVersion();
if (expectedVersion > actualVersion) {
log.info(
"expecting params version {} but my version is {}",
expectedVersion,
actualVersion);
isStale = true;
} else if (expectedVersion != -1) {
log.info("I already have the expected version {} of params", expectedVersion);
}
if (isStale && req.getCore().getResourceLoader() instanceof ZkSolrResourceLoader) {
new Thread(
() -> {
if (!reloadLock.tryLock()) {
log.info("Another reload is in progress . Not doing anything");
return;
}
try {
log.info("Trying to update my configs");
SolrCore.getConfListener(
req.getCore(),
(ZkSolrResourceLoader) req.getCore().getResourceLoader())
.run();
} catch (Exception e) {
log.error("Unable to refresh conf ", e);
} finally {
reloadLock.unlock();
}
},
SolrConfigHandler.class.getSimpleName() + "-refreshconf")
.start();
} else {
if (log.isInfoEnabled()) {
log.info(
"isStale {} , resourceloader {}",
isStale,
req.getCore().getResourceLoader().getClass().getName());
}
}
} else {
Map m = getConfigDetails(parts.get(1), req);
Map val = new LinkedHashMap<>();
val.put(parts.get(1), m.get(parts.get(1)));
String componentName = req.getParams().get("componentName");
if (componentName != null) {
@SuppressWarnings({"rawtypes"})
Map pluginNameVsPluginInfo = (Map) val.get(parts.get(1));
if (pluginNameVsPluginInfo != null) {
Object o =
pluginNameVsPluginInfo instanceof MapSerializable
? pluginNameVsPluginInfo
: pluginNameVsPluginInfo.get(componentName);
Map pluginInfo =
o instanceof MapSerializable
? ((MapSerializable) o).toMap(new LinkedHashMap<>())
: (Map) o;
val.put(
parts.get(1),
pluginNameVsPluginInfo instanceof PluginInfo
? pluginInfo
: Map.of(componentName, pluginInfo));
if (req.getParams().getBool("meta", false)) {
// meta=true is asking for the package info of the plugin
// We go through all the listeners and see if there is one registered for this
// plugin
List listeners =
req.getCore().getPackageListeners().getListeners();
for (PackageListeners.Listener listener : listeners) {
Map infos = listener.packageDetails();
if (infos == null || infos.isEmpty()) continue;
infos.forEach(
(s, mapWriter) -> {
if (s.equals(pluginInfo.get("class"))) {
(pluginInfo).put("_packageinfo_", mapWriter);
}
});
}
}
}
}
resp.add("config", val);
}
}
}
}
private Map getConfigDetails(String componentType, SolrQueryRequest req) {
String componentName = componentType == null ? null : req.getParams().get("componentName");
boolean showParams = req.getParams().getBool("expandParams", false);
Map map = this.req.getCore().getSolrConfig().toMap(new LinkedHashMap<>());
if (componentType != null && !SolrRequestHandler.TYPE.equals(componentType)) return map;
@SuppressWarnings({"unchecked"})
Map reqHandlers =
(Map)
map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>());
List plugins = this.req.getCore().getImplicitHandlers();
for (PluginInfo plugin : plugins) {
if (SolrRequestHandler.TYPE.equals(plugin.type)) {
if (!reqHandlers.containsKey(plugin.name)) {
reqHandlers.put(plugin.name, plugin);
}
}
}
if (!showParams) return map;
for (Map.Entry e : reqHandlers.entrySet()) {
if (componentName == null || e.getKey().equals(componentName)) {
Map m = expandUseParams(req, e.getValue());
e.setValue(m);
}
}
return map;
}
@SuppressWarnings({"unchecked"})
private Map expandUseParams(SolrQueryRequest req, Object plugin) {
Map pluginInfo = null;
if (plugin instanceof Map) {
pluginInfo = (Map) plugin;
} else if (plugin instanceof PluginInfo) {
pluginInfo = ((PluginInfo) plugin).toMap(new LinkedHashMap<>());
}
String useParams = (String) pluginInfo.get(USEPARAM);
String useParamsInReq = req.getOriginalParams().get(USEPARAM);
if (useParams != null || useParamsInReq != null) {
Map m = new LinkedHashMap<>();
pluginInfo.put("_useParamsExpanded_", m);
List params = new ArrayList<>();
if (useParams != null) params.addAll(StrUtils.splitSmart(useParams, ','));
if (useParamsInReq != null) params.addAll(StrUtils.splitSmart(useParamsInReq, ','));
for (String param : params) {
RequestParams.ParamSet p =
this.req.getCore().getSolrConfig().getRequestParams().getParams(param);
if (p != null) {
m.put(param, p);
} else {
m.put(param, "[NOT AVAILABLE]");
}
}
LocalSolrQueryRequest r = new LocalSolrQueryRequest(req.getCore(), req.getOriginalParams());
r.getContext().put(USEPARAM, useParams);
NamedList nl = new PluginInfo(SolrRequestHandler.TYPE, pluginInfo).initArgs;
SolrPluginUtils.setDefaults(
r,
getSolrParamsFromNamedList(nl, DEFAULTS),
getSolrParamsFromNamedList(nl, APPENDS),
getSolrParamsFromNamedList(nl, INVARIANTS));
// SolrParams.wrapDefaults(maskUseParams, req.getParams())
MapSolrParams mask = new MapSolrParams(Map.of("componentName", "", "expandParams", ""));
pluginInfo.put("_effectiveParams_", SolrParams.wrapDefaults(mask, r.getParams()));
}
return pluginInfo;
}
private void handlePOST() throws IOException {
List ops =
CommandOperation.readCommands(req.getContentStreams(), resp.getValues());
if (ops == null) return;
try {
for (; ; ) {
ArrayList opsCopy = new ArrayList<>(ops.size());
for (CommandOperation op : ops) opsCopy.add(op.getCopy());
try {
if (parts.size() > 1 && RequestParams.NAME.equals(parts.get(1))) {
RequestParams params =
RequestParams.getFreshRequestParams(
req.getCore().getResourceLoader(),
req.getCore().getSolrConfig().getRequestParams());
handleParams(opsCopy, params);
} else {
ConfigOverlay overlay =
SolrConfig.getConfigOverlay(req.getCore().getResourceLoader());
handleCommands(opsCopy, overlay);
}
break; // succeeded . so no need to go over the loop again
} catch (ZkController.ResourceModifiedInZkException e) {
// retry
if (log.isInfoEnabled()) {
log.info("Race condition, the node is modified in ZK by someone else", e);
}
}
}
} catch (Exception e) {
resp.setException(e);
resp.add(CommandOperation.ERR_MSGS, singletonList(SchemaManager.getErrorStr(e)));
}
}
@SuppressWarnings({"unchecked"})
private void handleParams(ArrayList ops, RequestParams params) {
for (CommandOperation op : ops) {
switch (op.name) {
case SET:
case UPDATE:
{
Map map = op.getDataMap();
if (op.hasError()) break;
for (Map.Entry entry : map.entrySet()) {
@SuppressWarnings({"rawtypes"})
Map val;
String key = entry.getKey();
if (StrUtils.isNullOrEmpty(key)) {
op.addError("null key ");
continue;
}
key = key.trim();
String err = validateName(key);
if (err != null) {
op.addError(err);
continue;
}
try {
val = (Map) entry.getValue();
} catch (Exception e1) {
op.addError("invalid params for key : " + key);
continue;
}
if (val.containsKey("")) {
op.addError("Empty keys are not allowed in params");
continue;
}
RequestParams.ParamSet old = params.getParams(key);
if (op.name.equals(UPDATE)) {
if (old == null) {
op.addError(formatString("unknown paramset {0} cannot update ", key));
continue;
}
params = params.setParams(key, old.update(val));
} else {
Long version = old == null ? 0 : old.getVersion() + 1;
params = params.setParams(key, RequestParams.createParamSet(val, version));
}
}
break;
}
case "delete":
{
List name = op.getStrs(CommandOperation.ROOT_OBJ);
if (op.hasError()) break;
for (String s : name) {
if (params.getParams(s) == null) {
op.addError(formatString("Could not delete. No such params ''{0}'' exist", s));
}
params = params.setParams(s, null);
}
break;
}
default:
{
op.unknownOperation();
}
}
}
@SuppressWarnings({"rawtypes"})
List errs = CommandOperation.captureErrors(ops);
if (!errs.isEmpty()) {
throw new ApiBag.ExceptionWithErrObject(
SolrException.ErrorCode.BAD_REQUEST, "error processing params", errs);
}
SolrResourceLoader loader = req.getCore().getResourceLoader();
if (loader instanceof ZkSolrResourceLoader) {
ZkSolrResourceLoader zkLoader = (ZkSolrResourceLoader) loader;
if (ops.isEmpty()) {
ZkController.touchConfDir(zkLoader);
} else {
if (log.isDebugEnabled()) {
log.debug(
"persisting params data : {}",
Utils.toJSONString(params.toMap(new LinkedHashMap<>())));
}
int latestVersion =
ZkController.persistConfigResourceToZooKeeper(
zkLoader,
params.getZnodeVersion(),
RequestParams.RESOURCE,
params.toByteArray(),
true);
log.debug("persisted to version : {} ", latestVersion);
waitForAllReplicasState(
req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
req.getCoreContainer().getZkController(),
RequestParams.NAME,
latestVersion,
30);
}
} else {
SolrResourceLoader.persistConfLocally(loader, RequestParams.RESOURCE, params.toByteArray());
req.getCore().getSolrConfig().refreshRequestParams();
}
}
@SuppressWarnings({"unchecked"})
private void handleCommands(List ops, ConfigOverlay overlay)
throws IOException {
for (CommandOperation op : ops) {
switch (op.name) {
case SET_PROPERTY:
overlay = applySetProp(op, overlay);
break;
case UNSET_PROPERTY:
overlay = applyUnset(op, overlay);
break;
case SET_USER_PROPERTY:
overlay = applySetUserProp(op, overlay);
break;
case UNSET_USER_PROPERTY:
overlay = applyUnsetUserProp(op, overlay);
break;
default:
{
List pcs = StrUtils.splitSmart(op.name.toLowerCase(Locale.ROOT), '-');
if (pcs.size() != 2) {
op.addError(formatString("Unknown operation ''{0}'' ", op.name));
} else {
String prefix = pcs.get(0);
String name = pcs.get(1);
if (cmdPrefixes.contains(prefix) && namedPlugins.containsKey(name)) {
SolrConfig.SolrPluginInfo info = namedPlugins.get(name);
if ("delete".equals(prefix)) {
overlay = deleteNamedComponent(op, overlay, info.getCleanTag());
} else {
overlay =
updateNamedPlugin(
info, op, overlay, prefix.equals("create") || prefix.equals("add"));
}
} else {
op.unknownOperation();
}
}
}
}
}
@SuppressWarnings({"rawtypes"})
List errs = CommandOperation.captureErrors(ops);
if (!errs.isEmpty()) {
log.error("ERROR:{}", Utils.toJSONString(errs));
throw new ApiBag.ExceptionWithErrObject(
SolrException.ErrorCode.BAD_REQUEST, "error processing commands", errs);
}
SolrResourceLoader loader = req.getCore().getResourceLoader();
if (loader instanceof ZkSolrResourceLoader) {
int latestVersion =
ZkController.persistConfigResourceToZooKeeper(
(ZkSolrResourceLoader) loader,
overlay.getVersion(),
ConfigOverlay.RESOURCE_NAME,
overlay.toByteArray(),
true);
log.debug("Executed config commands successfully and persisted to ZK {}", ops);
waitForAllReplicasState(
req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
req.getCoreContainer().getZkController(),
ConfigOverlay.NAME,
latestVersion,
30);
} else {
SolrResourceLoader.persistConfLocally(
loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray());
req.getCoreContainer().reload(req.getCore().getName(), req.getCore().uniqueId);
log.info("Executed config commands successfully and persisted to File System {}", ops);
}
}
private ConfigOverlay deleteNamedComponent(
CommandOperation op, ConfigOverlay overlay, String typ) {
String name = op.getStr(CommandOperation.ROOT_OBJ);
if (op.hasError()) return overlay;
if (overlay.getNamedPlugins(typ).containsKey(name)) {
return overlay.deleteNamedPlugin(name, typ);
} else {
op.addError(formatString("NO such {0} ''{1}'' ", typ, name));
return overlay;
}
}
private ConfigOverlay updateNamedPlugin(
SolrConfig.SolrPluginInfo info,
CommandOperation op,
ConfigOverlay overlay,
boolean isCeate) {
String name = op.getStr(NAME);
String clz =
info.options.contains(REQUIRE_CLASS)
? op.getStr(CLASS_NAME)
: op.getStr(CLASS_NAME, null);
op.getMap(DEFAULTS, null);
op.getMap(PluginInfo.INVARIANTS, null);
op.getMap(PluginInfo.APPENDS, null);
if (op.hasError()) return overlay;
if (!verifyClass(op, clz, info.clazz)) return overlay;
if (pluginExists(info, overlay, name)) {
if (isCeate) {
op.addError(
formatString(
" ''{0}'' already exists . Do an ''{1}'' , if you want to change it ",
name, "update-" + info.getTagCleanLower()));
return overlay;
} else {
return overlay.addNamedPlugin(op.getDataMap(), info.getCleanTag());
}
} else {
if (isCeate) {
return overlay.addNamedPlugin(op.getDataMap(), info.getCleanTag());
} else {
op.addError(
formatString(
" ''{0}'' does not exist . Do an ''{1}'' , if you want to create it ",
name, "create-" + info.getTagCleanLower()));
return overlay;
}
}
}
private boolean pluginExists(
SolrConfig.SolrPluginInfo info, ConfigOverlay overlay, String name) {
List l = req.getCore().getSolrConfig().getPluginInfos(info.clazz.getName());
for (PluginInfo pluginInfo : l) if (name.equals(pluginInfo.name)) return true;
return overlay.getNamedPlugins(info.getCleanTag()).containsKey(name);
}
private boolean verifyClass(CommandOperation op, String clz, Class expected) {
if (clz == null) return true;
PluginInfo info = new PluginInfo(SolrRequestHandler.TYPE, op.getDataMap());
try {
if (expected == Expressible.class) {
@SuppressWarnings("resource")
SolrResourceLoader resourceLoader =
info.pkgName == null
? req.getCore().getResourceLoader()
: req.getCore().getResourceLoader(info.pkgName);
resourceLoader.findClass(info.className, expected);
} else {
req.getCore().createInitInstance(info, expected, clz, "");
}
} catch (Exception e) {
log.error("Error checking plugin : ", e);
op.addError(e.getMessage());
return false;
}
return true;
}
private ConfigOverlay applySetUserProp(CommandOperation op, ConfigOverlay overlay) {
Map m = op.getDataMap();
if (op.hasError()) return overlay;
for (Map.Entry e : m.entrySet()) {
String name = e.getKey();
Object val = e.getValue();
overlay = overlay.setUserProperty(name, val);
}
return overlay;
}
private ConfigOverlay applyUnsetUserProp(CommandOperation op, ConfigOverlay overlay) {
List name = op.getStrs(CommandOperation.ROOT_OBJ);
if (op.hasError()) return overlay;
for (String o : name) {
if (!overlay.getUserProps().containsKey(o)) {
op.addError(formatString("No such property ''{0}''", name));
} else {
overlay = overlay.unsetUserProperty(o);
}
}
return overlay;
}
private ConfigOverlay applyUnset(CommandOperation op, ConfigOverlay overlay) {
List name = op.getStrs(CommandOperation.ROOT_OBJ);
if (op.hasError()) return overlay;
for (String o : name) {
if (!ConfigOverlay.isEditableProp(o, false, null)) {
op.addError(formatString(NOT_EDITABLE, name));
} else {
overlay = overlay.unsetProperty(o);
}
}
return overlay;
}
private ConfigOverlay applySetProp(CommandOperation op, ConfigOverlay overlay) {
Map m = op.getDataMap();
if (op.hasError()) return overlay;
for (Map.Entry e : m.entrySet()) {
String name = e.getKey();
Object val = e.getValue();
Class typ = ConfigOverlay.checkEditable(name, false, null);
if (typ == null) {
op.addError(formatString(NOT_EDITABLE, name));
continue;
}
if (val != null) {
if (typ == String.class) val = val.toString();
String typeErr = "Property {0} must be of {1} type ";
if (typ == Boolean.class) {
try {
val = Boolean.parseBoolean(val.toString());
} catch (Exception exp) {
op.addError(formatString(typeErr, name, typ.getSimpleName()));
continue;
}
} else if (typ == Integer.class) {
try {
val = Integer.parseInt(val.toString());
} catch (Exception exp) {
op.addError(formatString(typeErr, name, typ.getSimpleName()));
continue;
}
} else if (typ == Float.class) {
try {
val = Float.parseFloat(val.toString());
} catch (Exception exp) {
op.addError(formatString(typeErr, name, typ.getSimpleName()));
continue;
}
}
}
overlay = overlay.setProperty(name, val);
}
return overlay;
}
}
public static String validateName(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if ((c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| c == '_'
|| c == '-'
|| c == '.') continue;
else {
return formatString("''{0}'' name should only have chars [a-zA-Z_-.0-9] ", s);
}
}
return null;
}
@Override
public SolrRequestHandler getSubHandler(String path) {
if (subPaths.contains(path)) return this;
if (path.startsWith("/params/")) return this;
return null;
}
private static Set subPaths =
new HashSet<>(
Arrays.asList(
"/overlay",
"/params",
"/updateHandler",
"/query",
"/jmx",
"/requestDispatcher",
"/znodeVersion"));
static {
for (SolrConfig.SolrPluginInfo solrPluginInfo : SolrConfig.plugins)
subPaths.add("/" + solrPluginInfo.getCleanTag());
}
//////////////////////// SolrInfoMBeans methods //////////////////////
@Override
public String getDescription() {
return "Edit solrconfig.xml";
}
@Override
public Category getCategory() {
return Category.ADMIN;
}
public static final String SET_PROPERTY = "set-property";
public static final String UNSET_PROPERTY = "unset-property";
public static final String SET_USER_PROPERTY = "set-user-property";
public static final String UNSET_USER_PROPERTY = "unset-user-property";
public static final String SET = "set";
public static final String UPDATE = "update";
public static final String CREATE = "create";
private static final Set cmdPrefixes = Set.of(CREATE, UPDATE, "delete", "add");
/**
* Block up to a specified maximum time until we see agreement on the schema version in ZooKeeper
* across all replicas for a collection.
*/
private static void waitForAllReplicasState(
String collection,
ZkController zkController,
String prop,
int expectedVersion,
int maxWaitSecs) {
final RTimer timer = new RTimer();
// get a list of active replica cores to query for the schema zk version (skipping this core of
// course)
List concurrentTasks = new ArrayList<>();
for (Replica replica : getActiveReplicas(zkController, collection)) {
PerReplicaCallable e = new PerReplicaCallable(replica, prop, expectedVersion, maxWaitSecs);
concurrentTasks.add(e);
}
if (concurrentTasks.isEmpty()) return; // nothing to wait for ...
if (log.isInfoEnabled()) {
log.info(
formatString(
"Waiting up to {0} secs for {1} replicas to set the property {2} to be of version {3} for collection {4}",
maxWaitSecs, concurrentTasks.size(), prop, expectedVersion, collection));
}
// use an executor service to invoke schema zk version requests in parallel with a max wait time
int poolSize = Math.min(concurrentTasks.size(), 10);
ExecutorService parallelExecutor =
ExecutorUtil.newMDCAwareFixedThreadPool(
poolSize, new SolrNamedThreadFactory("solrHandlerExecutor"));
try {
List> results =
parallelExecutor.invokeAll(concurrentTasks, maxWaitSecs, TimeUnit.SECONDS);
// determine whether all replicas have the update
List failedList = null; // lazily init'd
for (int f = 0; f < results.size(); f++) {
Boolean success = false;
Future next = results.get(f);
if (next.isDone() && !next.isCancelled()) {
// looks to have finished, but need to check if it succeeded
try {
success = next.get();
} catch (ExecutionException e) {
// shouldn't happen since we checked isCancelled
}
}
if (!success) {
String coreUrl = concurrentTasks.get(f).replica.getCoreUrl();
log.warn("Core {} could not get the expected version {}", coreUrl, expectedVersion);
if (failedList == null) failedList = new ArrayList<>();
failedList.add(coreUrl);
}
}
// if any tasks haven't completed within the specified timeout, it's an error
if (failedList != null)
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR,
formatString(
"{0} out of {1} the property {2} to be of version {3} within {4} seconds! Failed cores: {5}",
failedList.size(),
concurrentTasks.size() + 1,
prop,
expectedVersion,
maxWaitSecs,
failedList));
} catch (InterruptedException ie) {
log.warn(
formatString(
"Core was interrupted . trying to set the property {1} to version {2} to propagate to {3} replicas for collection {4}",
prop, expectedVersion, concurrentTasks.size(), collection));
Thread.currentThread().interrupt();
} finally {
ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
}
if (log.isInfoEnabled()) {
log.info(
"Took {}ms to set the property {} to be of version {} for collection {}",
timer.getTime(),
prop,
expectedVersion,
collection);
}
}
public static List getActiveReplicas(ZkController zkController, String collection) {
List activeReplicas = new ArrayList<>();
ClusterState clusterState = zkController.getZkStateReader().getClusterState();
Set liveNodes = clusterState.getLiveNodes();
final DocCollection docCollection = clusterState.getCollectionOrNull(collection);
if (docCollection != null
&& docCollection.getActiveSlices() != null
&& docCollection.getActiveSlices().size() > 0) {
final Collection activeSlices = docCollection.getActiveSlices();
for (Slice next : activeSlices) {
Map replicasMap = next.getReplicasMap();
if (replicasMap != null) {
for (Map.Entry entry : replicasMap.entrySet()) {
Replica replica = entry.getValue();
if (replica.getState() == Replica.State.ACTIVE
&& liveNodes.contains(replica.getNodeName())) {
activeReplicas.add(replica);
}
}
}
}
}
return activeReplicas;
}
@Override
public Name getPermissionName(AuthorizationContext ctx) {
switch (ctx.getHttpMethod()) {
case "GET":
return Name.CONFIG_READ_PERM;
case "POST":
return Name.CONFIG_EDIT_PERM;
default:
return null;
}
}
private static class PerReplicaCallable extends CollectionRequiringSolrRequest
implements Callable {
Replica replica;
String prop;
int expectedZkVersion;
Number remoteVersion = null;
int maxWait;
PerReplicaCallable(Replica replica, String prop, int expectedZkVersion, int maxWait) {
super(METHOD.GET, "/config/" + ZNODEVER);
this.replica = replica;
this.expectedZkVersion = expectedZkVersion;
this.prop = prop;
this.maxWait = maxWait;
}
@Override
public SolrParams getParams() {
return new ModifiableSolrParams()
.set(prop, expectedZkVersion)
.set(CommonParams.WT, CommonParams.JAVABIN);
}
@Override
public Boolean call() throws Exception {
final RTimer timer = new RTimer();
int attempts = 0;
try (HttpSolrClient solr =
new HttpSolrClient.Builder(replica.getBaseUrl())
.withDefaultCollection(replica.getCoreName())
.build()) {
// eventually, this loop will get killed by the ExecutorService's timeout
while (true) {
try {
long timeElapsed = (long) timer.getTime() / 1000;
if (timeElapsed >= maxWait) {
return false;
}
log.info("Time elapsed : {} secs, maxWait {}", timeElapsed, maxWait);
Thread.sleep(100);
NamedList