blob: 0ef3ebbd999d11258f169b8e39ae840da9b2a136 [file] [log] [blame]
/*
* 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.admin;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.api.Api;
import org.apache.solr.cloud.CloudDescriptor;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonAdminParams;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.metrics.SolrMetricsContext;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.util.stats.MetricUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import static org.apache.solr.common.params.CoreAdminParams.ACTION;
import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.STATUS;
import static org.apache.solr.security.PermissionNameProvider.Name.CORE_EDIT_PERM;
import static org.apache.solr.security.PermissionNameProvider.Name.CORE_READ_PERM;
/**
*
* @since solr 1.3
*/
public class CoreAdminHandler extends RequestHandlerBase implements PermissionNameProvider {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
protected final CoreContainer coreContainer;
protected final Map<String, Map<String, TaskObject>> requestStatusMap;
private final CoreAdminHandlerApi coreAdminHandlerApi;
protected ExecutorService parallelExecutor = ExecutorUtil.newMDCAwareFixedThreadPool(50,
new SolrNamedThreadFactory("parallelCoreAdminExecutor"));
protected static int MAX_TRACKED_REQUESTS = 100;
public static String RUNNING = "running";
public static String COMPLETED = "completed";
public static String FAILED = "failed";
public static String RESPONSE = "Response";
public static String RESPONSE_STATUS = "STATUS";
public static String RESPONSE_MESSAGE = "msg";
public CoreAdminHandler() {
super();
// Unlike most request handlers, CoreContainer initialization
// should happen in the constructor...
this.coreContainer = null;
HashMap<String, Map<String, TaskObject>> map = new HashMap<>(3, 1.0f);
map.put(RUNNING, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
map.put(COMPLETED, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
map.put(FAILED, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
requestStatusMap = Collections.unmodifiableMap(map);
coreAdminHandlerApi = new CoreAdminHandlerApi(this);
}
/**
* Overloaded ctor to inject CoreContainer into the handler.
*
* @param coreContainer Core Container of the solr webapp installed.
*/
public CoreAdminHandler(final CoreContainer coreContainer) {
this.coreContainer = coreContainer;
HashMap<String, Map<String, TaskObject>> map = new HashMap<>(3, 1.0f);
map.put(RUNNING, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
map.put(COMPLETED, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
map.put(FAILED, Collections.synchronizedMap(new LinkedHashMap<String, TaskObject>()));
requestStatusMap = Collections.unmodifiableMap(map);
coreAdminHandlerApi = new CoreAdminHandlerApi(this);
}
@Override
final public void init(@SuppressWarnings({"rawtypes"})NamedList args) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"CoreAdminHandler should not be configured in solrconf.xml\n" +
"it is a special Handler configured directly by the RequestDispatcher");
}
@Override
public void initializeMetrics(SolrMetricsContext parentContext, String scope) {
super.initializeMetrics(parentContext, scope);
parallelExecutor = MetricUtils.instrumentedExecutorService(parallelExecutor, this, solrMetricsContext.getMetricRegistry(),
SolrMetricManager.mkName("parallelCoreAdminExecutor", getCategory().name(), scope, "threadPool"));
}
@Override
public Boolean registerV2() {
return Boolean.TRUE;
}
/**
* The instance of CoreContainer this handler handles. This should be the CoreContainer instance that created this
* handler.
*
* @return a CoreContainer instance
*/
public CoreContainer getCoreContainer() {
return this.coreContainer;
}
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
// Make sure the cores is enabled
try {
CoreContainer cores = getCoreContainer();
if (cores == null) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Core container instance missing");
}
//boolean doPersist = false;
final String taskId = req.getParams().get(CommonAdminParams.ASYNC);
final TaskObject taskObject = new TaskObject(taskId);
if(taskId != null) {
// Put the tasks into the maps for tracking
if (getRequestStatusMap(RUNNING).containsKey(taskId) || getRequestStatusMap(COMPLETED).containsKey(taskId) || getRequestStatusMap(FAILED).containsKey(taskId)) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Duplicate request with the same requestid found.");
}
addTask(RUNNING, taskObject);
}
// Pick the action
CoreAdminOperation op = opMap.get(req.getParams().get(ACTION, STATUS.toString()).toLowerCase(Locale.ROOT));
if (op == null) {
handleCustomAction(req, rsp);
return;
}
final CallInfo callInfo = new CallInfo(this, req, rsp, op);
String coreName = req.getParams().get(CoreAdminParams.CORE);
if (coreName == null) {
coreName = req.getParams().get(CoreAdminParams.NAME);
}
MDCLoggingContext.setCoreName(coreName);
if (taskId == null) {
callInfo.call();
} else {
try {
MDC.put("CoreAdminHandler.asyncId", taskId);
MDC.put("CoreAdminHandler.action", op.action.toString());
parallelExecutor.execute(() -> {
boolean exceptionCaught = false;
try {
callInfo.call();
taskObject.setRspObject(callInfo.rsp);
} catch (Exception e) {
exceptionCaught = true;
taskObject.setRspObjectFromException(e);
} finally {
removeTask("running", taskObject.taskId);
if (exceptionCaught) {
addTask("failed", taskObject, true);
} else {
addTask("completed", taskObject, true);
}
}
});
} finally {
MDC.remove("CoreAdminHandler.asyncId");
MDC.remove("CoreAdminHandler.action");
}
}
} finally {
rsp.setHttpCaching(false);
}
}
/**
* Handle Custom Action.
* <p>
* This method could be overridden by derived classes to handle custom actions. <br> By default - this method throws a
* solr exception. Derived classes are free to write their derivation if necessary.
*/
protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unsupported operation: " +
req.getParams().get(ACTION));
}
public static ImmutableMap<String, String> paramToProp = ImmutableMap.<String, String>builder()
.put(CoreAdminParams.CONFIG, CoreDescriptor.CORE_CONFIG)
.put(CoreAdminParams.SCHEMA, CoreDescriptor.CORE_SCHEMA)
.put(CoreAdminParams.DATA_DIR, CoreDescriptor.CORE_DATADIR)
.put(CoreAdminParams.ULOG_DIR, CoreDescriptor.CORE_ULOGDIR)
.put(CoreAdminParams.CONFIGSET, CoreDescriptor.CORE_CONFIGSET)
.put(CoreAdminParams.LOAD_ON_STARTUP, CoreDescriptor.CORE_LOADONSTARTUP)
.put(CoreAdminParams.TRANSIENT, CoreDescriptor.CORE_TRANSIENT)
.put(CoreAdminParams.SHARD, CoreDescriptor.CORE_SHARD)
.put(CoreAdminParams.COLLECTION, CoreDescriptor.CORE_COLLECTION)
.put(CoreAdminParams.ROLES, CoreDescriptor.CORE_ROLES)
.put(CoreAdminParams.CORE_NODE_NAME, CoreDescriptor.CORE_NODE_NAME)
.put(ZkStateReader.NUM_SHARDS_PROP, CloudDescriptor.NUM_SHARDS)
.put(CoreAdminParams.REPLICA_TYPE, CloudDescriptor.REPLICA_TYPE)
.build();
protected static Map<String, String> buildCoreParams(SolrParams params) {
Map<String, String> coreParams = new HashMap<>();
// standard core create parameters
for (Map.Entry<String, String> entry : paramToProp.entrySet()) {
String value = params.get(entry.getKey(), null);
if (StringUtils.isNotEmpty(value)) {
coreParams.put(entry.getValue(), value);
}
}
// extra properties
Iterator<String> paramsIt = params.getParameterNamesIterator();
while (paramsIt.hasNext()) {
String param = paramsIt.next();
if (param.startsWith(CoreAdminParams.PROPERTY_PREFIX)) {
String propName = param.substring(CoreAdminParams.PROPERTY_PREFIX.length());
String propValue = params.get(param);
coreParams.put(propName, propValue);
}
if (param.startsWith(ZkController.COLLECTION_PARAM_PREFIX)) {
coreParams.put(param, params.get(param));
}
}
return coreParams;
}
protected static String normalizePath(String path) {
if (path == null)
return null;
path = path.replace('/', File.separatorChar);
path = path.replace('\\', File.separatorChar);
return path;
}
public static ModifiableSolrParams params(String... params) {
ModifiableSolrParams msp = new ModifiableSolrParams();
for (int i=0; i<params.length; i+=2) {
msp.add(params[i], params[i+1]);
}
return msp;
}
//////////////////////// SolrInfoMBeans methods //////////////////////
@Override
public String getDescription() {
return "Manage Multiple Solr Cores";
}
@Override
public Category getCategory() {
return Category.ADMIN;
}
@Override
public Name getPermissionName(AuthorizationContext ctx) {
String action = ctx.getParams().get(CoreAdminParams.ACTION);
if (action == null) return CORE_READ_PERM;
CoreAdminParams.CoreAdminAction coreAction = CoreAdminParams.CoreAdminAction.get(action);
if (coreAction == null) return CORE_READ_PERM;
return coreAction.isRead ?
CORE_READ_PERM :
CORE_EDIT_PERM;
}
/**
* Helper class to manage the tasks to be tracked.
* This contains the taskId, request and the response (if available).
*/
static class TaskObject {
String taskId;
String rspInfo;
public TaskObject(String taskId) {
this.taskId = taskId;
}
public String getRspObject() {
return rspInfo;
}
public void setRspObject(SolrQueryResponse rspObject) {
this.rspInfo = rspObject.getToLogAsString("TaskId: " + this.taskId);
}
public void setRspObjectFromException(Exception e) {
this.rspInfo = e.getMessage();
}
}
/**
* Helper method to add a task to a tracking type.
*/
void addTask(String type, TaskObject o, boolean limit) {
synchronized (getRequestStatusMap(type)) {
if(limit && getRequestStatusMap(type).size() == MAX_TRACKED_REQUESTS) {
String key = getRequestStatusMap(type).entrySet().iterator().next().getKey();
getRequestStatusMap(type).remove(key);
}
addTask(type, o);
}
}
private void addTask(String type, TaskObject o) {
synchronized (getRequestStatusMap(type)) {
getRequestStatusMap(type).put(o.taskId, o);
}
}
/**
* Helper method to remove a task from a tracking map.
*/
private void removeTask(String map, String taskId) {
synchronized (getRequestStatusMap(map)) {
getRequestStatusMap(map).remove(taskId);
}
}
/**
* Helper method to get a request status map given the name.
*/
Map<String, TaskObject> getRequestStatusMap(String key) {
return requestStatusMap.get(key);
}
/**
* Method to ensure shutting down of the ThreadPool Executor.
*/
public void shutdown() {
if (parallelExecutor != null)
ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
}
private static final Map<String, CoreAdminOperation> opMap = new HashMap<>();
static class CallInfo {
final CoreAdminHandler handler;
final SolrQueryRequest req;
final SolrQueryResponse rsp;
final CoreAdminOperation op;
CallInfo(CoreAdminHandler handler, SolrQueryRequest req, SolrQueryResponse rsp, CoreAdminOperation op) {
this.handler = handler;
this.req = req;
this.rsp = rsp;
this.op = op;
}
void call() throws Exception {
op.execute(this);
}
}
@Override
public Collection<Api> getApis() {
return coreAdminHandlerApi.getApis();
}
static {
for (CoreAdminOperation op : CoreAdminOperation.values())
opMap.put(op.action.toString().toLowerCase(Locale.ROOT), op);
}
/**
* used by the INVOKE action of core admin handler
*/
public interface Invocable {
Map<String, Object> invoke(SolrQueryRequest req);
}
interface CoreAdminOp {
/**
* @param it request/response object
*
* If the request is invalid throw a SolrException with SolrException.ErrorCode.BAD_REQUEST ( 400 )
* If the execution of the command fails throw a SolrException with SolrException.ErrorCode.SERVER_ERROR ( 500 )
*
* Any non-SolrException's are wrapped at a higher level as a SolrException with SolrException.ErrorCode.SERVER_ERROR.
*/
void execute(CallInfo it) throws Exception;
}
}