From a29bc3a48c9579692c1064b65cf05dd3adeb10d5 Mon Sep 17 00:00:00 2001 From: Jason Gerlowski Date: Sun, 28 Dec 2025 21:50:10 -0500 Subject: [PATCH 1/2] Refactor 'AdminHandlersProxy' into its own package --- .../src/java/org/apache/solr/handler/admin/LoggingHandler.java | 1 + .../src/java/org/apache/solr/handler/admin/MetricsHandler.java | 1 + .../java/org/apache/solr/handler/admin/SystemInfoHandler.java | 1 + .../solr/handler/admin/{ => proxy}/AdminHandlersProxy.java | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) rename solr/core/src/java/org/apache/solr/handler/admin/{ => proxy}/AdminHandlersProxy.java (99%) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java index 7593bb7cbdd..121f807136b 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java @@ -31,6 +31,7 @@ import org.apache.solr.core.CoreContainer; import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.handler.admin.api.NodeLogging; +import org.apache.solr.handler.admin.proxy.AdminHandlersProxy; import org.apache.solr.handler.api.V2ApiUtils; import org.apache.solr.logging.LogWatcher; import org.apache.solr.request.SolrQueryRequest; diff --git a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java index 13b4d044c7e..5d4994f6b3a 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java @@ -39,6 +39,7 @@ import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.CoreContainer; import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.handler.admin.proxy.AdminHandlersProxy; import org.apache.solr.metrics.SolrMetricManager; import org.apache.solr.metrics.otel.FilterablePrometheusMetricReader; import org.apache.solr.request.SolrQueryRequest; diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java index 16e78ab4268..88a77ab1452 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java @@ -55,6 +55,7 @@ import org.apache.solr.core.SolrCore; import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.handler.admin.api.NodeSystemInfoAPI; +import org.apache.solr.handler.admin.proxy.AdminHandlersProxy; import org.apache.solr.metrics.GpuMetricsProvider; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; diff --git a/solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java b/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java similarity index 99% rename from solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java rename to solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java index 5f253c4ec4e..a73a482c67d 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.solr.handler.admin; +package org.apache.solr.handler.admin.proxy; import java.io.IOException; import java.lang.invoke.MethodHandles; From 44d9aab51b6b52ce07cc4528a1fc929a98c0921a Mon Sep 17 00:00:00 2001 From: Jason Gerlowski Date: Mon, 29 Dec 2025 10:00:00 -0500 Subject: [PATCH 2/2] Initial refactor of AdminHandlersProxy --- .../solr/handler/admin/LoggingHandler.java | 7 +- .../solr/handler/admin/MetricsHandler.java | 8 +- .../solr/handler/admin/SystemInfoHandler.java | 6 +- .../admin/proxy/AdminHandlersProxy.java | 164 ++++++++---------- .../admin/proxy/NormalV1RequestProxy.java | 64 +++++++ .../admin/proxy/PrometheusRequestProxy.java | 68 ++++++++ .../handler/admin/proxy/package-info.java | 19 ++ 7 files changed, 234 insertions(+), 102 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/proxy/NormalV1RequestProxy.java create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/proxy/PrometheusRequestProxy.java create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/proxy/package-info.java diff --git a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java index 121f807136b..4d93ff60b1a 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java @@ -92,8 +92,11 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw } rsp.setHttpCaching(false); - if (cc != null && AdminHandlersProxy.maybeProxyToNodes(req, rsp, cc)) { - return; // Request was proxied to other node + if (cc != null) { + final var adminProxy = AdminHandlersProxy.create(cc, req, rsp); + if (adminProxy.shouldProxy()) { + adminProxy.proxyRequest(); + } } } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java index 5d4994f6b3a..5218f53da88 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java @@ -123,8 +123,12 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw + format); } - if (cc != null && AdminHandlersProxy.maybeProxyToNodes(req, rsp, cc)) { - return; // Request was proxied to other node + if (cc != null) { + final var adminProxy = AdminHandlersProxy.create(cc, req, rsp); + if (adminProxy.shouldProxy()) { + adminProxy.proxyRequest(); + return; + } } SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, rsp)); try { diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java index 88a77ab1452..e635e5b6b16 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java @@ -221,8 +221,10 @@ private void initHostname() { public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { rsp.setHttpCaching(false); SolrCore core = req.getCore(); - if (AdminHandlersProxy.maybeProxyToNodes(req, rsp, getCoreContainer(req))) { - return; // Request was proxied to other node + final var adminProxy = AdminHandlersProxy.create(getCoreContainer(req), req, rsp); + if (adminProxy.shouldProxy()) { + adminProxy.proxyRequest(); + return; } if (core != null) rsp.add("core", getCoreInfo(core, req.getSchema())); boolean solrCloudMode = getCoreContainer(req).isZooKeeperAware(); diff --git a/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java b/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java index a73a482c67d..6e5ef805be2 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/proxy/AdminHandlersProxy.java @@ -17,10 +17,13 @@ package org.apache.solr.handler.admin.proxy; +import static org.apache.solr.common.params.CommonParams.WT; + import java.io.IOException; import java.lang.invoke.MethodHandles; import java.net.URI; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -33,10 +36,7 @@ import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.GenericSolrRequest; -import org.apache.solr.client.solrj.response.InputStreamResponseParser; -import org.apache.solr.cloud.ZkController; import org.apache.solr.common.SolrException; -import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.core.CoreContainer; @@ -49,65 +49,55 @@ * Static methods to proxy calls to an Admin (GET) API to other nodes in the cluster and return a * combined response */ -public class AdminHandlersProxy { +public abstract class AdminHandlersProxy { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final String PARAM_NODES = "nodes"; - private static final String PARAM_NODE = "node"; + protected static final String PARAM_NODES = "nodes"; + // TODO Move to NormalV1RequestProxy if not used elsewhere when finished + protected static final String PARAM_NODE = "node"; + // TODO Move to PrometheusRequestProxy if not used elsewhere when finished private static final long PROMETHEUS_FETCH_TIMEOUT_SECONDS = 10; - /** Proxy this request to a different remote node if 'node' or 'nodes' parameter is provided */ - public static boolean maybeProxyToNodes( - SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container) - throws IOException, SolrServerException, InterruptedException { + protected final CoreContainer + coreContainer; // TODO reduce this to just ZkStateReader or something similar + protected final SolrQueryRequest req; - String pathStr = req.getPath(); - ModifiableSolrParams params = new ModifiableSolrParams(req.getParams()); + public AdminHandlersProxy(CoreContainer coreContainer, SolrQueryRequest req) { + this.coreContainer = coreContainer; + this.req = req; + } - // Check if response format is Prometheus/OpenMetrics - String wt = params.get("wt"); - boolean isPrometheusFormat = "prometheus".equals(wt) || "openmetrics".equals(wt); + public abstract boolean shouldProxy(); - if (isPrometheusFormat) { - // Prometheus format: use singular 'node' parameter for single-node proxy - String nodeName = req.getParams().get(PARAM_NODE); - if (nodeName == null || nodeName.isEmpty()) { - return false; // No node parameter, handle locally - } + /** + * TODO fill in these javadocs + * + *

Only called if 'shouldProxy()' returns true + */ + public abstract Collection getDestinationNodes(); - params.remove(PARAM_NODE); - handlePrometheusSingleNode(nodeName, pathStr, params, container, rsp); - } else { - // Other formats (JSON/XML): use plural 'nodes' parameter for multi-node aggregation - String nodeNames = req.getParams().get(PARAM_NODES); - if (nodeNames == null || nodeNames.isEmpty()) { - return false; // No nodes parameter, handle locally - } + public abstract SolrRequest prepareProxiedRequest(); - params.remove(PARAM_NODES); - Set nodes = resolveNodes(nodeNames, container); - handleNamedListFormat(nodes, pathStr, params, container.getZkController(), rsp); + public abstract void processProxiedResponse(String nodeName, NamedList proxiedResponse); + + public boolean proxyRequest() { + if (!shouldProxy()) { + return false; } + final var nodesToProxyTo = getDestinationNodes(); + final var solrRequest = prepareProxiedRequest(); + final var responseFutures = doProxyToNodes(nodesToProxyTo, solrRequest); + bulkProcessResponses(responseFutures); return true; } - /** Handle non-Prometheus formats using the existing NamedList approach. */ - private static void handleNamedListFormat( - Set nodes, - String pathStr, - SolrParams params, - ZkController zkController, - SolrQueryResponse rsp) { - - Map>> responses = new LinkedHashMap<>(); - for (String node : nodes) { - responses.put(node, callRemoteNode(node, pathStr, params, zkController)); - } - - for (Map.Entry>> entry : responses.entrySet()) { + // TODO Should we make either the request-submission or waiting timeout more configurable by + // sub-classes? + private void bulkProcessResponses(Map>> responseFutures) { + for (Map.Entry>> entry : responseFutures.entrySet()) { try { NamedList resp = entry.getValue().get(10, TimeUnit.SECONDS); - rsp.add(entry.getKey(), resp); + processProxiedResponse(entry.getKey(), resp); } catch (ExecutionException ee) { log.warn( "Exception when fetching result from node {}", entry.getKey(), ee.getCause()); // nowarn @@ -120,14 +110,39 @@ private static void handleNamedListFormat( } } if (log.isDebugEnabled()) { - log.debug("Fetched response from {} nodes: {}", responses.size(), responses.keySet()); + log.debug( + "Fetched response from {} nodes: {}", responseFutures.size(), responseFutures.keySet()); } } + public static AdminHandlersProxy create( + CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) { + final var wtValue = req.getParams().get(WT); + if ("prometheus".equals(wtValue) || "openmetrics".equals(wtValue)) { + return new PrometheusRequestProxy(coreContainer, req, rsp); + } + + return new NormalV1RequestProxy(coreContainer, req, rsp); + } + + public static SolrRequest createGenericRequest(String apiPath, SolrParams params) { + return new GenericSolrRequest(SolrRequest.METHOD.GET, apiPath, params); + } + + private Map>> doProxyToNodes( + Collection nodesToProxyTo, SolrRequest solrRequest) { + Map>> responses = new LinkedHashMap<>(); + for (String node : nodesToProxyTo) { + responses.put(node, callRemoteNode(node, solrRequest)); + } + return responses; + } + /** Makes a remote request asynchronously. */ - public static CompletableFuture> callRemoteNode( - String nodeName, String uriPath, SolrParams params, ZkController zkController) { + public CompletableFuture> callRemoteNode( + String nodeName, SolrRequest solrRequest) { + final var zkController = coreContainer.getZkController(); // Validate that the node exists in the cluster if (!zkController.zkStateReader.getClusterState().getLiveNodes().contains(nodeName)) { throw new SolrException( @@ -135,21 +150,14 @@ public static CompletableFuture> callRemoteNode( "Requested node " + nodeName + " is not part of cluster"); } - log.debug("Proxying {} request to node {}", uriPath, nodeName); + log.debug("Proxying {} request to node {}", solrRequest, nodeName); URI baseUri = URI.create(zkController.zkStateReader.getBaseUrlForNodeName(nodeName)); - SolrRequest proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, uriPath, params); - - // Set response parser based on wt parameter to ensure correct format is used - String wt = params.get("wt"); - if ("prometheus".equals(wt) || "openmetrics".equals(wt)) { - proxyReq.setResponseParser(new InputStreamResponseParser(wt)); - } try { return zkController .getCoreContainer() .getDefaultHttpSolrClient() - .requestWithBaseUrl(baseUri.toString(), c -> c.requestAsync(proxyReq)); + .requestWithBaseUrl(baseUri.toString(), c -> c.requestAsync(solrRequest)); } catch (SolrServerException | IOException e) { // requestWithBaseUrl declares it throws these but it actually depends on the lambda assert false : "requestAsync doesn't throw; it returns a Future"; @@ -165,7 +173,7 @@ public static CompletableFuture> callRemoteNode( * @return set of resolved node names * @throws SolrException if node format is invalid */ - private static Set resolveNodes(String nodeNames, CoreContainer container) { + protected static Set resolveNodes(String nodeNames, CoreContainer container) { Set liveNodes = container.getZkController().zkStateReader.getClusterState().getLiveNodes(); @@ -184,40 +192,4 @@ private static Set resolveNodes(String nodeNames, CoreContainer containe log.debug("Nodes requested: {}", nodes); return nodes; } - - /** - * Handle Prometheus format by proxying to a single node. * - * - * @param nodeName the name of the single node to proxy to - * @param pathStr the request path - * @param params the request parameters (with 'node' parameter already removed) - * @param container the CoreContainer - * @param rsp the response to populate - */ - private static void handlePrometheusSingleNode( - String nodeName, - String pathStr, - ModifiableSolrParams params, - CoreContainer container, - SolrQueryResponse rsp) - throws IOException, SolrServerException { - - // Keep wt=prometheus for the remote request so MetricsHandler accepts it - // The InputStreamResponseParser will return the Prometheus text in a "stream" key - Future> response = - callRemoteNode(nodeName, pathStr, params, container.getZkController()); - - try { - try { - NamedList resp = response.get(PROMETHEUS_FETCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); - rsp.getValues().addAll(resp); - } catch (ExecutionException e) { - throw e.getCause(); - } - } catch (IOException | SolrServerException | RuntimeException | Error e) { - throw e; - } catch (Throwable t) { // unlikely? - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, t); - } - } } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/proxy/NormalV1RequestProxy.java b/solr/core/src/java/org/apache/solr/handler/admin/proxy/NormalV1RequestProxy.java new file mode 100644 index 00000000000..f36d1cce1f3 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/proxy/NormalV1RequestProxy.java @@ -0,0 +1,64 @@ +/* + * 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.proxy; + +import java.util.Collection; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; + +// TODO Fix this name +public class NormalV1RequestProxy extends AdminHandlersProxy { + + private final ModifiableSolrParams params; + private final SolrQueryResponse rsp; + + public NormalV1RequestProxy( + CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) { + super(coreContainer, req); + this.params = new ModifiableSolrParams(req.getParams()); + this.rsp = rsp; + } + + @Override + public boolean shouldProxy() { + String nodeNames = params.get(PARAM_NODES); + if (nodeNames == null || nodeNames.isEmpty()) { + return false; // No nodes parameter, handle locally + } + return true; + } + + @Override + public Collection getDestinationNodes() { + return AdminHandlersProxy.resolveNodes(params.get(PARAM_NODES), coreContainer); + } + + @Override + public SolrRequest prepareProxiedRequest() { + params.remove(PARAM_NODES); + return AdminHandlersProxy.createGenericRequest(req.getPath(), params); + } + + @Override + public void processProxiedResponse(String nodeName, NamedList proxiedResponse) { + rsp.add(nodeName, proxiedResponse); + } +} diff --git a/solr/core/src/java/org/apache/solr/handler/admin/proxy/PrometheusRequestProxy.java b/solr/core/src/java/org/apache/solr/handler/admin/proxy/PrometheusRequestProxy.java new file mode 100644 index 00000000000..9f29501b4f7 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/proxy/PrometheusRequestProxy.java @@ -0,0 +1,68 @@ +/* + * 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.proxy; + +import java.util.List; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.response.InputStreamResponseParser; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; + +public class PrometheusRequestProxy extends AdminHandlersProxy { + + private final ModifiableSolrParams params; + private final SolrQueryResponse rsp; + + public PrometheusRequestProxy( + CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) { + super(coreContainer, req); + this.params = new ModifiableSolrParams(req.getParams()); + this.rsp = rsp; + } + + @Override + public boolean shouldProxy() { + String nodeName = params.get(PARAM_NODE); + if (nodeName == null || nodeName.isEmpty()) { + return false; // No node parameter, handle locally + } + + return true; + } + + @Override + public List getDestinationNodes() { + return List.of(params.get(PARAM_NODE)); + } + + @Override + public SolrRequest prepareProxiedRequest() { + params.remove(PARAM_NODE); + final var solrRequest = AdminHandlersProxy.createGenericRequest(req.getPath(), params); + // TODO assert that 'wt' is either 'prometheus' or 'openmetrics' + solrRequest.setResponseParser(new InputStreamResponseParser(params.get("wt"))); + return solrRequest; + } + + @Override + public void processProxiedResponse(String nodeName, NamedList proxiedResponse) { + rsp.getValues().addAll(proxiedResponse); + } +} diff --git a/solr/core/src/java/org/apache/solr/handler/admin/proxy/package-info.java b/solr/core/src/java/org/apache/solr/handler/admin/proxy/package-info.java new file mode 100644 index 00000000000..a7d2fff60fa --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/proxy/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** Code for proxying "admin" (i.e. non-SolrCore) requests to different Solr nodes */ +package org.apache.solr.handler.admin.proxy;