You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ab...@apache.org on 2026/10/14 14:36:31 UTC
(solr) branch branch_10_1 updated (dfda5e1d482 -> a8cda556154)
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a change to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
from dfda5e1d482 Regenerate CHANGELOG.md for v10.1.0 RC2
new 2db17cf2586 SOLR-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree (#4952)
new 3a86b4943ba SOLR-18450: Use /api/collections in test_auth.bats (#5055)
new fd42ef4d7ff SOLR-18031: Respect --filetypes for files and globs in bin/solr post (#5032)
new e884a1bbd8a SolrTestCase: reset system properties per test (#5056)
new fb56d1f6cc0 SOLR-17398: Link V1 collection operations to their V2 equivalents (#5053)
new c1e73c13924 SOLR-18400: Plugins screen 500s when metrics are disabled (#4803)
new a3603260eaa SOLR-17820: Fix CLUSTERSTATUS health reporting GREEN with down replicas (#5046)
new c8c86434cd2 SOLR-18520: Package Uninstall should work in absence of clusterprops.json (#5052)
new d43e574fd2a SOLR-18166: Remove last vestiges of 'linkconfig' ZK storage
new a8cda556154 Documented V2 API Conventions (#4826)
The 10 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
AGENTS.md | 3 +-
.../SOLR-18400-plugins-metrics-disabled.yml | 11 +
.../SOLR-18450-v2-collections-detailed.yml | 14 ++
changelog/unreleased/SOLR-18520-uninstall-bug.yml | 9 +
...lr-17820-cluster-status-health-ordering-fix.yml | 8 +
changelog/unreleased/solr-18031-post-filetypes.yml | 7 +
...i-conventions.adoc => apis-v2-conventions.adoc} | 39 +++-
dev-docs/apis.adoc | 81 ++++++-
.../client/api/endpoint/ListCollectionsApi.java | 36 +++-
.../client/api/model/ListCollectionsResponse.java | 100 +++++++++
.../src/java/org/apache/solr/cli/PostTool.java | 19 +-
.../solr/cloud/api/collections/CollApiCmds.java | 10 -
.../java/org/apache/solr/handler/ClusterAPI.java | 9 -
.../apache/solr/handler/admin/ClusterStatus.java | 135 ++++++++----
.../solr/handler/admin/CollectionsHandler.java | 45 +++-
.../apache/solr/handler/admin/MetricsHandler.java | 4 +-
.../solr/handler/admin/api/ListCollections.java | 48 ++++-
.../apache/solr/packagemanager/PackageManager.java | 13 +-
.../src/test/org/apache/solr/cli/PostToolTest.java | 40 ++++
.../solr/handler/V2ClusterAPIMappingTest.java | 8 -
.../solr/handler/admin/ClusterStatusTest.java | 69 ++++++
.../solr/handler/admin/MetricsDisabledTest.java | 65 ++++++
... => ListCollectionsDetailedStandaloneTest.java} | 12 +-
.../admin/api/ListCollectionsDetailedTest.java | 236 +++++++++++++++++++++
solr/packaging/test/test_auth.bats | 4 +-
.../modules/configuration-guide/pages/v2-api.adoc | 10 +-
.../pages/cluster-node-management.adoc | 25 +--
.../pages/collection-management.adoc | 200 +++++++++++++++++
.../deployment-guide/pages/metrics-reporting.adoc | 3 +
.../modules/indexing-guide/pages/post-command.adoc | 3 +
.../src/java/org/apache/solr/SolrTestCase.java | 20 +-
.../src/java/org/apache/solr/SolrTestCaseJ4.java | 6 -
.../AdminUiMetricsDisabledStandaloneTest.java | 74 +++++++
solr/webapp/web/js/angular/app.js | 7 +-
.../web/js/angular/controllers/alias-overview.js | 2 +-
solr/webapp/web/js/angular/controllers/cloud.js | 124 +++++------
.../js/angular/controllers/collection-overview.js | 13 +-
.../web/js/angular/controllers/collections.js | 11 +-
solr/webapp/web/js/angular/controllers/plugins.js | 7 +
solr/webapp/web/js/angular/services.js | 14 +-
solr/webapp/web/partials/plugins.html | 3 +
41 files changed, 1328 insertions(+), 219 deletions(-)
create mode 100644 changelog/unreleased/SOLR-18400-plugins-metrics-disabled.yml
create mode 100644 changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
create mode 100644 changelog/unreleased/SOLR-18520-uninstall-bug.yml
create mode 100644 changelog/unreleased/solr-17820-cluster-status-health-ordering-fix.yml
create mode 100644 changelog/unreleased/solr-18031-post-filetypes.yml
rename dev-docs/{v2-api-conventions.adoc => apis-v2-conventions.adoc} (59%)
create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/ClusterStatusTest.java
create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/MetricsDisabledTest.java
copy solr/core/src/test/org/apache/solr/handler/admin/api/{ListClusterNodesStandaloneTest.java => ListCollectionsDetailedStandaloneTest.java} (78%)
create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiMetricsDisabledStandaloneTest.java
(solr) 01/10: SOLR-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree (#4952)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 2db17cf25860c6ed5ae20e18fa9eaa3ab9311f08
Author: Prithvi S <pr...@gmail.com>
AuthorDate: Tue Oct 6 22:11:55 2026 +0530
SOLR-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree (#4952)
Co-authored-by: Eric Pugh <ep...@opensourceconnections.com>
(cherry picked from commit 0cfeeb26a6cae10d02e5ae23a7fecca47781ea1e)
(cherry picked from commit a6c06108a31c003d0d09f7e83a63ee2dc230b47c)
---
.../SOLR-18450-v2-collections-detailed.yml | 14 ++
.../client/api/endpoint/ListCollectionsApi.java | 36 +++-
.../client/api/model/ListCollectionsResponse.java | 100 +++++++++
.../java/org/apache/solr/handler/ClusterAPI.java | 9 -
.../apache/solr/handler/admin/ClusterStatus.java | 127 +++++++----
.../solr/handler/admin/CollectionsHandler.java | 3 +-
.../solr/handler/admin/api/ListCollections.java | 48 ++++-
.../solr/handler/V2ClusterAPIMappingTest.java | 8 -
.../api/ListCollectionsDetailedStandaloneTest.java | 45 ++++
.../admin/api/ListCollectionsDetailedTest.java | 236 +++++++++++++++++++++
.../modules/configuration-guide/pages/v2-api.adoc | 10 +-
.../pages/cluster-node-management.adoc | 25 +--
.../pages/collection-management.adoc | 200 +++++++++++++++++
solr/webapp/web/js/angular/app.js | 2 +-
.../web/js/angular/controllers/alias-overview.js | 2 +-
solr/webapp/web/js/angular/controllers/cloud.js | 124 +++++------
.../js/angular/controllers/collection-overview.js | 13 +-
.../web/js/angular/controllers/collections.js | 11 +-
solr/webapp/web/js/angular/services.js | 14 +-
19 files changed, 863 insertions(+), 164 deletions(-)
diff --git a/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml b/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
new file mode 100644
index 00000000000..3406f380b72
--- /dev/null
+++ b/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
@@ -0,0 +1,14 @@
+title:
+ V2 GET /api/collections gains a `detailed=true` param returning the collections, shards, and
+ replicas tree (in `collectionsDetail`), without live nodes, aliases, or cluster properties. This
+ tree previously lived at GET /api/cluster in an earlier draft of this change; it was relocated to
+ /api/collections since it's collection data, not cluster-wide data -- `/api/cluster/nodes`,
+ `/api/aliases`, and `/api/cluster/properties` already cover the cluster-wide pieces.
+type: changed
+authors:
+ - name: Prithvi S
+ nick: iprithv
+ - name: Eric Pugh
+links:
+ - name: SOLR-18450
+ url: https://issues.apache.org/jira/browse/SOLR-18450
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
index 2f20f4b4366..468b79d52bf 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
@@ -17,8 +17,10 @@
package org.apache.solr.client.api.endpoint;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
import org.apache.solr.client.api.model.ListCollectionsResponse;
@Path("/collections")
@@ -27,5 +29,37 @@ public interface ListCollectionsApi {
@Operation(
summary = "List all collections in this Solr cluster",
tags = {"collections"})
- ListCollectionsResponse listCollections();
+ ListCollectionsResponse listCollections(
+ @Parameter(
+ description =
+ "When true, return the collections, shards, and replicas tree (in"
+ + " 'collectionsDetail') instead of the plain collection name list.")
+ @QueryParam("detailed")
+ Boolean detailed,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Collection or alias to return. Omit to"
+ + " return every collection. An alias returns the collections it points"
+ + " at.")
+ @QueryParam("collection")
+ String collection,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Shard or comma-separated shards to return."
+ + " Applied to each selected collection.")
+ @QueryParam("shard")
+ String shard,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Route key of a document. Limits the tree to"
+ + " the shard that would hold that document.")
+ @QueryParam("_route_")
+ String routeKey,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Include per-replica state when the"
+ + " collection uses it.")
+ @QueryParam("prs")
+ Boolean prs)
+ throws Exception;
}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
index 17bf44c44ec..996b6e70729 100644
--- a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
@@ -16,10 +16,110 @@
*/
package org.apache.solr.client.api.model;
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
public class ListCollectionsResponse extends SolrJerseyResponse {
@JsonProperty("collections")
public List<String> collections;
+
+ @Schema(description = "Collections, shards, and replicas, present only when detailed=true.")
+ @JsonProperty("collectionsDetail")
+ public Map<String, CollectionState> collectionsDetail;
+
+ /** State of one collection, including its shards and replicas. */
+ public static class CollectionState {
+ @JsonProperty public Map<String, ShardState> shards;
+
+ @Schema(description = "Worst shard health in this collection: GREEN, YELLOW, ORANGE, or RED.")
+ @JsonProperty
+ public String health;
+
+ @JsonProperty public String configName;
+ @JsonProperty public Integer znodeVersion;
+ @JsonProperty public Long creationTimeMillis;
+
+ @Schema(description = "Aliases that point at this collection.")
+ @JsonProperty
+ public List<String> aliases;
+
+ @Schema(description = "Document router for this collection, e.g. name=compositeId.")
+ @JsonProperty
+ public Map<String, String> router;
+
+ @JsonProperty public Integer replicationFactor;
+
+ private final Map<String, Object> additionalProperties = new LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
+
+ /** State of one shard. */
+ public static class ShardState {
+ @JsonProperty public String state;
+ @JsonProperty public String range;
+
+ @Schema(description = "Shard health: GREEN, YELLOW, ORANGE, or RED.")
+ @JsonProperty
+ public String health;
+
+ @JsonProperty public Map<String, ReplicaState> replicas;
+
+ private final Map<String, Object> additionalProperties = new LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
+
+ /** State of one replica. */
+ public static class ReplicaState {
+ @JsonProperty public String state;
+ @JsonProperty public String core;
+
+ @JsonProperty("node_name")
+ public String nodeName;
+
+ @JsonProperty("base_url")
+ public String baseUrl;
+
+ /**
+ * {@code "true"} when this replica is the leader. Absent otherwise. state.json stores this as a
+ * string.
+ */
+ @JsonProperty public String leader;
+
+ @JsonProperty public String type;
+
+ private final Map<String, Object> additionalProperties = new LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index 41dd007d955..c73f10f2ec6 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -41,8 +41,6 @@ import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.cloud.ClusterProperties;
import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CollectionParams.CollectionAction;
-import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.DefaultSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ReflectMapWriter;
@@ -215,13 +213,6 @@ public class ClusterAPI {
return req;
}
- @EndPoint(method = GET, path = "/cluster", permission = COLL_READ_PERM)
- public void getClusterStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- final Map<String, Object> v1Params =
- Map.of(CommonParams.ACTION, CollectionAction.CLUSTERSTATUS.toLower());
- collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
- }
-
private CoreContainer getCoreContainer() {
return collectionsHandler.getCoreContainer();
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
index 74b1db62382..7d07cdd67a8 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
@@ -20,10 +20,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.stream.Stream;
import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.common.MapWriter;
@@ -120,9 +122,10 @@ public class ClusterStatus {
boolean withAliases = solrParams.getBool(ALIASES_PROP, includeAll);
List<String> liveNodes = null;
- if (withLiveNodes || collection != null) {
+ // Replica state is cross-checked against live nodes whenever the collection tree is built,
+ // including when the caller asked for that tree without the live-node list itself.
+ if (withLiveNodes || withCollection) {
liveNodes = zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null);
- // add live_nodes
if (withLiveNodes) clusterStatus.add("live_nodes", liveNodes);
}
@@ -133,7 +136,7 @@ public class ClusterStatus {
if (withCollection) {
assert liveNodes != null;
- fetchClusterStatusForCollOrAlias(clusterStatus, liveNodes, aliases, solrVersion);
+ addCollectionStatuses(clusterStatus, liveNodes, aliases, solrVersion);
}
if (withAliases) {
@@ -163,13 +166,49 @@ public class ClusterStatus {
results.add("cluster", clusterStatus);
}
- private void fetchClusterStatusForCollOrAlias(
+ /**
+ * Collections, shards, and replicas selected by this request's {@code collection}, {@code shard},
+ * and {@code _route_} parameters.
+ *
+ * <p>Live nodes are read so replica state can be cross-checked, and aliases are read so a
+ * collection alias in {@code collection} can be resolved. Each collection still lists the aliases
+ * that point at it. The live-node list, the alias map, and cluster properties are not returned.
+ */
+ public Map<String, Object> getCollectionStatuses() throws KeeperException, InterruptedException {
+ List<String> liveNodes =
+ zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null);
+ Aliases aliases = zkStateReader.getAliases();
+ Map<String, Object> collections = new LinkedHashMap<>();
+ forEachCollectionStatus(
+ liveNodes, aliases, (name, status) -> collections.put(name, copyForJackson(status)));
+ return collections;
+ }
+
+ private void addCollectionStatuses(
NamedList<Object> clusterStatus,
List<String> liveNodes,
Aliases aliases,
SolrVersion solrVersion) {
+ // Resolve the collection list now so a missing name fails the request, rather than during
+ // response writing. The per-collection JSON is still built while the response is written.
+ PreparedCollections prepared = prepareCollections(aliases);
+ if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) {
+ MapWriter collectionPropsWriter =
+ ew -> prepared.forEach(liveNodes, (name, status) -> ew.putNoEx(name, status));
+ clusterStatus.add("collections", collectionPropsWriter);
+ } else {
+ NamedList<Object> collectionProps = new SimpleOrderedMap<>();
+ prepared.forEach(liveNodes, collectionProps::add);
+ clusterStatus.add("collections", collectionProps);
+ }
+ }
+
+ private void forEachCollectionStatus(
+ List<String> liveNodes, Aliases aliases, BiConsumer<String, Map<String, Object>> consumer) {
+ prepareCollections(aliases).forEach(liveNodes, consumer);
+ }
- // read aliases
+ private PreparedCollections prepareCollections(Aliases aliases) {
Map<String, List<String>> collectionVsAliases = new HashMap<>();
Map<String, List<String>> aliasVsCollections = aliases.getCollectionAliasListMap();
for (Map.Entry<String, List<String>> entry : aliasVsCollections.entrySet()) {
@@ -184,12 +223,6 @@ public class ClusterStatus {
}
ClusterState clusterState = zkStateReader.getClusterState();
-
- String routeKey = solrParams.get(ShardParams._ROUTE_);
- String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP);
-
- Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null;
-
Stream<DocCollection> collectionStream;
if (collection == null) {
collectionStream = clusterState.collectionStream();
@@ -215,33 +248,53 @@ public class ClusterStatus {
}
}
- if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) {
- MapWriter collectionPropsWriter =
- ew -> {
- collectionStream.forEach(
- (collectionState) -> {
- ew.putNoEx(
- collectionState.getName(),
- buildResponseForCollection(
- collectionState,
- collectionVsAliases,
- routeKey,
- liveNodes,
- requestedShards));
- });
- };
- clusterStatus.add("collections", collectionPropsWriter);
- } else {
- NamedList<Object> collectionProps = new SimpleOrderedMap<>();
- collectionStream.forEach(
- collectionState -> {
- collectionProps.add(
- collectionState.getName(),
- buildResponseForCollection(
- collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards));
- });
- clusterStatus.add("collections", collectionProps);
+ String routeKey = solrParams.get(ShardParams._ROUTE_);
+ String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP);
+ Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null;
+ return new PreparedCollections(
+ collectionStream.toList(), collectionVsAliases, routeKey, requestedShards);
+ }
+
+ private final class PreparedCollections {
+ private final List<DocCollection> collections;
+ private final Map<String, List<String>> collectionVsAliases;
+ private final String routeKey;
+ private final Set<String> requestedShards;
+
+ private PreparedCollections(
+ List<DocCollection> collections,
+ Map<String, List<String>> collectionVsAliases,
+ String routeKey,
+ Set<String> requestedShards) {
+ this.collections = collections;
+ this.collectionVsAliases = collectionVsAliases;
+ this.routeKey = routeKey;
+ this.requestedShards = requestedShards;
+ }
+
+ private void forEach(List<String> liveNodes, BiConsumer<String, Map<String, Object>> consumer) {
+ for (DocCollection collectionState : collections) {
+ consumer.accept(
+ collectionState.getName(),
+ buildResponseForCollection(
+ collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards));
+ }
+ }
+ }
+
+ /**
+ * Jersey serializes the v2 response with Jackson, which does not write Solr {@link MapWriter}s.
+ * Per-replica state is the one value still attached as a writer; turn it into plain JSON objects.
+ * Other fields are already maps and scalars.
+ */
+ private static Map<String, Object> copyForJackson(Map<String, Object> status) {
+ Object prs = status.get("PRS");
+ if (!(prs instanceof MapWriter)) {
+ return status;
}
+ Map<String, Object> copy = new LinkedHashMap<>(status);
+ copy.put("PRS", Utils.fromJSON(Utils.toJSON(prs)));
+ return copy;
}
private void addAliasMap(Aliases aliases, NamedList<Object> clusterStatus) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index f84281eeb00..05a47f4106c 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -808,7 +808,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
LIST,
(req, rsp, h) -> {
final ListCollections listCollectionsAPI = new ListCollections(h.coreContainer, req, rsp);
- final SolrJerseyResponse listCollectionsResponse = listCollectionsAPI.listCollections();
+ final SolrJerseyResponse listCollectionsResponse =
+ listCollectionsAPI.listCollections(null, null, null, null, null);
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, listCollectionsResponse);
return null;
}),
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
index 81c5fac32c5..831e5929b25 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
@@ -21,11 +21,18 @@ import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PER
import jakarta.inject.Inject;
import java.util.List;
+import java.util.Map;
import org.apache.solr.client.api.endpoint.ListCollectionsApi;
import org.apache.solr.client.api.model.ListCollectionsResponse;
+import org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState;
import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.CollectionUtil;
import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.ClusterStatus;
import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJacksonMapper;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
@@ -33,6 +40,12 @@ import org.apache.solr.response.SolrQueryResponse;
* V2 API for listing collections.
*
* <p>This API (GET /v2/collections) is equivalent to the v1 /admin/collections?action=LIST command
+ *
+ * <p>With {@code detailed=true}, it instead returns the collections, shards, and replicas tree (in
+ * {@code collectionsDetail}) -- the same tree v1 {@code CLUSTERSTATUS} returns under {@code
+ * cluster.collections}, without the live-node list, alias map, or cluster properties that accompany
+ * it there. Live nodes, aliases, and cluster properties have their own v2 endpoints: {@code GET
+ * /api/cluster/nodes}, {@code GET /api/aliases}, and {@code GET /api/cluster/properties}.
*/
public class ListCollections extends AdminAPIBase implements ListCollectionsApi {
@@ -43,11 +56,31 @@ public class ListCollections extends AdminAPIBase implements ListCollectionsApi
@Override
@PermissionName(COLL_READ_PERM)
- public ListCollectionsResponse listCollections() {
+ public ListCollectionsResponse listCollections(
+ Boolean detailed, String collection, String shard, String routeKey, Boolean prs)
+ throws Exception {
final ListCollectionsResponse response =
instantiateJerseyResponse(ListCollectionsResponse.class);
validateZooKeeperAwareCoreContainer(coreContainer);
+ if (Boolean.TRUE.equals(detailed)) {
+ if (collection != null) {
+ recordCollectionForLogAndTracing(collection, solrQueryRequest);
+ }
+ // Bind only the documented query params; anything else on the request is ignored.
+ final ModifiableSolrParams params = new ModifiableSolrParams();
+ params.setNonNull("collection", collection);
+ params.setNonNull("shard", shard);
+ params.setNonNull(ShardParams._ROUTE_, routeKey);
+ if (prs != null) {
+ params.set("prs", prs);
+ }
+ final ClusterStatus clusterStatus =
+ new ClusterStatus(coreContainer.getZkController().getZkStateReader(), params);
+ response.collectionsDetail = typedCollections(clusterStatus.getCollectionStatuses());
+ return response;
+ }
+
// resolve each name to ensure it exists.
// TODO https://issues.apache.org/jira/browse/SOLR-16909 to go direct to ZK?
List<String> collectionList =
@@ -63,4 +96,17 @@ public class ListCollections extends AdminAPIBase implements ListCollectionsApi
return response;
}
+
+ /**
+ * Bind the state maps onto the response types. Fields the model knows about are typed; every
+ * other entry stays on the object through its catch-all.
+ */
+ private static Map<String, CollectionState> typedCollections(Map<String, Object> raw) {
+ var mapper = SolrJacksonMapper.getObjectMapper();
+ Map<String, CollectionState> collections = CollectionUtil.newLinkedHashMap(raw.size());
+ for (Map.Entry<String, Object> entry : raw.entrySet()) {
+ collections.put(entry.getKey(), mapper.convertValue(entry.getValue(), CollectionState.class));
+ }
+ return collections;
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
index 4fc458bc589..3974894386d 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
@@ -29,7 +29,6 @@ import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.api.Api;
import org.apache.solr.api.ApiBag;
import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CollectionParams.CollectionAction;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
@@ -75,13 +74,6 @@ public class V2ClusterAPIMappingTest extends SolrTestCaseJ4 {
assertEquals(CollectionParams.CollectionAction.OVERSEERSTATUS.lowerName, v1Params.get(ACTION));
}
- @Test
- public void testClusterStatusAllParams() throws Exception {
- final SolrParams v1Params = captureConvertedV1Params("/cluster", "GET", null);
-
- assertEquals(CollectionAction.CLUSTERSTATUS.lowerName, v1Params.get(ACTION));
- }
-
@Test
public void testAddRoleAllParams() throws Exception {
final SolrParams v1Params =
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java
new file mode 100644
index 00000000000..48a08b7a9e3
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.api;
+
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.CollectionsApi;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** Standalone coverage for {@code GET /api/collections?detailed=true}. */
+public class ListCollectionsDetailedStandaloneTest extends SolrTestCase {
+
+ @ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule();
+
+ @BeforeClass
+ public static void setupSolr() throws Exception {
+ solrTestRule.startSolr(createTempDir());
+ }
+
+ @Test
+ public void testRequiresSolrCloud() {
+ var req = new CollectionsApi.ListCollections();
+ req.setDetailed(true);
+ final RemoteSolrException ex =
+ expectThrows(RemoteSolrException.class, () -> req.process(solrTestRule.getAdminClient()));
+ assertEquals(400, ex.code());
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
new file mode 100644
index 00000000000..93808b8a27a
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.api;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.solr.client.api.model.ListCollectionsResponse;
+import org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState;
+import org.apache.solr.client.api.model.ListCollectionsResponse.ReplicaState;
+import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.CollectionsApi;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.Utils;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * HTTP tests for {@code GET /api/collections?detailed=true}.
+ *
+ * <p>The response is the collections, shards, and replicas tree. Live nodes, the alias map, and
+ * cluster properties stay on their own endpoints (not this one). v1 {@code CLUSTERSTATUS} still
+ * returns them.
+ */
+public class ListCollectionsDetailedTest extends SolrCloudTestCase {
+
+ private static final String COLLECTION = "clusterstatuscoll";
+ private static final String ALIAS = "clusterstatusalias";
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure();
+ CollectionAdminRequest.createCollection(COLLECTION, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ CollectionAdminRequest.createAlias(ALIAS, COLLECTION).process(cluster.getSolrClient());
+ }
+
+ @Test
+ public void testReturnsCollectionTreeWithoutClusterLevelExtras() throws Exception {
+ Map<String, Object> body = getCollections("?detailed=true");
+ assertNull(body.get("cluster"));
+ assertNull(body.get("live_nodes"));
+ assertNull(body.get("aliases"));
+ assertNull(body.get("properties"));
+
+ Map<String, Object> collection = collection(body, COLLECTION);
+ assertNotNull(collection.get("health"));
+ assertEquals("conf", collection.get("configName"));
+ assertNotNull(collection.get("router"));
+ assertEquals(Set.of("shard1"), shards(collection).keySet());
+ assertNotNull(replica(collection).get("node_name"));
+ assertNotNull(replica(collection).get("state"));
+ assertTrue(aliasesOf(collection).contains(ALIAS));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGeneratedClientReadsTheTree() throws Exception {
+ var req = new CollectionsApi.ListCollections();
+ req.setDetailed(true);
+ ListCollectionsResponse response = req.process(cluster.getSolrClient());
+ assertNull(response.error);
+ assertNull(response.collections);
+ CollectionState collection = response.collectionsDetail.get(COLLECTION);
+ assertNotNull(collection);
+ assertEquals("conf", collection.configName);
+ assertNotNull(collection.health);
+ assertTrue(collection.aliases.contains(ALIAS));
+ assertNotNull(collection.router);
+ assertEquals("compositeId", collection.router.get("name"));
+ assertEquals(Integer.valueOf(1), collection.replicationFactor);
+
+ ReplicaState replica = collection.shards.get("shard1").replicas.values().iterator().next();
+ assertNotNull(replica.nodeName);
+ assertNotNull(replica.state);
+ assertEquals("true", replica.leader);
+ }
+
+ @Test
+ public void testWithoutDetailedReturnsPlainNameList() throws Exception {
+ var req = new CollectionsApi.ListCollections();
+ ListCollectionsResponse response = req.process(cluster.getSolrClient());
+ assertNull(response.error);
+ assertNull(response.collectionsDetail);
+ assertTrue(response.collections.contains(COLLECTION));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testPerReplicaState() throws Exception {
+ final String prsCollection = "prsclusterstatus";
+ CollectionAdminRequest.createCollection(prsCollection, "conf", 1, 1)
+ .setPerReplicaState(Boolean.TRUE)
+ .process(cluster.getSolrClient());
+
+ Map<String, Object> prs =
+ (Map<String, Object>)
+ collection(
+ getCollections("?detailed=true&collection=" + prsCollection + "&prs=true"),
+ prsCollection)
+ .get("PRS");
+ assertNotNull(prs);
+ assertNotNull(prs.get("states"));
+
+ var request = new CollectionsApi.ListCollections();
+ request.setDetailed(true);
+ request.setCollection(prsCollection);
+ request.setPrs(true);
+ CollectionState typed =
+ request.process(cluster.getSolrClient()).collectionsDetail.get(prsCollection);
+ assertNotNull(typed.unknownProperties().get("PRS"));
+ }
+
+ @Test
+ public void testCollectionShardAndAliasFilters() throws Exception {
+ Map<String, Object> byCollection =
+ collections(getCollections("?detailed=true&collection=" + COLLECTION));
+ assertEquals(Set.of(COLLECTION), byCollection.keySet());
+
+ Map<String, Object> oneShard =
+ shards(
+ collection(
+ getCollections("?detailed=true&collection=" + COLLECTION + "&shard=shard1"),
+ COLLECTION));
+ assertEquals(Set.of("shard1"), oneShard.keySet());
+
+ ContentResponse missingShard =
+ httpGet("?detailed=true&collection=" + COLLECTION + "&shard=nosuchshard");
+ assertEquals(400, missingShard.getStatus());
+
+ assertNotNull(
+ collections(getCollections("?detailed=true&collection=" + ALIAS)).get(COLLECTION));
+ }
+
+ @Test
+ public void testUnknownCollectionIsRejected() throws Exception {
+ ContentResponse response = httpGet("?detailed=true&collection=not-a-collection");
+ assertEquals(400, response.getStatus());
+ assertTrue(response.getContentAsString().contains("not found"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testV1ClusterStatusStillReturnsLiveNodes() throws Exception {
+ NamedList<Object> response =
+ CollectionAdminRequest.getClusterStatus().process(cluster.getSolrClient()).getResponse();
+ Map<String, Object> clusterState = (Map<String, Object>) response.get("cluster");
+ assertNotNull(clusterState.get("live_nodes"));
+ assertNotNull(clusterState.get("collections"));
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("action", "CLUSTERSTATUS");
+ params.set("liveNodes", false);
+ params.set("aliases", false);
+ params.set("clusterProperties", false);
+ NamedList<Object> collectionsOnly =
+ cluster
+ .getSolrClient()
+ .request(
+ new GenericSolrRequest(GET, "/admin/collections", SolrRequestType.ADMIN, params));
+ Map<String, Object> filtered = (Map<String, Object>) collectionsOnly.get("cluster");
+ assertNull(filtered.get("live_nodes"));
+ assertNull(filtered.get("aliases"));
+ assertNull(filtered.get("properties"));
+ assertNotNull(((Map<String, Object>) filtered.get("collections")).get(COLLECTION));
+ }
+
+ private static Map<String, Object> getCollections(String query) throws Exception {
+ ContentResponse response = httpGet(query);
+ assertEquals(response.getContentAsString(), 200, response.getStatus());
+ return parsed(response);
+ }
+
+ private static ContentResponse httpGet(String query) throws Exception {
+ HttpClient httpClient = cluster.getJettySolrRunner(0).getSolrClient().getHttpClient();
+ String url = cluster.getJettySolrRunner(0).getBaseURLV2().toString() + "/collections" + query;
+ return httpClient.GET(url);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> parsed(ContentResponse response) {
+ return (Map<String, Object>) Utils.fromJSONString(response.getContentAsString());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> collections(Map<String, Object> body) {
+ return (Map<String, Object>) body.get("collectionsDetail");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> collection(Map<String, Object> body, String name) {
+ Map<String, Object> collection = (Map<String, Object>) collections(body).get(name);
+ assertNotNull(collection);
+ return collection;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> shards(Map<String, Object> collection) {
+ return (Map<String, Object>) collection.get("shards");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<String> aliasesOf(Map<String, Object> collection) {
+ return (List<String>) collection.get("aliases");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> replica(Map<String, Object> collection) {
+ Map<String, Object> shard = (Map<String, Object>) shards(collection).get("shard1");
+ Map<String, Object> replicas = (Map<String, Object>) shard.get("replicas");
+ assertFalse(replicas.isEmpty());
+ return (Map<String, Object>) replicas.values().iterator().next();
+ }
+}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
index 26390f38c30..6d0e6c0e60d 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
@@ -38,7 +38,7 @@ Following are some v2 API URL paths and path prefixes, along with some of the op
[width="100%",options="header",]
|===
|Path prefix |Some Supported Operations
-|`/api/collections` |Create, alias, backup, and restore a collection.
+|`/api/collections` |Create, alias, backup, and restore a collection. Fetch the collections, shards, and replicas tree (`?detailed=true`).
|`/api/c/\{collectionName}/update` |Update requests.
|`/api/c/\{collectionName}/config` |Configuration requests.
|`/api/c/\{collectionName}/schema` |Schema requests.
@@ -49,7 +49,7 @@ Following are some v2 API URL paths and path prefixes, along with some of the op
|`/api/cores` |Create a core.
|`/api/cores/\{coreName}` |Reload, rename, delete, and unload a core.
|`/api/node` |Perform overseer operation, rejoin leader election.
-|`/api/cluster` |List live nodes, add role, remove role, set cluster property.
+|`/api/cluster` |List live nodes, manage node roles, set cluster property. `POST` sets the cluster rate limiter.
|===
== Introspect
@@ -149,13 +149,13 @@ curl http://localhost:8983/api/c/gettingstarted -H 'Content-type:application/jso
{"responseHeader":{"status":0,"QTime":842}}
----
-See the state of the cluster:
+See the collections, shards, and replicas in the cluster:
[source,bash]
----
-curl http://localhost:8983/api/cluster
+curl http://localhost:8983/api/collections?detailed=true
-{"responseHeader":{"status":0,"QTime":0},"collections":["gettingstarted"]}
+{"responseHeader":{"status":0,"QTime":1},"collectionsDetail":{"gettingstarted":{"health":"GREEN","shards":{"shard1":{"state":"active"}}}}}
----
Set a cluster property:
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
index 22ee652954a..8028a4d7d5e 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
@@ -27,6 +27,10 @@ These API commands work with a SolrCloud cluster at the entire cluster level, or
Fetch the cluster status including collections, shards, replicas, configuration name as well as collection aliases and cluster properties.
+NOTE: This is a v1-only command.
+The v2 API splits this information up: see xref:collection-management.adoc#collectionsdetail[Detailed Collections Tree] for the collections, shards, and replicas tree (`detailed=true`), and `GET /api/cluster/nodes`, `GET /api/aliases`, and `GET /api/cluster/properties` for live nodes, the alias map, and cluster properties, respectively.
+The two collection-tree responses overlap in content -- same `health` states, same shard/replica fields -- because they cover the same underlying data; they're documented separately because the parameters and response shape otherwise differ between v1 and v2 here.
+
Additionally, this command reports a `health` status of each collection and shard, in
order to make it easier to monitor the operational state of the collections.
The
@@ -44,28 +48,10 @@ No active replicas *OR* there's no shard leader.
The collection health state is reported as the worst state of any shard, e.g., for a collection with all shards GREEN except for one YELLOW the collection health will be reported as YELLOW.
-[tabs#clusterstatus-request]
-======
-V1 API::
-+
-====
[source,bash]
----
http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS
-
----
-====
-
-V2 API::
-+
-====
-[source,bash]
-----
-curl http://localhost:8983/api/cluster
-
-----
-====
-======
=== CLUSTERSTATUS Parameters
@@ -106,6 +92,7 @@ This can be used if you need the details of the shard where a particular documen
|Optional |Default: will default to the default value of `includeAll` parameter specified below
|===
+
+If set to true, returns the cluster alias map.
`liveNodes`::
+
@@ -114,7 +101,7 @@ This can be used if you need the details of the shard where a particular documen
|Optional |Default: will default to the default value of `includeAll` parameter specified below
|===
+
-If set to true, returns the status of live nodes in the cluster.
+If set to true, returns the live nodes in the cluster.
`clusterProperties`::
+
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index 8023f72bcb1..8dbc01b365a 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -510,6 +510,206 @@ curl http://localhost:8983/api/collections
"example2"]}
----
+[[collectionsdetail]]
+=== Detailed Collections Tree (V2 Only)
+
+With `detailed=true`, the V2 API returns the collections, shards, and replicas tree, in `collectionsDetail`, including each collection's configuration name and health, instead of the plain collection name list `LIST` returns by default.
+
+NOTE: This is a v2-only variant of `LIST`.
+See xref:cluster-node-management.adoc#clusterstatus[CLUSTERSTATUS] for the v1 command that returns similar information (plus live nodes, the alias map, and cluster properties).
+The two overlap in content -- same `health` states, same shard/replica fields -- because they cover the same underlying data; they're documented separately because the parameters and response shape otherwise differ between v1 and v2 here.
+
+This command reports a `health` status of each collection and shard, in order to make
+it easier to monitor the operational state of the collections.
+The following health
+state values are defined, ordered from the best to worst, based on the percentage of
+active replicas (`active`):
+
+`GREEN`::
+`active == 100%`, all replicas are active and there's a shard leader.
+`YELLOW`::
+`100% > active > 50%`, AND there's a shard leader.
+`ORANGE`::
+`50% >= active > 0%`, AND there's a shard leader.
+`RED`::
+No active replicas *OR* there's no shard leader.
+
+The collection health state is reported as the worst state of any shard, e.g., for a collection with all shards GREEN except for one YELLOW the collection health will be reported as YELLOW.
+
+[source,bash]
+----
+curl -X GET "http://localhost:8983/api/collections?detailed=true"
+----
+
+==== Detailed Collections Tree Parameters
+
+`detailed`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `false`
+|===
++
+Return the collections, shards, and replicas tree in `collectionsDetail` instead of the plain collection name list in `collections`.
+
+`collection`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+The collection or alias name for which information is requested.
+If omitted, information on all collections in the cluster will be returned.
+If an alias is supplied, information on the collections in the alias will be returned.
+
+`shard`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+The shard(s) for which information is requested.
+Multiple shard names can be specified as a comma-separated list.
+
+`\_route_`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+This can be used if you need the details of the shard where a particular document belongs to and you don't know which shard it falls under.
+
+`prs`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `false`
+|===
++
+Only used when `detailed=true`.
+Include per-replica state when the collection uses it.
+
+==== Detailed Collections Tree Response
+
+The response will include the status of the request and `collectionsDetail`, a map of collection name to collection state.
+
+==== Example
+
+*Input*
+
+[source,text]
+----
+http://localhost:8983/api/collections?detailed=true
+----
+
+*Output*
+
+[source,json]
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":333},
+ "collectionsDetail":{
+ "collection1":{
+ "shards":{
+ "shard1":{
+ "range":"80000000-ffffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node1":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8983_solr",
+ "base_url":"http://127.0.1.1:8983/solr",
+ "leader":"true"},
+ "core_node3":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8900_solr",
+ "base_url":"http://127.0.1.1:8900/solr"}}},
+ "shard2":{
+ "range":"0-7fffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node2":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:7574_solr",
+ "base_url":"http://127.0.1.1:7574/solr",
+ "leader":"true"},
+ "core_node4":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:7500_solr",
+ "base_url":"http://127.0.1.1:7500/solr"}}}},
+ "router":{"name":"compositeId"},
+ "replicationFactor":1,
+ "znodeVersion": 11,
+ "configName" : "my_config",
+ "health": "GREEN",
+ "aliases":["both_collections"]
+ },
+ "collection2":{"key": "value"}
+ }
+}
+----
+
+Adding `collection` and `shard` narrows the tree to just that shard of that collection:
+
+*Input*
+
+[source,text]
+----
+http://localhost:8983/api/collections?detailed=true&collection=collection1&shard=shard1
+----
+
+*Output*
+
+[source,json]
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":12},
+ "collectionsDetail":{
+ "collection1":{
+ "shards":{
+ "shard1":{
+ "range":"80000000-ffffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node1":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8983_solr",
+ "base_url":"http://127.0.1.1:8983/solr",
+ "leader":"true"},
+ "core_node3":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8900_solr",
+ "base_url":"http://127.0.1.1:8900/solr"}}}},
+ "router":{"name":"compositeId"},
+ "replicationFactor":1,
+ "znodeVersion": 11,
+ "configName" : "my_config",
+ "health": "GREEN",
+ "aliases":["both_collections"]
+ }
+ }
+}
+----
+
[[rename]]
== RENAME: Rename a Collection
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index f61b4e8d51c..1558918d09e 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -576,7 +576,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
var currentCollectionName = $route.current.params.core;
delete $scope.currentCollection;
if ($scope.isCloudEnabled) {
- CollectionsV2.listCollections(function (error, cdata, response) {
+ CollectionsV2.listCollections({}, function (error, cdata, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
AliasesV2.getAliases(function (error, adata, response) {
diff --git a/solr/webapp/web/js/angular/controllers/alias-overview.js b/solr/webapp/web/js/angular/controllers/alias-overview.js
index 6fbca386fe2..212493e8444 100644
--- a/solr/webapp/web/js/angular/controllers/alias-overview.js
+++ b/solr/webapp/web/js/angular/controllers/alias-overview.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('AliasOverviewController',
-function($scope, $routeParams, Collections, Constants) {
+function($scope, $routeParams, Constants) {
$scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE);
$scope.refresh = function() {
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js
index 2ce9a392d1c..87ae9eecf31 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('CloudController',
- function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2, Constants, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) {
+ function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2, Constants, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) {
$scope.showDebug = false;
@@ -37,7 +37,7 @@ solrAdminApp.controller('CloudController',
graphSubController($scope, $timeout, Zookeeper, ClusterV2, ApiErrorHandler);
} else if (view === "nodes") {
$scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
- nodesSubController($scope, $timeout, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler);
+ nodesSubController($scope, $timeout, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler);
} else if (view === "zkstatus") {
$scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
zkStatusSubController($scope, ZookeeperStatus, false);
@@ -107,7 +107,7 @@ function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
-var nodesSubController = function($scope, $timeout, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) {
+var nodesSubController = function($scope, $timeout, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) {
$scope.pageSize = 10;
$scope.showNodes = true;
$scope.showTree = false;
@@ -178,70 +178,74 @@ var nodesSubController = function($scope, $timeout, Collections, ClusterV2, Syst
var live_nodes = [];
// We build a node-centric view of the cluster state which we can easily consume to render the table
- Collections.status(function (data) {
- // Fetch cluster state from collections API and invert to a nodes structure
- for (var name in data.cluster.collections) {
- var collection = data.cluster.collections[name];
- collection.name = name;
- var shards = collection.shards;
- collection.shards = [];
- for (var shardName in shards) {
- var shard = shards[shardName];
- shard.name = shardName;
- shard.collection = collection.name;
- var replicas = shard.replicas;
- shard.replicas = [];
- for (var replicaName in replicas) {
- var core = replicas[replicaName];
- core.name = replicaName;
- core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1');
- core.collection = collection.name;
- core.shard = shard.name;
- core.shard_state = shard.state;
- core.label = core['collection'] + "_"
- + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1')
- + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1');
-
- var node_name = core['node_name'];
- var node = getOrCreateObj(node_name, nodes);
- var cores = getOrCreateList("cores", node);
- cores.push(core);
- node['base_url'] = core.base_url;
- node['id'] = core.base_url.replace(/[^\w\d]/g, '');
- node['host'] = node_name.split(":")[0];
- var collections = getOrCreateList("collections", node);
- ensureInList(core.collection, collections);
- ensureNodeInHosts(node_name, hosts);
+ CollectionsV2.listCollections({detailed: true}, function (error, data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+
+ // Fetch cluster state from CollectionsApi and invert to a nodes structure
+ for (var name in data.collectionsDetail) {
+ var collection = data.collectionsDetail[name];
+ collection.name = name;
+ var shards = collection.shards;
+ collection.shards = [];
+ for (var shardName in shards) {
+ var shard = shards[shardName];
+ shard.name = shardName;
+ shard.collection = collection.name;
+ var replicas = shard.replicas;
+ shard.replicas = [];
+ for (var replicaName in replicas) {
+ var core = replicas[replicaName];
+ core.name = replicaName;
+ core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1');
+ core.collection = collection.name;
+ core.shard = shard.name;
+ core.shard_state = shard.state;
+ core.label = core['collection'] + "_"
+ + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1')
+ + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1');
+
+ var node_name = core['node_name'];
+ var node = getOrCreateObj(node_name, nodes);
+ var cores = getOrCreateList("cores", node);
+ cores.push(core);
+ node['base_url'] = core.base_url;
+ node['id'] = core.base_url.replace(/[^\w\d]/g, '');
+ node['host'] = node_name.split(":")[0];
+ var collections = getOrCreateList("collections", node);
+ ensureInList(core.collection, collections);
+ ensureNodeInHosts(node_name, hosts);
+ }
}
}
- }
- ClusterV2.listClusterNodes(function (error, nodesData, response) {
- $timeout(function() {
- if (error) { ApiErrorHandler.handle(response); return; }
-
- live_nodes = nodesData.nodes;
- for (n in live_nodes) {
- node = live_nodes[n];
- if (!(node in nodes)) {
- var hostName = node.split(":")[0];
- nodes[node] = {};
- nodes[node]['host'] = hostName;
+ ClusterV2.listClusterNodes(function (error, nodesData, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+
+ live_nodes = nodesData.nodes;
+ for (n in live_nodes) {
+ node = live_nodes[n];
+ if (!(node in nodes)) {
+ var hostName = node.split(":")[0];
+ nodes[node] = {};
+ nodes[node]['host'] = hostName;
+ }
+ ensureNodeInHosts(node, hosts);
}
- ensureNodeInHosts(node, hosts);
- }
- // Make sure nodes are sorted alphabetically to align with rowspan in table
- for (var host in hosts) {
- hosts[host].nodes.sort();
- }
+ // Make sure nodes are sorted alphabetically to align with rowspan in table
+ for (var host in hosts) {
+ hosts[host].nodes.sort();
+ }
- $scope.nodes = nodes;
- $scope.hosts = hosts;
- $scope.live_nodes = live_nodes;
+ $scope.nodes = nodes;
+ $scope.hosts = hosts;
+ $scope.live_nodes = live_nodes;
- $scope.Math = window.Math;
- $scope.reload();
+ $scope.Math = window.Math;
+ $scope.reload();
+ });
});
});
});
diff --git a/solr/webapp/web/js/angular/controllers/collection-overview.js b/solr/webapp/web/js/angular/controllers/collection-overview.js
index d1834b2bd1e..9292da4f7c6 100644
--- a/solr/webapp/web/js/angular/controllers/collection-overview.js
+++ b/solr/webapp/web/js/angular/controllers/collection-overview.js
@@ -16,14 +16,17 @@
*/
solrAdminApp.controller('CollectionOverviewController',
-function($scope, $routeParams, Collections, Constants) {
+function($scope, $routeParams, $timeout, CollectionsV2, Constants, ApiErrorHandler) {
$scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE);
$scope.refresh = function() {
- Collections.status({}, function(data) {
- $scope.selectedCollection = data.cluster.collections[$routeParams.core];
- $scope.selectedCollection.name = $routeParams.core;
- $scope.rootUrl = Constants.ROOT_URL;
+ CollectionsV2.listCollections({detailed: true, collection: $routeParams.core}, function(error, data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+ $scope.selectedCollection = data.collectionsDetail[$routeParams.core];
+ $scope.selectedCollection.name = $routeParams.core;
+ $scope.rootUrl = Constants.ROOT_URL;
+ });
});
};
diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js
index 1c93b088b5e..17707f34243 100644
--- a/solr/webapp/web/js/angular/controllers/collections.js
+++ b/solr/webapp/web/js/angular/controllers/collections.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('CollectionsController',
- function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants, ApiErrorHandler){
+ function($scope, $routeParams, $location, $timeout, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants, ApiErrorHandler){
$scope.resetMenu("collections", Constants.IS_ROOT_PAGE);
$scope.refresh = function() {
@@ -30,13 +30,15 @@ solrAdminApp.controller('CollectionsController',
});
});
- Collections.status(function (data) {
+ CollectionsV2.listCollections({detailed: true}, function (error, data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
$scope.collections = [];
- for (var name in data.cluster.collections) {
+ for (var name in data.collectionsDetail) {
if (name.startsWith("._designer_")) {
continue;
}
- var collection = data.cluster.collections[name];
+ var collection = data.collectionsDetail[name];
collection.name = name;
collection.type = 'collection';
var shards = collection.shards;
@@ -85,6 +87,7 @@ solrAdminApp.controller('CollectionsController',
}
});
});
+ });
});
ConfigSetsV2.listConfigSet(function(error, data, response) {
$timeout(function() {
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index c0b525b1698..462d684121e 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -166,16 +166,6 @@ solrAdminServices.factory('Metrics',
delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
return new solrApi.ConfigApi();
})
-.factory('Collections',
- ['$resource', function ($resource) {
- // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler
- // that v1's CLUSTERSTATUS action used, so the response shape is byte-identical -- no
- // generated solrApi client class exists for it (old-style @EndPoint API, predates the
- // OpenAPI-based v2 framework), so this stays a plain $resource, like ParamSet.
- return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, {
- "status": {}
- });
- }])
.factory('ConfigSetFiles',
['$http', function ($http) {
// Fetches a single file from a configset via V2 /api/configsets/{name}/files/{path}.
@@ -276,7 +266,7 @@ solrAdminServices.factory('Metrics',
// v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still
// delegate straight through to the same v1 SolrConfigHandler, so the response shape is
// byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API,
- // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like Collections.
+ // predates the OpenAPI-based v2 framework), so this stays a plain $resource.
// NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a
// collection name (SolrCloud) or an actual core name (standalone/user-managed) --
// /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK),
@@ -395,7 +385,7 @@ solrAdminServices.factory('Metrics',
// body (the server deliberately reads the raw content stream, dispatched by Content-Type,
// rather than a formal parameter) and query() takes no query params at all (the server
// forwards arbitrary SolrParams straight through). Both stay on this plain $resource, like
- // Collections/ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2.
+ // ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2.
return $resource('/api/schema-designer/:configSet/:path', {wt: 'json', path: '@path', configSet: '@configSet', filePath: '@filePath', _:Date.now()}, {
get: {method: "GET"},
post: {method: "POST", timeout: 90000},
(solr) 02/10: SOLR-18450: Use /api/collections in test_auth.bats (#5055)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 3a86b4943babad87b0d65e7caadcd1cfd68b11a5
Author: Serhiy Bzhezytskyy <me...@serhiy-bzhezytskyy.com>
AuthorDate: Wed Oct 7 14:32:43 2026 +0300
SOLR-18450: Use /api/collections in test_auth.bats (#5055)
(cherry picked from commit b3345a97b0d59820bf75fefc2a0fcdfaf8268d90)
(cherry picked from commit bbac948d28ddd7f1192312e0a01dd21975597be7)
---
solr/packaging/test/test_auth.bats | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/solr/packaging/test/test_auth.bats b/solr/packaging/test/test_auth.bats
index b476541b392..4124d9845ad 100644
--- a/solr/packaging/test/test_auth.bats
+++ b/solr/packaging/test/test_auth.bats
@@ -53,11 +53,11 @@ setup() {
solr start
run solr auth enable --type basicAuth --credentials name:password -z localhost:${ZK_PORT}
assert_output --partial 'Successfully enabled basic auth'
- run curl -u name:password --basic "http://localhost:${SOLR_PORT}/api/cluster"
+ run curl -u name:password --basic "http://localhost:${SOLR_PORT}/api/collections"
assert_output --partial '"status":0'
solr auth disable -z localhost:${ZK_PORT}
- run curl "http://localhost:${SOLR_PORT}/api/cluster"
+ run curl "http://localhost:${SOLR_PORT}/api/collections"
assert_output --partial '"status":0'
solr stop --all
}
(solr) 03/10: SOLR-18031: Respect --filetypes for files and globs in bin/solr post (#5032)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit fd42ef4d7ff9716e2dda1cdec73074a4f631bac9
Author: sarkaramrit1993 <sa...@gmail.com>
AuthorDate: Wed Oct 7 13:01:52 2026 -0700
SOLR-18031: Respect --filetypes for files and globs in bin/solr post (#5032)
(cherry picked from commit d97bc70cefb84614ed6ecb61792df1155817340d)
(cherry picked from commit 93ccce22a8ec5b5a1e4dac6ab5ed33dc11c8e507)
---
changelog/unreleased/solr-18031-post-filetypes.yml | 7 ++++
.../src/java/org/apache/solr/cli/PostTool.java | 19 +++++++---
.../src/test/org/apache/solr/cli/PostToolTest.java | 40 ++++++++++++++++++++++
.../modules/indexing-guide/pages/post-command.adoc | 3 ++
4 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/changelog/unreleased/solr-18031-post-filetypes.yml b/changelog/unreleased/solr-18031-post-filetypes.yml
new file mode 100644
index 00000000000..65e19647a09
--- /dev/null
+++ b/changelog/unreleased/solr-18031-post-filetypes.yml
@@ -0,0 +1,7 @@
+title: bin/solr post now honors --filetypes for files and globs, and reports the right file count.
+type: fixed
+authors:
+ - name: Amrit Sarkar
+links:
+ - name: SOLR-18031
+ url: https://issues.apache.org/jira/browse/SOLR-18031
diff --git a/solr/core/src/java/org/apache/solr/cli/PostTool.java b/solr/core/src/java/org/apache/solr/cli/PostTool.java
index 33449d508c6..02acbb1c1ae 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java
@@ -482,6 +482,10 @@ public class PostTool extends ToolBase {
return Files.exists(srcFile);
}
+ private boolean matchesFileTypes(Path file) {
+ return !auto || fileFilter == null || fileFilter.test(file);
+ }
+
private static Collection<Path> listFiles(Path directory, Predicate<Path> fileFilter)
throws IOException {
Predicate<Path> filter = fileFilter != null ? fileFilter : p -> true;
@@ -523,7 +527,7 @@ public class PostTool extends ToolBase {
reset();
int filesPosted = 0;
for (int j = startIndexInArgs; j < args.length; j++) {
- filesPosted = getFilesPosted(out, type, args[j]);
+ filesPosted += getFilesPosted(out, type, args[j]);
}
return filesPosted;
}
@@ -536,7 +540,11 @@ public class PostTool extends ToolBase {
if (isValidPath && Files.isDirectory(srcFile) && Files.isReadable(srcFile)) {
filesPosted += postDirectory(srcFile, out, type);
} else if (isValidPath && Files.isRegularFile(srcFile) && Files.isReadable(srcFile)) {
- filesPosted += postFiles(List.of(srcFile), out, type);
+ if (matchesFileTypes(srcFile)) {
+ filesPosted += postFiles(List.of(srcFile), out, type);
+ } else {
+ warn("Skipping " + srcFile + ", its file type is not in " + fileTypes);
+ }
} else {
filesPosted += handleGlob(src, out, type);
}
@@ -612,9 +620,12 @@ public class PostTool extends ToolBase {
}
String fileGlob = globPath.getFileName().toString();
GlobFilter ff = new GlobFilter(fileGlob, false);
- Collection<Path> fileList = listFiles(parent, ff);
- if (fileList.isEmpty()) {
+ Collection<Path> matches = listFiles(parent, ff);
+ List<Path> fileList = matches.stream().filter(this::matchesFileTypes).toList();
+ if (matches.isEmpty()) {
warn("No files or directories matching " + globPath);
+ } else if (fileList.isEmpty()) {
+ warn("No files matching " + globPath + " have a file type in " + fileTypes);
} else {
filesPosted = postFiles(fileList, out, type);
}
diff --git a/solr/core/src/test/org/apache/solr/cli/PostToolTest.java b/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
index a238bd62c15..5390fbeafcf 100644
--- a/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
@@ -33,6 +33,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.apache.lucene.util.Constants;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrResponse;
@@ -293,6 +294,45 @@ public class PostToolTest extends SolrCloudTestCase {
assertEquals(2, num);
}
+ @Test
+ public void testFileTypesAppliedToFileArgs() throws IOException {
+ Path dir = createTempDir();
+ Path a = Files.writeString(dir.resolve("a.xml"), "<add/>");
+ Path b = Files.writeString(dir.resolve("B.XML"), "<add/>");
+ Path c = Files.writeString(dir.resolve("c.csv"), "id\n1\n");
+ Path d = Files.writeString(dir.resolve("noext"), "text");
+
+ PostTool postTool = dryRunPostTool("xml");
+ String[] files = {a.toString(), b.toString(), c.toString(), d.toString()};
+ assertEquals(2, postTool.postFiles(files, 0, null, null));
+
+ postTool.auto = false;
+ assertEquals(4, postTool.postFiles(files, 0, null, null));
+ }
+
+ @Test
+ public void testFileTypesAppliedToGlobs() throws IOException {
+ assumeFalse("'*' is not a valid path character on Windows", Constants.WINDOWS);
+ Path dir = createTempDir();
+ Files.writeString(dir.resolve("a.xml"), "<add/>");
+ Files.writeString(dir.resolve("B.XML"), "<add/>");
+ Files.writeString(dir.resolve("c.csv"), "id\n1\n");
+
+ PostTool postTool = dryRunPostTool("xml");
+ assertEquals(2, postTool.postFiles(new String[] {dir + "/*"}, 0, null, null));
+ assertEquals(0, postTool.postFiles(new String[] {dir + "/*.csv"}, 0, null, null));
+ }
+
+ private PostTool dryRunPostTool(String fileTypes) {
+ PostTool postTool = new PostTool(new CLITestHelper.TestingRuntime(false));
+ postTool.recursive = 0;
+ postTool.dryRun = true;
+ postTool.solrUpdateUrl = URI.create("http://localhost:8983/solr/fake/update");
+ postTool.fileTypes = fileTypes;
+ postTool.fileFilter = postTool.getFileFilterFromFileTypes(fileTypes);
+ return postTool;
+ }
+
@Test
public void testDetectingIfRecursionPossibleInFilesMode() throws IOException {
ToolRuntime runtime = new CLITestHelper.TestingRuntime(false);
diff --git a/solr/solr-ref-guide/modules/indexing-guide/pages/post-command.adoc b/solr/solr-ref-guide/modules/indexing-guide/pages/post-command.adoc
index 9c008b51ab3..b7a8cfb8738 100644
--- a/solr/solr-ref-guide/modules/indexing-guide/pages/post-command.adoc
+++ b/solr/solr-ref-guide/modules/indexing-guide/pages/post-command.adoc
@@ -121,6 +121,9 @@ Index all CSV and JSON files into `gettingstarted` from current directory:
$ bin/solr post -c gettingstarted --filetypes json,csv .
----
+`--filetypes` also applies to files named on the command line, including a shell-expanded `*`.
+A file with any other extension is skipped with a warning, unless `--type` is set.
+
Index a tab-separated file into `gettingstarted`:
[,console]
(solr) 04/10: SolrTestCase: reset system properties per test (#5056)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit e884a1bbd8acf70b3b096f0d7ff6a34b39a630e4
Author: David Smiley <ds...@apache.org>
AuthorDate: Wed Oct 7 18:21:33 2026 -0400
SolrTestCase: reset system properties per test (#5056)
(cherry picked from commit 9d7cc2884e8a843f33076e8171401e3f39d1f533)
(cherry picked from commit 447de11169e850691ecb3baa0b83a340becd5b9d)
---
.../src/java/org/apache/solr/SolrTestCase.java | 20 ++++++++------------
.../src/java/org/apache/solr/SolrTestCaseJ4.java | 6 ------
2 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
index 554f65b8d49..e467e583b39 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
@@ -57,12 +57,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * All Solr test cases should derive from this class eventually. This is originally a result of
- * async logging, see: SOLR-12055 and associated. To enable async logging, we must gracefully shut
- * down logging. Many Solr tests subclass LuceneTestCase.
+ * All Solr test cases should derive from this class.
*
- * <p>Rather than add the cruft from SolrTestCaseJ4 to all the Solr tests that currently subclass
- * LuceneTestCase, we'll add the shutdown to this class and subclass it.
+ * <p><em>Be conservative in what's here; avoid arbitrary/misc utilities.</em>
*
* <p>Other changes that should affect every Solr test case may go here if they don't require the
* added capabilities in SolrTestCaseJ4.
@@ -195,11 +192,14 @@ public class SolrTestCase extends LuceneTestCase {
}
@AfterClass
- public static void afterClassShutdownLogging() {
+ public static void afterClassSolrTestCase() {
StartupLoggingUtils.shutdown();
+ OpenTelemetryConfigurator.resetForTest();
}
- @Rule public TestRule methodRules = new LogLevelTestRule();
+ @Rule
+ public TestRule solrTestRules =
+ RuleChain.outerRule(new SystemPropertiesRestoreRule()).around(new LogLevelTestRule());
/**
* Special hook for sanity checking if any tests trigger failures when an Assumption failure
@@ -213,12 +213,8 @@ public class SolrTestCase extends LuceneTestCase {
assumeFalse(PROP + " == true", systemPropertyAsBoolean(PROP, false));
}
- @AfterClass
- public static void afterSolrTestCase() {
- OpenTelemetryConfigurator.resetForTest();
- }
-
// UTILITY METHODS FOLLOW
+ // NOTE: be very conservative in what we add!
public static void assertJSONEquals(String expected, String actual) {
Object json1 = fromJSONString(expected);
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
index fc9416924fa..402ce3c9633 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -22,7 +22,6 @@ import static org.hamcrest.core.StringContains.containsString;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.carrotsearch.randomizedtesting.RandomizedTest;
-import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
@@ -132,9 +131,6 @@ import org.apache.zookeeper.KeeperException;
import org.hamcrest.Matcher;
import org.junit.AfterClass;
import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
import org.noggit.CharArr;
import org.noggit.JSONUtil;
import org.noggit.ObjectBuilder;
@@ -230,8 +226,6 @@ public abstract class SolrTestCaseJ4 extends SolrTestCase {
// thread will read the latest value
public static volatile SSLTestConfig sslConfig;
- @Rule public TestRule solrTestRules = RuleChain.outerRule(new SystemPropertiesRestoreRule());
-
@BeforeClass
public static void setupTestCases() {
if (ignoreExceptionMuter != null) {
(solr) 09/10: SOLR-18166: Remove last vestiges of 'linkconfig' ZK storage
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit d43e574fd2abd1de2fdc69ae09b867891708618f
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Sep 17 18:44:19 2026 -0400
SOLR-18166: Remove last vestiges of 'linkconfig' ZK storage
(cherry picked from commit 31d089b1c95a3d5aee2d020975e03412f50d686f)
(cherry picked from commit 556552ad1221938e6b45ca4f161150b53c83a381)
---
.../org/apache/solr/cloud/api/collections/CollApiCmds.java | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
index ab8a8c58e4c..21435d961e1 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
@@ -430,16 +430,6 @@ public class CollApiCmds {
if (configName != null) {
CollectionHandlingUtils.validateConfigOrThrowSolrException(
ccc.getCoreContainer().getConfigSetService(), configName);
-
- // Back-compatibility reason: update configName in old location
- // TODO in Solr 10 this code should go away
- String collPath = ZkStateReader.COLLECTIONS_ZKNODE + "/" + collectionName;
- if (ccc.getSolrCloudManager().getDistribStateManager().hasData(collPath)) {
- ccc.getSolrCloudManager()
- .getDistribStateManager()
- .setData(
- collPath, Utils.toJSON(Map.of(ZkStateReader.CONFIGNAME_PROP, configName)), -1);
- }
}
ZkNodeProps m = cloneZkPropsWithOperation(message, MODIFYCOLLECTION);
(solr) 06/10: SOLR-18400: Plugins screen 500s when metrics are disabled (#4803)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit c1e73c13924cd4c4cf8c9f4249fe893b49151b83
Author: Jan Høydahl <ja...@apache.org>
AuthorDate: Thu Oct 8 08:33:22 2026 +0200
SOLR-18400: Plugins screen 500s when metrics are disabled (#4803)
(cherry picked from commit e34067ae6471af9b37f3da07409b0ab62c44f7bf)
(cherry picked from commit 06eddf725910e0e6b005c0a7a950d588bbb4df0e)
---
.../SOLR-18400-plugins-metrics-disabled.yml | 11 ++++
.../apache/solr/handler/admin/MetricsHandler.java | 4 +-
.../solr/handler/admin/MetricsDisabledTest.java | 65 +++++++++++++++++++
.../deployment-guide/pages/metrics-reporting.adoc | 3 +
.../AdminUiMetricsDisabledStandaloneTest.java | 74 ++++++++++++++++++++++
solr/webapp/web/js/angular/app.js | 5 +-
solr/webapp/web/js/angular/controllers/plugins.js | 7 ++
solr/webapp/web/partials/plugins.html | 3 +
8 files changed, 169 insertions(+), 3 deletions(-)
diff --git a/changelog/unreleased/SOLR-18400-plugins-metrics-disabled.yml b/changelog/unreleased/SOLR-18400-plugins-metrics-disabled.yml
new file mode 100644
index 00000000000..812f62b29f8
--- /dev/null
+++ b/changelog/unreleased/SOLR-18400-plugins-metrics-disabled.yml
@@ -0,0 +1,11 @@
+title: >
+ The metrics endpoints now answer HTTP 510 when metrics collection is disabled in solr.xml,
+ on both the v1 and v2 API. The v1 API used to return HTTP 500, which made the Admin UI
+ Plugins / Stats screen render blank. It now explains that metrics are disabled.
+type: fixed
+authors:
+ - name: Jan Høydahl
+ - name: Eric Pugh
+links:
+ - name: SOLR-18400
+ url: https://issues.apache.org/jira/browse/SOLR-18400
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 784676656ae..d8063bfe988 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
@@ -124,8 +124,8 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName
public void handleRequest(SolrParams params, BiConsumer<String, Object> consumer) {
if (!enabled) {
- consumer.accept("error", "metrics collection is disabled");
- return;
+ throw new SolrException(
+ SolrException.ErrorCode.INVALID_STATE, "Metrics collection is disabled");
}
Set<String> metricNames = MetricUtils.readParamsAsSet(params, MetricUtils.METRIC_NAME_PARAM);
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/MetricsDisabledTest.java b/solr/core/src/test/org/apache/solr/handler/admin/MetricsDisabledTest.java
new file mode 100644
index 00000000000..5f70377c444
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsDisabledTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 org.apache.solr.client.solrj.request.MetricsRequest;
+import org.apache.solr.client.solrj.response.InputStreamResponseParser;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Both the v1 {@code /admin/metrics} and the v2 {@code /api/metrics} endpoint answer HTTP 510 when
+ * metrics collection is switched off in solr.xml.
+ */
+public class MetricsDisabledTest extends SolrCloudTestCase {
+
+ private static final String METRICS_V2_PATH = "/metrics";
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ // MiniSolrCloudCluster's default solr.xml has <metrics enabled="false">
+ configureCluster(1).configure();
+ }
+
+ @Test
+ public void testV1MetricsDisabled() throws Exception {
+ assertMetricsDisabled(CommonParams.METRICS_PATH);
+ }
+
+ @Test
+ public void testV2MetricsDisabled() throws Exception {
+ assertMetricsDisabled(METRICS_V2_PATH);
+ }
+
+ private static void assertMetricsDisabled(String path) throws Exception {
+ var req = new MetricsRequest(path, SolrParams.of(CommonParams.WT, "prometheus"));
+
+ NamedList<Object> resp = cluster.getSolrClient().request(req);
+ String body = InputStreamResponseParser.consumeResponseToString(resp);
+
+ assertEquals(
+ "Expected HTTP 510 from " + path,
+ SolrException.ErrorCode.INVALID_STATE.code,
+ (int) (Integer) resp.get(InputStreamResponseParser.HTTP_STATUS_KEY));
+ assertTrue(body, body.contains("Metrics collection is disabled"));
+ }
+}
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
index 1be5df0b61b..880359491fd 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
@@ -171,6 +171,9 @@ The V2 `/metrics` endpoint is equivalent to the V1 `/admin/metrics` endpoint.
Examples on this page show only the V2 endpoint.
====
+Metrics collection can be switched off for a node with `<metrics enabled="false"/>` in `solr.xml`.
+Both endpoints then respond with `HTTP 510` and the message `Metrics collection is disabled`, which a Prometheus scraper reports as a target that is down.
+
=== Prometheus
See https://prometheus.io/docs/concepts/data_model/[Prometheus Data Model] documentation for more information on its data model.
diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiMetricsDisabledStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiMetricsDisabledStandaloneTest.java
new file mode 100644
index 00000000000..7a97de4c6be
--- /dev/null
+++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiMetricsDisabledStandaloneTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.webapp;
+
+import java.nio.file.Path;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+/**
+ * SOLR-18400: the Plugins screen on a node with metrics collection switched off in solr.xml. The
+ * metrics endpoint answers HTTP 510 there, which the screen must turn into an explanation rather
+ * than a blank page or the global error banner.
+ */
+public class AdminUiMetricsDisabledStandaloneTest extends AdminUiStandaloneTestBase {
+
+ private static final String CORE = "collection1";
+
+ @BeforeClass
+ public static void startStandaloneNode() throws Exception {
+ // the base class turned metrics on for the UI screens; this suite is about them being off.
+ // Must come before the node starts, and is restored after the class with the other properties
+ System.setProperty("metricsEnabled", "false");
+ Path home = buildStandaloneHome(CORE);
+ standaloneJetty = startStandaloneJetty(home);
+ baseUrl = standaloneJetty.getBaseUrl().toString();
+ assertFalse(
+ "fixture node should have metrics disabled",
+ standaloneJetty.getCoreContainer().getConfig().getMetricsConfig().isEnabled());
+ }
+
+ @AfterClass
+ public static void stopStandaloneNode() throws Exception {
+ if (standaloneJetty != null) {
+ standaloneJetty.stop();
+ standaloneJetty = null;
+ }
+ }
+
+ @Test
+ public void testPluginsScreenExplainsDisabledMetrics() {
+ openPage(CORE + "/plugins", By.id("plugins"));
+
+ String message = waitForText(By.cssSelector("#plugins .message-container .message"));
+ assertTrue(message, message.contains("Metrics collection is disabled"));
+
+ // no plugin categories or entries, as there is no metrics data to build them from
+ assertTrue(driver.findElements(By.cssSelector("#plugins #navigation a[rel]")).isEmpty());
+ assertTrue(driver.findElements(By.cssSelector("#plugins #frame li.entry")).isEmpty());
+
+ // the 510 is handled by the screen itself; the global error banner must stay away
+ assertTrue(
+ "global error banner should not show for a disabled feature",
+ driver.findElements(By.id("http-exception")).isEmpty());
+
+ // Chrome reports the failed XHR itself at SEVERE level; everything else must be clean
+ assertNoSevereConsoleErrors("510");
+ }
+}
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index 1558918d09e..aba91049975 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -429,6 +429,9 @@ solrAdminApp.config([
// Schema Designer and Security panels handle errors internally to provide a better user experience than the global error handler
var isHandledBySchemaDesigner = rejection.config.url && rejection.config.url.startsWith("/api/schema-designer/");
var isHandledBySecurity = rejection.config.url && rejection.config.url.startsWith("/api/cluster/security/");
+ // HTTP 510 means a feature is switched off in solr.xml, e.g. metrics collection. The screen
+ // asking for that data explains it in place, so skip the global error banner.
+ var isDisabledFeature = rejection.status === 510 && rejection.config.url && rejection.config.url.endsWith("admin/metrics");
if (rejection.status === 0) {
$rootScope.$broadcast('connectionStatusActive');
if (!$rootScope.retryCount) $rootScope.retryCount=0;
@@ -463,7 +466,7 @@ solrAdminApp.config([
} else if (isHandledBySecurity) {
// Let the security panel surface the detailed error in its own dialog
$rootScope.$broadcast('securityApiError', rejection);
- } else {
+ } else if (!isDisabledFeature) {
// schema designer prefers to handle errors itself
if (!isHandledBySchemaDesigner) {
$rootScope.exceptions[rejection.config.url] = rejection.data.error;
diff --git a/solr/webapp/web/js/angular/controllers/plugins.js b/solr/webapp/web/js/angular/controllers/plugins.js
index bf0dd9bcac7..157639980f0 100644
--- a/solr/webapp/web/js/angular/controllers/plugins.js
+++ b/solr/webapp/web/js/angular/controllers/plugins.js
@@ -35,6 +35,7 @@ solrAdminApp.controller('PluginsController',
var type = $location.search().type;
Metrics.raw(params, function (response) {
+ $scope.metricsDisabled = false;
$scope.types = getPluginTypesFromMetrics(response.data, type);
$scope.type = getSelectedType($scope.types, type);
@@ -44,6 +45,12 @@ solrAdminApp.controller('PluginsController',
} else {
$scope.plugins = [];
}
+ }, function (response) {
+ // Solr answers HTTP 510 when metrics collection is turned off in solr.xml
+ $scope.metricsDisabled = response.status === 510;
+ $scope.types = [];
+ $scope.type = null;
+ $scope.plugins = [];
});
};
diff --git a/solr/webapp/web/partials/plugins.html b/solr/webapp/web/partials/plugins.html
index 1e80b6f9b6e..f858f4ce6dd 100644
--- a/solr/webapp/web/partials/plugins.html
+++ b/solr/webapp/web/partials/plugins.html
@@ -17,6 +17,9 @@ limitations under the License.
<div id="plugins" class="clearfix">
<div id="frame">
+ <div class="message-container" ng-show="metricsDisabled">
+ <div class="message">Metrics collection is disabled in this node's solr.xml (<metrics enabled="false">), so no plugin information is available.</div>
+ </div>
<ul>
<li class="entry" ng-class="{changed: plugin.changed}" ng-repeat="plugin in type.plugins">
<a ng-click="selectPlugin(plugin)">
(solr) 07/10: SOLR-17820: Fix CLUSTERSTATUS health reporting GREEN with down replicas (#5046)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit a3603260eaa00ffe7a9f8421c09cd1624f9fbfae
Author: Eric Pugh <ep...@opensourceconnections.com>
AuthorDate: Thu Oct 8 12:15:06 2026 -0400
SOLR-17820: Fix CLUSTERSTATUS health reporting GREEN with down replicas (#5046)
(cherry picked from commit 7e3b17502194f83bc28bbfd99c665d523aaf7d59)
(cherry picked from commit 6b2edf4525a8bd34b199637caf25ebd890261bbc)
---
AGENTS.md | 1 +
...lr-17820-cluster-status-health-ordering-fix.yml | 8 +++
.../apache/solr/handler/admin/ClusterStatus.java | 8 ++-
.../solr/handler/admin/ClusterStatusTest.java | 69 ++++++++++++++++++++++
4 files changed, 83 insertions(+), 3 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 47cb9fb4ba6..2232abc0b4e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,6 +39,7 @@ While README.md and CONTRIBUTING.md are mainly written for humans, this file is
- Subclass SolrTestCase, or if SolrCloud is needed then SolrCloudTestCase
- If SolrTestCase and need to embed Solr, use either EmbeddedSolrServerTestRule (doesn't use HTTP) or SolrJettyTestRule if HTTP/Jetty is relevant to what is being tested.
- Avoid SolrTestCaseJ4 for new tests
+- System properties set in a test are restored after each test by the base classes (`SystemPropertiesRestoreRule`); do not add `try/finally` + `System.clearProperty` cleanup
- For BATS shell integration tests in `solr/packaging/test/`:
- Always use `run <command>` followed by `assert_output --partial "..."` or `refute_output --partial "..."` instead of capturing output into local variables and using `[[ ]]` comparisons
- Avoid patterns like `local var=$(cmd | grep ...); [[ "$var" == *"..."* ]]` — use `run cmd` + `assert_output`/`refute_output` instead
diff --git a/changelog/unreleased/solr-17820-cluster-status-health-ordering-fix.yml b/changelog/unreleased/solr-17820-cluster-status-health-ordering-fix.yml
new file mode 100644
index 00000000000..f55ddfa71d3
--- /dev/null
+++ b/changelog/unreleased/solr-17820-cluster-status-health-ordering-fix.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Fix CLUSTERSTATUS/collections-detail "health" reporting GREEN for a shard with a replica on a dead node, because health was computed before the live-node cross-check corrected that replica's stale state.json "active" entry.
+type: fixed
+authors:
+ - name: Eric Pugh
+links:
+ - name: SOLR-17820
+ url: https://issues.apache.org/jira/browse/SOLR-17820
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
index 7d07cdd67a8..aaa1d6f5383 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
@@ -428,6 +428,11 @@ public class ClusterStatus {
byte[] bytes = Utils.toJSON(clusterStateCollection);
@SuppressWarnings("unchecked")
Map<String, Object> docCollection = (Map<String, Object>) Utils.fromJSON(bytes);
+
+ // Replicas on dead nodes can still be marked active in state.json.
+ // Correct their states before computing health.
+ crossCheckReplicaStateWithLiveNodes(liveNodes, docCollection);
+
collectionStatus = getCollectionStatus(docCollection, name, shards);
collectionStatus.put("znodeVersion", clusterStateCollection.getZNodeVersion());
@@ -444,9 +449,6 @@ public class ClusterStatus {
collectionStatus.put("PRS", prs);
}
- // now we need to walk the collectionProps tree to cross-check replica state with live nodes
- crossCheckReplicaStateWithLiveNodes(liveNodes, collectionStatus);
-
return collectionStatus;
}
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ClusterStatusTest.java b/solr/core/src/test/org/apache/solr/handler/admin/ClusterStatusTest.java
new file mode 100644
index 00000000000..54b68a2d16c
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/ClusterStatusTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Test;
+
+/** Tests that cluster health reflects replica states corrected against live nodes. */
+public class ClusterStatusTest extends SolrTestCase {
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testHealthReflectsReplicaOnDeadNode() {
+ // Both replicas are marked active, but only node1 is live.
+ Map<String, Object> replica1 = new LinkedHashMap<>();
+ replica1.put("state", "active");
+ replica1.put("node_name", "node1:8983_solr");
+ replica1.put("leader", "true");
+
+ Map<String, Object> replica2 = new LinkedHashMap<>();
+ replica2.put("state", "active");
+ replica2.put("node_name", "node2:8983_solr");
+
+ Map<String, Object> replicas = new LinkedHashMap<>();
+ replicas.put("core_node1", replica1);
+ replicas.put("core_node2", replica2);
+
+ Map<String, Object> shard1 = new LinkedHashMap<>();
+ shard1.put("replicas", replicas);
+
+ Map<String, Object> shards = new LinkedHashMap<>();
+ shards.put("shard1", shard1);
+
+ Map<String, Object> docCollection = new LinkedHashMap<>();
+ docCollection.put("shards", shards);
+
+ List<String> liveNodes = List.of("node1:8983_solr");
+
+ // Match buildResponseForCollection: correct replica states, then compute health.
+ ClusterStatus clusterStatus = new ClusterStatus(null, new ModifiableSolrParams());
+ clusterStatus.crossCheckReplicaStateWithLiveNodes(liveNodes, docCollection);
+ Map<String, Object> result = ClusterStatus.postProcessCollectionJSON(docCollection);
+
+ assertEquals("down", replica2.get("state"));
+ // One of two replicas is active, so shard and collection health are ORANGE.
+ Map<String, Object> resultShard1 =
+ (Map<String, Object>) ((Map<String, Object>) result.get("shards")).get("shard1");
+ assertEquals("ORANGE", resultShard1.get("health"));
+ assertEquals("ORANGE", result.get("health"));
+ }
+}
(solr) 08/10: SOLR-18520: Package Uninstall should work in absence of clusterprops.json (#5052)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit c8c86434cd27cf4dabebbefcbcec3afb69781d78
Author: Jalaz Kumar <ja...@gmail.com>
AuthorDate: Thu Oct 8 21:01:13 2026 +0530
SOLR-18520: Package Uninstall should work in absence of clusterprops.json (#5052)
(cherry picked from commit 2f2b21be654f10534f1e1f995acd104470dba677)
(cherry picked from commit 9ebb012fbb6dea6aef11cf36048aa1d30853b7a1)
---
changelog/unreleased/SOLR-18520-uninstall-bug.yml | 9 +++++++++
.../java/org/apache/solr/packagemanager/PackageManager.java | 13 +++++++------
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/changelog/unreleased/SOLR-18520-uninstall-bug.yml b/changelog/unreleased/SOLR-18520-uninstall-bug.yml
new file mode 100644
index 00000000000..6e66dcb25cb
--- /dev/null
+++ b/changelog/unreleased/SOLR-18520-uninstall-bug.yml
@@ -0,0 +1,9 @@
+title:
+ Package Uninstall should work in absence of clusterprops.json file
+type: fixed
+authors:
+ - name: Jalaz Kumar
+ nick: jaykay12
+links:
+ - name: SOLR-18520
+ url: https://issues.apache.org/jira/browse/SOLR-18520
diff --git a/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java b/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
index b8c38274f8d..e344f8ec35d 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
@@ -42,6 +42,7 @@ import org.apache.solr.cli.CLIUtils;
import org.apache.solr.cli.SolrCLI;
import org.apache.solr.cli.ToolRuntime;
import org.apache.solr.client.api.util.SolrVersion;
+import org.apache.solr.client.solrj.RemoteSolrException;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
@@ -154,7 +155,7 @@ public class PackageManager implements Closeable {
del.pkg = packageName;
V2Request req =
- new V2Request.Builder(PackageUtils.PACKAGE_PATH)
+ new V2Request.Builder("/cluster/package")
.forceV2(true)
.withMethod(SolrRequest.METHOD.POST)
.withPayload(Map.of("delete", del))
@@ -278,18 +279,18 @@ public class PackageManager implements Closeable {
Map<String, String> packageVersions = new HashMap<>();
// map of package name to multiple values of pluginMeta(Map<String, String>)
Map<String, Set<PluginMeta>> packagePlugins = new HashMap<>();
- Object pluginsValue;
+ Object pluginsValue = null;
try {
NamedList<Object> response =
solrClient.request(
new GenericV2SolrRequest(SolrRequest.METHOD.GET, PackageUtils.CLUSTERPROPS_PATH));
Integer statusCode = (Integer) response._get(List.of("responseHeader", "status"), null);
- if (statusCode == null || statusCode == ErrorCode.NOT_FOUND.code) {
- // Cluster props doesn't exist, that means there are no cluster level plugins installed.
- pluginsValue = null;
- } else {
+ if (statusCode != null && statusCode != ErrorCode.NOT_FOUND.code) {
+ // Cluster props exists, that means there are cluster level plugins installed.
pluginsValue = response.get(ContainerPluginsApi.PLUGIN);
}
+ } catch (RemoteSolrException ex) {
+ log.debug("Unable to fetch clusterprops for package plugins", ex);
} catch (SolrServerException | IOException ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
(solr) 10/10: Documented V2 API Conventions (#4826)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit a8cda556154de37c296f203e3f5e8e13e836ed23
Author: Eric Pugh <ep...@opensourceconnections.com>
AuthorDate: Fri Sep 25 10:20:15 2026 -0400
Documented V2 API Conventions (#4826)
(cherry picked from commit bc428ba81bc0ce1dad04a8032dfc35966166136d)
(cherry picked from commit 82f00f105a36f51ab37d3c9254fb225d7e7bc25f)
---
AGENTS.md | 2 +-
...i-conventions.adoc => apis-v2-conventions.adoc} | 39 ++++++++++-
dev-docs/apis.adoc | 81 +++++++++++++++++++++-
3 files changed, 119 insertions(+), 3 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 2232abc0b4e..6fc83465cd7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,7 +63,7 @@ Internals:
- `dev-docs/shard-split/shard-split.adoc` — SPLITSHARD: shard/replica states, tlog buffering during split
- `dev-docs/distributed-update-internals.adoc` — SolrCloud update path: routing, `_version_`/optimistic concurrency, tlog durability, replication acks, shard terms (user-facing consistency model: ref-guide page `solrcloud-update-consistency.adoc`)
- `dev-docs/plugins-modules-packages.adoc` — plugin/module/package concepts
-- `dev-docs/apis.adoc`, `dev-docs/v2-api-conventions.adoc` — API design and v2 conventions
+- `dev-docs/apis.adoc`, `dev-docs/apis-v2-conventions.adoc` — API design and v2 conventions
- `dev-docs/ui/` — new Admin UI architecture, component development, testing
Process & tooling:
diff --git a/dev-docs/v2-api-conventions.adoc b/dev-docs/apis-v2-conventions.adoc
similarity index 59%
rename from dev-docs/v2-api-conventions.adoc
rename to dev-docs/apis-v2-conventions.adoc
index 0dc61247bea..945c94afe72 100644
--- a/dev-docs/v2-api-conventions.adoc
+++ b/dev-docs/apis-v2-conventions.adoc
@@ -65,7 +65,7 @@ Solr's "modify collection" functionality uses partial update semantics, but the
For use within the v2 API, the four "popular" HTTP methods have the following semantics and implications:
-* `GET` - used for non-mutating (i.e. "read only") requests. Most often used to list elements of a particular resource type, or fetch information about a specific named resource.
+* `GET` - used for non-mutating (i.e. "read only") requests. Most often used to list elements of a particular resource type, or fetch information about a specific named resource. Do not use for requests like queries that change state.
* `POST` - used for non-idempotent resource modifications.
* `PUT` - used for idempotent resource modifications.
* `DELETE` - Used to delete or cleanup resource
@@ -142,3 +142,40 @@ In these cases, developers may:
AddFieldOperation requestBody)
throws Exception;
```
+
+== Response POJOs
+
+Every v2 response body extends `SolrJerseyResponse`, which provides `responseHeader` and `error` - except for the raw/streaming APIs described under "Streaming" above, whose interface methods return `StreamingOutput` instead.
+Beyond that base, pick the narrowest existing response type that fits before writing a new one:
+
+* `SolrJerseyResponse` - plain success/error response with no extra data. The default for simple mutations.
+* `AsyncJerseyResponse extends SolrJerseyResponse` - adds a `requestId` field. Use this (or a subclass of it) for any API that supports the `async` request-body parameter, since `submitRemoteMessageAndHandleAsync`/`handlePotentiallyAsynchronousTask` populate `requestId` automatically when an async id is present.
+* `SubResponseAccumulatingJerseyResponse extends AsyncJerseyResponse` - adds `successfulSubResponsesByNodeName`, `failedSubResponsesByNodeName`, and `warning`. Use this for Overseer-driven APIs that fan out to multiple nodes/replicas (e.g. `CreateShard`, `DeleteShard`) - `AdminAPIBase.submitRemoteMessageAndHandleResponse` populates these fields from the Overseer's `success`/`failure`/`warning` NamedList entries automatically.
+* `FlexibleSolrJerseyResponse extends SolrJerseyResponse` - adds `@JsonAnyGetter`/`@JsonAnySetter`-backed dynamic *top-level* properties, for APIs whose entire response shape is genuinely open-ended (e.g. `SchemaDesigner`, the `Select` query API). This is different from the dynamic-request-POJO pattern above: it's for responses, and the dynamism applies to the whole top level rather than one field.
+
+If an API returns specific, known additional data beyond these bases (e.g. timing information, computed ranges, a resource's status fields), extend the appropriate base with typed `@JsonProperty` fields rather than reaching for `FlexibleSolrJerseyResponse` - see `SplitShardResponse.timing`, `SplitCoreResponse.ranges`, or `CollectionStatusResponse` for examples.
+Don't settle for a bare `SolrJerseyResponse` if the underlying operation actually produces more than a bare success/error - a v2 JSON caller has no other way to get that data, since (unlike v1) nothing else in the response pipeline will surface it (see the async caveat below for why this matters in practice).
+
+== Async Task Handling
+
+Solr has two distinct, non-interchangeable mechanisms for handling the `async` request parameter, depending on which base class the API extends:
+
+* **Core-level (`CoreAdminAPIBase`)**: `handlePotentiallyAsynchronousTask(response, coreName, taskId, actionName, supplier)`. If `taskId` is null, the supplier runs inline and its result is returned directly. If non-null, the supplier is wrapped in a `CoreAdminAsyncTracker.TaskObject` and submitted to `coreAdminAsyncTracker`, which tracks status for later `REQUESTSTATUS` polling. Because the supplier is a `Supplier<T>`, it cannot throw checked exceptions directly - wrap them in `CoreAdmi [...]
+* **Collection-level (`AdminAPIBase`)**: `submitRemoteMessageAndHandleAsync`/`submitRemoteMessageAndHandleResponse(response, action, remoteMessage, asyncId[, timeoutMs])`. The `asyncId` is baked into the submitted `ZkNodeProps` message and handled by the Overseer's own async-tracking machinery; `response.requestId` is populated automatically when `asyncId` is non-null. A timeout-aware overload exists for APIs (like shard split) that legitimately need longer than `CollectionsHandler.DEFAU [...]
+
+Only wrap the branches of an API that are actually meant to support async execution.
+A synchronous-by-design sub-operation (e.g. a "dry run" or "compute recommendations" branch that happens to share a method with the real mutating operation) should bypass async handling entirely rather than being routed through it - see the `async` caveat under "Relationship Between V1 and V2 Implementations" in link:apis.adoc[apis.adoc] for a concrete failure mode this avoids.
+
+== Testing V2 APIs
+
+Newer v2 API tests favor exercising the real HTTP stack over mocking it:
+
+1. Start a real embedded Solr instance with `SolrJettyTestRule` (a `@ClassRule`) rather than the older `initCore()`/mock-`SolrQueryRequest` style.
+2. Issue requests through the *generated* SolrJ client (e.g. `CoresApi.SplitCore`, `ShardsApi.SplitShard`) rather than hand-built `GenericV2SolrRequest`s, so the test also exercises the client codegen.
+3. Give each API its own dedicated test class under `handler.admin.api` (e.g. `RenameCoreAPITest`, `SplitCoreAPITest`, `SplitShardAPITest`), rather than adding cases to a large shared mapping test.
+4. Assert error responses via `expectThrows(RemoteSolrException.class, () -> request.process(client))`, then check `.code()` and `.getMessage()` - a non-2xx v2 response surfaces as a thrown exception on the client side, not as a populated `.error` field on a normally-returned response object.
+
+This has superseded the older `V2ApiMappingTest`-based style (e.g. `V2CoreAPIMappingTest`), which mocks the v1 handler and asserts on the `SolrParams` the v2-to-v1 conversion produces.
+That style still exists for APIs that haven't been migrated to the "logic lives in v2" pattern yet, but shouldn't be extended for newly-migrated APIs - once an API's logic moves into v2, its mapping test loses its reason to exist and should be removed (see "Relationship Between V1 and V2 Implementations" in link:apis.adoc[apis.adoc]) in favor of a real-client test.
+
+One exception: white-box tests that need access to package-private helper methods (e.g. `SplitHandlerTest`, which tests `SplitCoreAPI`'s histogram/range-recommendation internals) should live in the *same package* as the class under test, and move with it if that class's package ever changes.
diff --git a/dev-docs/apis.adoc b/dev-docs/apis.adoc
index fa0565a14d9..b394b16d452 100644
--- a/dev-docs/apis.adoc
+++ b/dev-docs/apis.adoc
@@ -51,7 +51,7 @@ Separating the API "definition" and "implementation" in this way allows us to on
Writing a new v2 API may appear daunting, but additions in reality are actually pretty simple:
-. *Agree on Endpoint Syntax*: Before implementing, developers should generate consensus around what the new API will look like. General guidelines and conventions for the v2 API syntax are described <<v2-api-conventions.adoc,here>>.
+. *Agree on Endpoint Syntax*: Before implementing, developers should generate consensus around what the new API will look like. General guidelines and conventions for the v2 API syntax are described <<apis-v2-conventions.adoc,here>>.
. *Create POJO ("Plain Old Java Object") classes as needed to represent the API's request and response*:
** POJOs are used to represent both the body of the API request (for some `POST` and `PUT` endpoints), as well as the response from the API.
** Re-use of existing classes here is preferred where possible. A library of available POJOs can be found in the `org.apache.solr.client.api.model` package of the `api` gradle project.
@@ -76,6 +76,85 @@ Writing a new v2 API may appear daunting, but additions in reality are actually
A good example for each of these steps can be seen in Solr's v2 "add-replica-property" API, which has a defining interface https://github.com/apache/solr/blob/9426902acb7081a2e9a1fa29699c5286459e1365/solr/api/src/java/org/apache/solr/client/api/endpoint/AddReplicaPropertyApi.java[AddReplicaPropertyApi], an implementing class https://github.com/apache/solr/blob/9426902acb7081a2e9a1fa29699c5286459e1365/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java[AddReplicaP [...]
+=== Relationship Between V1 and V2 Implementations
+
+Most v2 APIs have a corresponding legacy v1 API (e.g. `/admin/cores?action=RELOAD` backs `POST /api/cores/coreName/reload`).
+Where both exist, **the actual business logic should live in the v2 implementation class, and the v1 handler should delegate to it** - not the other way around.
+This convention exists for a few reasons:
+
+1. It makes it easier to delete the v1 code down the road.
+2. It prevents the v2 code from "falling behind" or being forgotten when query params or API functionality changes, since v1 traffic continuously exercises the same v2 code path.
+3. It gives the v2 endpoint test coverage "for free" - most of Solr's existing test suite targets v1 APIs, so having v1 call v2 gives confidence that the v2 endpoint works correctly even in the absence of direct v2-specific tests.
+
+How this delegation is implemented differs depending on whether the API executes synchronously or is processed by the Overseer.
+
+==== Synchronous (CoreAdminAPIBase) APIs
+
+Note: this is distinct from "per-core" APIs generally (see the "API Types" section above), which are registered on individual `SolrCore` objects (e.g. `/select`, `/update`, `/admin/ping`). `CoreAdminAPIBase`-derived APIs are instead a "cluster" API registered on the `CoreContainer`, corresponding to the legacy `/admin/cores` handler.
+
+For `CoreAdminAPIBase`-derived APIs (i.e. `/api/cores/...`), the v1 `CoreAdminHandler.CoreAdminOp` implementation is reduced to a thin adapter: parse the v1 `SolrParams` into the v2 request-body POJO, construct the v2 API class, call it, and squash the response back into the v1 `NamedList` via `V2ApiUtils.squashIntoSolrResponseWithoutHeader`.
+
+```java
+class MergeIndexesOp implements CoreAdminHandler.CoreAdminOp {
+ @Override
+ public void execute(CoreAdminHandler.CallInfo it) throws Exception {
+ SolrParams params = it.req.getParams();
+ final var requestBody = new MergeIndexesRequestBody();
+ // ... populate requestBody from params ...
+ final var mergeIndexesApi =
+ new MergeIndexes(it.handler.coreContainer, it.handler.coreAdminAsyncTracker, it.req, it.rsp);
+ final var response = mergeIndexesApi.mergeIndexes(cname, requestBody);
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
+ }
+}
+```
+
+See `RenameCore`/`CoreAdminOperation.RENAME_OP`, `MergeIndexes`/`MergeIndexesOp`, and `SplitCoreAPI`/`SplitOp` for examples of this pattern.
+
+==== Overseer-driven (AdminAPIBase) APIs
+
+Note: not all collection-level APIs are Overseer-driven, and that's expected to become even less true over time. This section covers only the subset of `AdminAPIBase`-derived APIs that are processed asynchronously by the Overseer via a ZK-based command queue.
+
+For these APIs (i.e. `/api/collections/...`), the underlying `cloud/api/collections/*Cmd.java` executor (e.g. `SplitShardCmd`, `CreateShardCmd`) is *not* moved into v2 - it stays registered in `CollApiCmds.java` and continues to execute on the Overseer thread exactly as before.
+"Logic in v2" here means the v2 class is responsible for:
+
+1. Validating parameters (including any cross-parameter conflict checks previously done by the v1 handler, so the check applies uniformly to v1 and native v2 requests)
+2. Building the `ZkNodeProps` "remote message" the Overseer command expects
+3. Submitting it via `AdminAPIBase.submitRemoteMessageAndHandleResponse`/`submitRemoteMessageAndHandleAsync` - the same ZK-queue submission machinery v1 always used
+
+The v1 `CollectionOperation` enum entry is then reduced to a one-line delegation:
+
+```java
+SPLITSHARD_OP(
+ SPLITSHARD,
+ (req, rsp, h) -> {
+ SplitShardAPI.invokeWithV1Params(h.coreContainer, req, rsp);
+ return null;
+ }),
+```
+
+Returning `null` from a `CollectionOperation` lambda tells `CollectionsHandler.invokeAction()` that the request has already been fully handled.
+Returning a non-null `Map<String, Object>` instead falls back to the older pattern where `CollectionsHandler` itself builds and submits the `ZkNodeProps` message generically - this is still the state of many not-yet-migrated v1 actions, and is *not* an example to follow for new migrations.
+
+See `CreateShard`/`CREATESHARD_OP`, `DeleteShard`/`DELETESHARD_OP`, and `SplitShardAPI`/`SPLITSHARD_OP` for examples of the target pattern.
+
+==== Standard v1-delegation entry points
+
+By convention, a migrated v2 API class exposes two `public static` methods for v1 to call:
+
+* `createRequestBodyFromV1Params(SolrParams params)` - parses legacy params into the v2 request-body POJO.
+* `invokeWithV1Params(CoreContainer, SolrQueryRequest, SolrQueryResponse)` (sometimes named `invokeFromV1Params` - naming isn't fully consistent yet) - constructs the v2 API class, calls it, and squashes the response into the v1 `SolrQueryResponse`.
+
+==== Don't forward `async` from v1
+
+`CoreAdminHandler.handleRequestBody()` and `CollectionsHandler.invokeAction()` already wrap the *entire* v1 dispatch in their own async-task submission, keyed by the request's `async` parameter, before the `CoreAdminOp`/`CollectionOperation` even runs.
+If a v1 adapter also copies that same `async` value into the v2 request body, the v2 class's own async handling (`handlePotentiallyAsynchronousTask`, `submitRemoteMessageAndHandleAsync`) will try to register the identical task id a second time, and fail with `Duplicate request with the same requestid found.`
+
+*Leave the v2 request body's `async` field unset when delegating from v1.*
+`MergeIndexesRequestBody` has no `async` field at all, and `SplitOp`'s v1 adapter deliberately omits setting one - both are correct examples of this. This only applies to genuine v1-delegation adapters; native v2 JSON requests should set `async` normally, and the v2 class's own handling of it is unaffected.
+
+Watch for this same class of bug in *internal* sub-requests too: some Overseer commands (e.g. `SplitShardCmd`'s `splitByPrefix` sub-request, sent via `ShardRequestTracker.sendShardRequest`) piggyback an internal correlation id on the `async` param purely for response tracking, not because the sub-request is meant to be tracked as a real async task. If a migrated v2 method routes such a sub-request through its own async wrapper, the same "duplicate requestid" failure results. Keep any suc [...]
+
=== Legacy v2 API Framework
While we've settled on JAX-RS as our framework for defining v2 APIs going forward, Solr still retains many v2 APIs that were written using an older homegrown framework.
(solr) 05/10: SOLR-17398: Link V1 collection operations to their V2 equivalents (#5053)
Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git
commit fb56d1f6cc0f2d40bd1def3d3c2243c3908fc290
Author: Serhiy Bzhezytskyy <me...@serhiy-bzhezytskyy.com>
AuthorDate: Wed Oct 7 23:14:04 2026 +0300
SOLR-17398: Link V1 collection operations to their V2 equivalents (#5053)
(cherry picked from commit 1d7a1e9062734acd37489e85173044d8d974c88a)
(cherry picked from commit 4c88380c161c31e40f35f9c5ab4e943dbef865f8)
---
.../solr/handler/admin/CollectionsHandler.java | 42 ++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index 05a47f4106c..12055863ea6 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -114,6 +114,11 @@ import java.util.concurrent.TimeoutException;
import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.ClusterPropertyApis;
+import org.apache.solr.client.api.endpoint.ListAliasesApi;
+import org.apache.solr.client.api.endpoint.ListClusterNodesApi;
+import org.apache.solr.client.api.endpoint.ListCollectionsApi;
+import org.apache.solr.client.api.endpoint.RenameCollectionApi;
import org.apache.solr.client.api.model.AddReplicaPropertyRequestBody;
import org.apache.solr.client.api.model.CreateAliasRequestBody;
import org.apache.solr.client.api.model.CreateCollectionSnapshotRequestBody;
@@ -160,6 +165,7 @@ import org.apache.solr.core.CloudConfig;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.snapshots.CollectionSnapshotMetaData;
import org.apache.solr.core.snapshots.SolrSnapshotManager;
+import org.apache.solr.handler.ClusterAPI;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.admin.api.AddReplicaProperty;
import org.apache.solr.handler.admin.api.AdminAPIBase;
@@ -538,6 +544,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
return null;
}),
+ /**
+ * V2 equivalent: {@link RenameCollectionApi} ({@code POST
+ * /collections/{collectionName}/rename}).
+ */
RENAME_OP(
RENAME,
(req, rsp, h) -> {
@@ -546,6 +556,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
return copy(req.getParams(), map, FOLLOW_ALIASES);
}),
+ /** No V2 equivalent yet. */
REINDEXCOLLECTION_OP(
REINDEXCOLLECTION,
(req, rsp, h) -> {
@@ -631,6 +642,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, getAliasesResponse);
return null;
}),
+ /**
+ * V2 equivalent: {@link SplitShardAPI} ({@code POST /collections/{collection}/shards} with the
+ * {@code split} command).
+ */
SPLITSHARD_OP(
SPLITSHARD,
DEFAULT_COLLECTION_OP_TIMEOUT * 5,
@@ -708,6 +723,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
DeleteReplica.invokeWithV1Params(h.coreContainer, req, rsp);
return null;
}),
+ /**
+ * V2 equivalent: {@link MigrateDocsAPI} ({@code POST /collections/{collection}} with the {@code
+ * migrate-docs} command).
+ */
MIGRATE_OP(
MIGRATE,
(req, rsp, h) -> {
@@ -792,7 +811,12 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
return null;
}),
+ /**
+ * V2 equivalent: {@link ClusterAPI#getOverseerStatus} ({@code GET /cluster/overseer}), which
+ * calls this operation.
+ */
OVERSEERSTATUS_OP(OVERSEERSTATUS, (req, rsp, h) -> new LinkedHashMap<>()),
+ /** No V2 equivalent yet. */
DISTRIBUTEDAPIPROCESSING_OP(
DISTRIBUTEDAPIPROCESSING,
(req, rsp, h) -> {
@@ -817,6 +841,12 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
* Handle cluster status request. Can return status per specific collection/shard or per all
* collections.
*/
+ /**
+ * Superseded by V2, which splits it up: {@link ListCollectionsApi} ({@code GET
+ * /collections?detailed=true}) for the collections tree, {@link ListClusterNodesApi} ({@code
+ * GET /cluster/nodes}), {@link ListAliasesApi} ({@code GET /aliases}) and {@link
+ * ClusterPropertyApis} ({@code GET /cluster/properties}).
+ */
CLUSTERSTATUS_OP(
CLUSTERSTATUS,
(req, rsp, h) -> {
@@ -865,6 +895,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
BalanceShardUnique.invokeFromV1Params(h.coreContainer, req, rsp);
return null;
}),
+ /**
+ * V2 equivalent: {@link RebalanceLeadersAPI} ({@code POST /collections/{collection}} with the
+ * {@code rebalance-leaders} command).
+ */
REBALANCELEADERS_OP(
REBALANCELEADERS,
(req, rsp, h) -> {
@@ -872,6 +906,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
return null;
}),
// XXX should this command support followAliases?
+ /**
+ * V2 equivalent: {@link ModifyCollectionAPI} ({@code POST /collections/{collection}} with the
+ * {@code modify} command).
+ */
MODIFYCOLLECTION_OP(
MODIFYCOLLECTION,
(req, rsp, h) -> {
@@ -1026,6 +1064,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, replaceNodeResponse);
return null;
}),
+ /**
+ * V2 equivalent: {@link MoveReplicaAPI} ({@code POST /collections/{collection}} with the {@code
+ * move-replica} command).
+ */
MOVEREPLICA_OP(
MOVEREPLICA,
(req, rsp, h) -> {