-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SOLR-8207: Add "Nodes" view to the Admin UI "Cloud" tab, listing node…
…s and key metrics
- Loading branch information
Showing
16 changed files
with
1,103 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* | ||
* 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.IOException; | ||
import java.lang.invoke.MethodHandles; | ||
import java.net.URL; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.Future; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.TimeoutException; | ||
|
||
import org.apache.solr.client.solrj.SolrClient; | ||
import org.apache.solr.client.solrj.SolrRequest; | ||
import org.apache.solr.client.solrj.SolrServerException; | ||
import org.apache.solr.client.solrj.impl.HttpSolrClient; | ||
import org.apache.solr.client.solrj.request.GenericSolrRequest; | ||
import org.apache.solr.cloud.ZkController; | ||
import org.apache.solr.common.SolrException; | ||
import org.apache.solr.common.params.MapSolrParams; | ||
import org.apache.solr.common.params.SolrParams; | ||
import org.apache.solr.common.util.NamedList; | ||
import org.apache.solr.common.util.Pair; | ||
import org.apache.solr.core.CoreContainer; | ||
import org.apache.solr.request.SolrQueryRequest; | ||
import org.apache.solr.response.SolrQueryResponse; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Static methods to proxy calls to an Admin (GET) API to other nodes in the cluster and return a combined response | ||
*/ | ||
public class AdminHandlersProxy { | ||
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); | ||
private static final String PARAM_NODES = "nodes"; | ||
|
||
// Proxy this request to a different remote node if 'node' parameter is provided | ||
public static boolean maybeProxyToNodes(SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container) | ||
throws IOException, SolrServerException, InterruptedException { | ||
String nodeNames = req.getParams().get(PARAM_NODES); | ||
if (nodeNames == null || nodeNames.isEmpty()) { | ||
return false; // local request | ||
} | ||
|
||
if (!container.isZooKeeperAware()) { | ||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Parameter " + PARAM_NODES + " only supported in Cloud mode"); | ||
} | ||
|
||
Set<String> nodes; | ||
String pathStr = req.getPath(); | ||
|
||
Map<String,String> paramsMap = req.getParams().toMap(new HashMap<>()); | ||
paramsMap.remove(PARAM_NODES); | ||
SolrParams params = new MapSolrParams(paramsMap); | ||
Set<String> liveNodes = container.getZkController().zkStateReader.getClusterState().getLiveNodes(); | ||
|
||
if (nodeNames.equals("all")) { | ||
nodes = liveNodes; | ||
log.debug("All live nodes requested"); | ||
} else { | ||
nodes = new HashSet<>(Arrays.asList(nodeNames.split(","))); | ||
for (String nodeName : nodes) { | ||
if (!nodeName.matches("^[^/:]+:\\d+_[\\w/]+$")) { | ||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Parameter " + PARAM_NODES + " has wrong format"); | ||
} | ||
|
||
if (!liveNodes.contains(nodeName)) { | ||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Requested node " + nodeName + " is not part of cluster"); | ||
} | ||
} | ||
log.debug("Nodes requested: {}", nodes); | ||
} | ||
log.debug(PARAM_NODES + " parameter {} specified on {} request", nodeNames, pathStr); | ||
|
||
Map<String, Pair<Future<NamedList<Object>>, SolrClient>> responses = new HashMap<>(); | ||
for (String node : nodes) { | ||
responses.put(node, callRemoteNode(node, pathStr, params, container.getZkController())); | ||
} | ||
|
||
for (Map.Entry<String, Pair<Future<NamedList<Object>>, SolrClient>> entry : responses.entrySet()) { | ||
try { | ||
NamedList<Object> resp = entry.getValue().first().get(10, TimeUnit.SECONDS); | ||
entry.getValue().second().close(); | ||
rsp.add(entry.getKey(), resp); | ||
} catch (ExecutionException ee) { | ||
log.warn("Exception when fetching result from node {}", entry.getKey(), ee); | ||
} catch (TimeoutException te) { | ||
log.warn("Timeout when fetching result from node {}", entry.getKey(), te); | ||
} | ||
} | ||
log.info("Fetched response from {} nodes: {}", responses.keySet().size(), responses.keySet()); | ||
return true; | ||
} | ||
|
||
/** | ||
* Makes a remote request and returns a future and the solr client. The caller is responsible for closing the client | ||
*/ | ||
public static Pair<Future<NamedList<Object>>, SolrClient> callRemoteNode(String nodeName, String endpoint, | ||
SolrParams params, ZkController zkController) | ||
throws IOException, SolrServerException { | ||
log.debug("Proxying {} request to node {}", endpoint, nodeName); | ||
URL baseUrl = new URL(zkController.zkStateReader.getBaseUrlForNodeName(nodeName)); | ||
HttpSolrClient solr = new HttpSolrClient.Builder(baseUrl.toString()).build(); | ||
SolrRequest proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, endpoint, params); | ||
HttpSolrClient.HttpUriRequestResponse proxyResp = solr.httpUriRequest(proxyReq); | ||
return new Pair<>(proxyResp.future, solr); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
solr/core/src/test/org/apache/solr/handler/admin/AdminHandlersProxyTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* 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.IOException; | ||
import java.util.Collections; | ||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.lucene.util.IOUtils; | ||
import org.apache.solr.client.solrj.SolrRequest; | ||
import org.apache.solr.client.solrj.SolrServerException; | ||
import org.apache.solr.client.solrj.impl.CloudSolrClient; | ||
import org.apache.solr.client.solrj.request.GenericSolrRequest; | ||
import org.apache.solr.client.solrj.response.SimpleSolrResponse; | ||
import org.apache.solr.cloud.SolrCloudTestCase; | ||
import org.apache.solr.common.SolrException; | ||
import org.apache.solr.common.params.MapSolrParams; | ||
import org.apache.solr.common.util.NamedList; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
|
||
public class AdminHandlersProxyTest extends SolrCloudTestCase { | ||
private CloseableHttpClient httpClient; | ||
private CloudSolrClient solrClient; | ||
|
||
@BeforeClass | ||
public static void setupCluster() throws Exception { | ||
configureCluster(2) | ||
.addConfig("conf", configset("cloud-minimal")) | ||
.configure(); | ||
} | ||
|
||
@Before | ||
@Override | ||
public void setUp() throws Exception { | ||
super.setUp(); | ||
solrClient = getCloudSolrClient(cluster); | ||
solrClient.connect(1000, TimeUnit.MILLISECONDS); | ||
httpClient = (CloseableHttpClient) solrClient.getHttpClient(); | ||
} | ||
|
||
@After | ||
@Override | ||
public void tearDown() throws Exception { | ||
super.tearDown(); | ||
IOUtils.close(solrClient, httpClient); | ||
} | ||
|
||
@Test | ||
public void proxySystemInfoHandlerAllNodes() throws IOException, SolrServerException { | ||
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "all")); | ||
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params); | ||
SimpleSolrResponse rsp = req.process(solrClient, null); | ||
NamedList<Object> nl = rsp.getResponse(); | ||
assertEquals(3, nl.size()); | ||
assertTrue(nl.getName(1).endsWith("_solr")); | ||
assertTrue(nl.getName(2).endsWith("_solr")); | ||
assertEquals("solrcloud", ((NamedList)nl.get(nl.getName(1))).get("mode")); | ||
assertEquals(nl.getName(2), ((NamedList)nl.get(nl.getName(2))).get("node")); | ||
} | ||
|
||
@Test | ||
public void proxyMetricsHandlerAllNodes() throws IOException, SolrServerException { | ||
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "all")); | ||
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/metrics", params); | ||
SimpleSolrResponse rsp = req.process(solrClient, null); | ||
NamedList<Object> nl = rsp.getResponse(); | ||
assertEquals(3, nl.size()); | ||
assertTrue(nl.getName(1).endsWith("_solr")); | ||
assertTrue(nl.getName(2).endsWith("_solr")); | ||
assertNotNull(((NamedList)nl.get(nl.getName(1))).get("metrics")); | ||
} | ||
|
||
@Test(expected = SolrException.class) | ||
public void proxySystemInfoHandlerNonExistingNode() throws IOException, SolrServerException { | ||
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "example.com:1234_solr")); | ||
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params); | ||
SimpleSolrResponse rsp = req.process(solrClient, null); | ||
} | ||
|
||
@Test | ||
public void proxySystemInfoHandlerOneNode() { | ||
Set<String> nodes = solrClient.getClusterStateProvider().getLiveNodes(); | ||
assertEquals(2, nodes.size()); | ||
nodes.forEach(node -> { | ||
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", node)); | ||
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params); | ||
SimpleSolrResponse rsp = null; | ||
try { | ||
rsp = req.process(solrClient, null); | ||
} catch (Exception e) { | ||
fail("Exception while proxying request to node " + node); | ||
} | ||
NamedList<Object> nl = rsp.getResponse(); | ||
assertEquals(2, nl.size()); | ||
assertEquals("solrcloud", ((NamedList)nl.get(nl.getName(1))).get("mode")); | ||
assertEquals(nl.getName(1), ((NamedList)nl.get(nl.getName(1))).get("node")); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.