| /* |
| * 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 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 com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| 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.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.PluginBag; |
| 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.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; |
| |
| import static com.google.common.base.Strings.isNullOrEmpty; |
| 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.common.util.Utils.makeMap; |
| 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; |
| |
| 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<String, SolrConfig.SolrPluginInfo> namedPlugins; |
| private Lock reloadLock = new ReentrantLock(true); |
| |
| public Lock getReloadLock() { |
| return reloadLock; |
| } |
| |
| private boolean isImmutableConfigSet = false; |
| |
| static { |
| Map<String, SolrConfig.SolrPluginInfo> 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) { |
| @SuppressWarnings({"rawtypes"}) |
| 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<String> 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)); |
| @SuppressWarnings({"rawtypes"}) |
| Map m = new LinkedHashMap<>(); |
| m.put(ZNODEVER, params.getZnodeVersion()); |
| if (p != null) { |
| m.put(RequestParams.NAME, makeMap(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, Utils.makeMap( |
| ConfigOverlay.NAME, req.getCore().getSolrConfig().getOverlay().getZnodeVersion(), |
| RequestParams.NAME, req.getCore().getSolrConfig().getRequestParams().getZnodeVersion())); |
| boolean isStale = false; |
| int expectedVersion = req.getParams().getInt(ConfigOverlay.NAME, -1); |
| int actualVersion = req.getCore().getSolrConfig().getOverlay().getZnodeVersion(); |
| 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<String, Object> m = getConfigDetails(parts.get(1), req); |
| Map<String, Object> val = makeMap(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) { |
| @SuppressWarnings({"rawtypes"}) |
| Object o = pluginNameVsPluginInfo instanceof MapSerializable ? |
| pluginNameVsPluginInfo: |
| pluginNameVsPluginInfo.get(componentName); |
| @SuppressWarnings({"rawtypes"}) |
| Map pluginInfo = o instanceof MapSerializable? ((MapSerializable) o).toMap(new LinkedHashMap<>()): (Map) o; |
| val.put(parts.get(1),pluginNameVsPluginInfo instanceof PluginInfo? pluginInfo : makeMap(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<PackageListeners.Listener> listeners = req.getCore().getPackageListeners().getListeners(); |
| for (PackageListeners.Listener listener : |
| listeners) { |
| Map<String, PackageAPI.PkgVersion> 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); |
| } |
| } |
| } |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private Map<String, Object> getConfigDetails(String componentType, SolrQueryRequest req) { |
| String componentName = componentType == null ? null : req.getParams().get("componentName"); |
| boolean showParams = req.getParams().getBool("expandParams", false); |
| Map<String, Object> map = this.req.getCore().getSolrConfig().toMap(new LinkedHashMap<>()); |
| if (componentType != null && !SolrRequestHandler.TYPE.equals(componentType)) return map; |
| @SuppressWarnings({"rawtypes"}) |
| Map reqHandlers = (Map) map.get(SolrRequestHandler.TYPE); |
| if (reqHandlers == null) map.put(SolrRequestHandler.TYPE, reqHandlers = new LinkedHashMap<>()); |
| List<PluginInfo> 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 (Object o : reqHandlers.entrySet()) { |
| @SuppressWarnings({"rawtypes"}) |
| Map.Entry e = (Map.Entry) o; |
| if (componentName == null || e.getKey().equals(componentName)) { |
| Map<String, Object> m = expandUseParams(req, e.getValue()); |
| e.setValue(m); |
| } |
| } |
| |
| return map; |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private Map<String, Object> expandUseParams(SolrQueryRequest req, |
| Object plugin) { |
| |
| Map<String, Object> pluginInfo = null; |
| if (plugin instanceof Map) { |
| pluginInfo = (Map) plugin; |
| } else if (plugin instanceof PluginInfo) { |
| pluginInfo = ((PluginInfo) plugin).toMap(new LinkedHashMap<>()); |
| } |
| @SuppressWarnings({"rawtypes"}) |
| String useParams = (String) pluginInfo.get(USEPARAM); |
| String useParamsInReq = req.getOriginalParams().get(USEPARAM); |
| if (useParams != null || useParamsInReq != null) { |
| @SuppressWarnings({"rawtypes"}) |
| Map m = new LinkedHashMap<>(); |
| pluginInfo.put("_useParamsExpanded_", m); |
| List<String> 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); |
| @SuppressWarnings({"rawtypes"}) |
| 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(ImmutableMap.<String, String>builder() |
| .put("componentName", "") |
| .put("expandParams", "") |
| .build()); |
| pluginInfo.put("_effectiveParams_", |
| SolrParams.wrapDefaults(mask, r.getParams())); |
| } |
| return pluginInfo; |
| } |
| |
| |
| private void handlePOST() throws IOException { |
| List<CommandOperation> ops = CommandOperation.readCommands(req.getContentStreams(), resp.getValues()); |
| if (ops == null) return; |
| try { |
| for (; ; ) { |
| ArrayList<CommandOperation> 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<CommandOperation> ops, RequestParams params) { |
| for (CommandOperation op : ops) { |
| switch (op.name) { |
| case SET: |
| case UPDATE: { |
| Map<String, Object> map = op.getDataMap(); |
| if (op.hasError()) break; |
| |
| for (Map.Entry<String, Object> entry : map.entrySet()) { |
| |
| @SuppressWarnings({"rawtypes"}) |
| Map val; |
| String key = entry.getKey(); |
| if (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<String> 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.getCore().getCoreContainer().getZkController(), RequestParams.NAME, latestVersion, 30); |
| } |
| |
| } else { |
| SolrResourceLoader.persistConfLocally(loader, RequestParams.RESOURCE, params.toByteArray()); |
| req.getCore().getSolrConfig().refreshRequestParams(); |
| } |
| |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private void handleCommands(List<CommandOperation> 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<String> 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.getZnodeVersion(), |
| ConfigOverlay.RESOURCE_NAME, overlay.toByteArray(), true); |
| log.debug("Executed config commands successfully and persisted to ZK {}", ops); |
| waitForAllReplicasState(req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(), |
| req.getCore().getCoreContainer().getZkController(), |
| ConfigOverlay.NAME, |
| latestVersion, 30); |
| } else { |
| SolrResourceLoader.persistConfLocally(loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray()); |
| req.getCore().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 (info.clazz == PluginBag.RuntimeLib.class) { |
| if (!PluginBag.RuntimeLib.isEnabled()) { |
| op.addError("Solr not started with -Denable.runtime.lib=true"); |
| return overlay; |
| } |
| try { |
| try (PluginBag.RuntimeLib rtl = new PluginBag.RuntimeLib(req.getCore())) { |
| rtl.init(new PluginInfo(info.tag, op.getDataMap())); |
| } |
| } catch (Exception e) { |
| op.addError(e.getMessage()); |
| log.error("can't load this plugin ", e); |
| 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<PluginInfo> 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); |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private boolean verifyClass(CommandOperation op, String clz, @SuppressWarnings({"rawtypes"})Class expected) { |
| if (clz == null) return true; |
| if (!"true".equals(String.valueOf(op.getStr("runtimeLib", null)))) { |
| PluginInfo info = new PluginInfo(SolrRequestHandler.TYPE, op.getDataMap()); |
| //this is not dynamically loaded so we can verify the class right away |
| 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<String, Object> m = op.getDataMap(); |
| if (op.hasError()) return overlay; |
| for (Map.Entry<String, Object> 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<String> 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<String> 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<String, Object> m = op.getDataMap(); |
| if (op.hasError()) return overlay; |
| for (Map.Entry<String, Object> e : m.entrySet()) { |
| String name = e.getKey(); |
| Object val = e.getValue(); |
| @SuppressWarnings({"rawtypes"}) |
| 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<String> 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 Set<String> cmdPrefixes = ImmutableSet.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<PerReplicaCallable> concurrentTasks = new ArrayList<>(); |
| |
| for (String coreUrl : getActiveReplicaCoreUrls(zkController, collection)) { |
| PerReplicaCallable e = new PerReplicaCallable(coreUrl, 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<Future<Boolean>> results = |
| parallelExecutor.invokeAll(concurrentTasks, maxWaitSecs, TimeUnit.SECONDS); |
| |
| // determine whether all replicas have the update |
| List<String> failedList = null; // lazily init'd |
| for (int f = 0; f < results.size(); f++) { |
| Boolean success = false; |
| Future<Boolean> 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).coreUrl; |
| 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<String> getActiveReplicaCoreUrls(ZkController zkController, |
| String collection) { |
| List<String> activeReplicaCoreUrls = new ArrayList<>(); |
| ClusterState clusterState = zkController.getZkStateReader().getClusterState(); |
| Set<String> liveNodes = clusterState.getLiveNodes(); |
| final DocCollection docCollection = clusterState.getCollectionOrNull(collection); |
| if (docCollection != null && docCollection.getActiveSlices() != null && docCollection.getActiveSlices().size() > 0) { |
| final Collection<Slice> activeSlices = docCollection.getActiveSlices(); |
| for (Slice next : activeSlices) { |
| Map<String, Replica> replicasMap = next.getReplicasMap(); |
| if (replicasMap != null) { |
| for (Map.Entry<String, Replica> entry : replicasMap.entrySet()) { |
| Replica replica = entry.getValue(); |
| if (replica.getState() == Replica.State.ACTIVE && liveNodes.contains(replica.getNodeName())) { |
| activeReplicaCoreUrls.add(replica.getCoreUrl()); |
| } |
| } |
| } |
| } |
| } |
| return activeReplicaCoreUrls; |
| } |
| |
| @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; |
| } |
| } |
| |
| @SuppressWarnings({"rawtypes"}) |
| private static class PerReplicaCallable extends SolrRequest implements Callable<Boolean> { |
| String coreUrl; |
| String prop; |
| int expectedZkVersion; |
| Number remoteVersion = null; |
| int maxWait; |
| |
| PerReplicaCallable(String coreUrl, String prop, int expectedZkVersion, int maxWait) { |
| super(METHOD.GET, "/config/" + ZNODEVER); |
| this.coreUrl = coreUrl; |
| 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(coreUrl).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<Object> resp = solr.httpUriRequest(this).future.get(); |
| if (resp != null) { |
| @SuppressWarnings({"rawtypes"}) |
| Map m = (Map) resp.get(ZNODEVER); |
| if (m != null) { |
| remoteVersion = (Number) m.get(prop); |
| if (remoteVersion != null && remoteVersion.intValue() >= expectedZkVersion) break; |
| } |
| } |
| |
| attempts++; |
| if (log.isInfoEnabled()) { |
| log.info(formatString("Could not get expectedVersion {0} from {1} for prop {2} after {3} attempts", expectedZkVersion, coreUrl, prop, attempts)); |
| } |
| } catch (Exception e) { |
| if (e instanceof InterruptedException) { |
| break; // stop looping |
| } else { |
| log.warn("Failed to get /schema/zkversion from {} due to: ", coreUrl, e); |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| |
| @Override |
| protected SolrResponse createResponse(SolrClient client) { |
| return null; |
| } |
| } |
| |
| @Override |
| public Collection<Api> getApis() { |
| return ApiBag.wrapRequestHandlers(this, |
| "core.config", |
| "core.config.Commands", |
| "core.config.Params", |
| "core.config.Params.Commands"); |
| } |
| |
| @Override |
| public Boolean registerV2() { |
| return Boolean.TRUE; |
| } |
| } |