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 000000000000..3406f380b72f --- /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 2f20f4b4366d..468b79d52bf1 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 17bf44c44ec9..996b6e707291 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 collections; + + @Schema(description = "Collections, shards, and replicas, present only when detailed=true.") + @JsonProperty("collectionsDetail") + public Map collectionsDetail; + + /** State of one collection, including its shards and replicas. */ + public static class CollectionState { + @JsonProperty public Map 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 aliases; + + @Schema(description = "Document router for this collection, e.g. name=compositeId.") + @JsonProperty + public Map router; + + @JsonProperty public Integer replicationFactor; + + private final Map additionalProperties = new LinkedHashMap<>(); + + @JsonAnyGetter + public Map 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 replicas; + + private final Map additionalProperties = new LinkedHashMap<>(); + + @JsonAnyGetter + public Map 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 additionalProperties = new LinkedHashMap<>(); + + @JsonAnyGetter + public Map 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 d292cb4a05f4..33b7bd41e537 100644 --- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java @@ -38,8 +38,6 @@ import org.apache.solr.common.SolrException; 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.Utils; @@ -210,13 +208,6 @@ public static SolrQueryRequest wrapParams(SolrQueryRequest req, Map 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 f7f206625066..1bd3e882a181 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.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; @@ -112,9 +114,10 @@ public void getClusterStatus(NamedList results, SolrVersion solrVersion) boolean withAliases = solrParams.getBool(ALIASES_PROP, includeAll); List 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); } @@ -125,7 +128,7 @@ public void getClusterStatus(NamedList results, SolrVersion solrVersion) if (withCollection) { assert liveNodes != null; - fetchClusterStatusForCollOrAlias(clusterStatus, liveNodes, aliases, solrVersion); + addCollectionStatuses(clusterStatus, liveNodes, aliases, solrVersion); } if (withAliases) { @@ -143,13 +146,49 @@ public void getClusterStatus(NamedList results, SolrVersion solrVersion) results.add("cluster", clusterStatus); } - private void fetchClusterStatusForCollOrAlias( + /** + * Collections, shards, and replicas selected by this request's {@code collection}, {@code shard}, + * and {@code _route_} parameters. + * + *

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 getCollectionStatuses() throws KeeperException, InterruptedException { + List liveNodes = + zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null); + Aliases aliases = zkStateReader.getAliases(); + Map collections = new LinkedHashMap<>(); + forEachCollectionStatus( + liveNodes, aliases, (name, status) -> collections.put(name, copyForJackson(status))); + return collections; + } + + private void addCollectionStatuses( NamedList clusterStatus, List 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 collectionProps = new SimpleOrderedMap<>(); + prepared.forEach(liveNodes, collectionProps::add); + clusterStatus.add("collections", collectionProps); + } + } + + private void forEachCollectionStatus( + List liveNodes, Aliases aliases, BiConsumer> consumer) { + prepareCollections(aliases).forEach(liveNodes, consumer); + } - // read aliases + private PreparedCollections prepareCollections(Aliases aliases) { Map> collectionVsAliases = new HashMap<>(); Map> aliasVsCollections = aliases.getCollectionAliasListMap(); for (Map.Entry> entry : aliasVsCollections.entrySet()) { @@ -164,12 +203,6 @@ private void fetchClusterStatusForCollOrAlias( } ClusterState clusterState = zkStateReader.getClusterState(); - - String routeKey = solrParams.get(ShardParams._ROUTE_); - String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP); - - Set requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; - Stream collectionStream; if (collection == null) { collectionStream = clusterState.collectionStream(); @@ -195,33 +228,53 @@ private void fetchClusterStatusForCollOrAlias( } } - 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 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 requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; + return new PreparedCollections( + collectionStream.toList(), collectionVsAliases, routeKey, requestedShards); + } + + private final class PreparedCollections { + private final List collections; + private final Map> collectionVsAliases; + private final String routeKey; + private final Set requestedShards; + + private PreparedCollections( + List collections, + Map> collectionVsAliases, + String routeKey, + Set requestedShards) { + this.collections = collections; + this.collectionVsAliases = collectionVsAliases; + this.routeKey = routeKey; + this.requestedShards = requestedShards; + } + + private void forEach(List liveNodes, BiConsumer> 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 copyForJackson(Map status) { + Object prs = status.get("PRS"); + if (!(prs instanceof MapWriter)) { + return status; } + Map copy = new LinkedHashMap<>(status); + copy.put("PRS", Utils.fromJSON(Utils.toJSON(prs))); + return copy; } private void addAliasMap(Aliases aliases, NamedList 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 9b8662176d69..34f4be920cf4 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 @@ -782,7 +782,8 @@ collection, propName, new UpdateCollectionPropertyRequestBody(val)) 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 81c5fac32c57..831e5929b25b 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 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 @@ * V2 API for listing collections. * *

This API (GET /v2/collections) is equivalent to the v1 /admin/collections?action=LIST command + * + *

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 ListCollections(CoreContainer coreContainer, SolrQueryRequest req, SolrQu @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 collectionList = @@ -63,4 +96,17 @@ public ListCollectionsResponse listCollections() { 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 typedCollections(Map raw) { + var mapper = SolrJacksonMapper.getObjectMapper(); + Map collections = CollectionUtil.newLinkedHashMap(raw.size()); + for (Map.Entry 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 0546551b11da..17ff7920837e 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.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 void testClusterOverseerAllParams() throws Exception { 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)); - } - private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody) throws Exception { return doCaptureParams(path, method, v2RequestBody, mockCollectionsHandler); 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 000000000000..48a08b7a9e31 --- /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 000000000000..93808b8a27a5 --- /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}. + * + *

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 body = getCollections("?detailed=true"); + assertNull(body.get("cluster")); + assertNull(body.get("live_nodes")); + assertNull(body.get("aliases")); + assertNull(body.get("properties")); + + Map 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 prs = + (Map) + 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 byCollection = + collections(getCollections("?detailed=true&collection=" + COLLECTION)); + assertEquals(Set.of(COLLECTION), byCollection.keySet()); + + Map 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 response = + CollectionAdminRequest.getClusterStatus().process(cluster.getSolrClient()).getResponse(); + Map clusterState = (Map) 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 collectionsOnly = + cluster + .getSolrClient() + .request( + new GenericSolrRequest(GET, "/admin/collections", SolrRequestType.ADMIN, params)); + Map filtered = (Map) collectionsOnly.get("cluster"); + assertNull(filtered.get("live_nodes")); + assertNull(filtered.get("aliases")); + assertNull(filtered.get("properties")); + assertNotNull(((Map) filtered.get("collections")).get(COLLECTION)); + } + + private static Map 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 parsed(ContentResponse response) { + return (Map) Utils.fromJSONString(response.getContentAsString()); + } + + @SuppressWarnings("unchecked") + private static Map collections(Map body) { + return (Map) body.get("collectionsDetail"); + } + + @SuppressWarnings("unchecked") + private static Map collection(Map body, String name) { + Map collection = (Map) collections(body).get(name); + assertNotNull(collection); + return collection; + } + + @SuppressWarnings("unchecked") + private static Map shards(Map collection) { + return (Map) collection.get("shards"); + } + + @SuppressWarnings("unchecked") + private static List aliasesOf(Map collection) { + return (List) collection.get("aliases"); + } + + @SuppressWarnings("unchecked") + private static Map replica(Map collection) { + Map shard = (Map) shards(collection).get("shard1"); + Map replicas = (Map) shard.get("replicas"); + assertFalse(replicas.isEmpty()); + return (Map) 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 26390f38c306..6d0e6c0e60d5 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 5301f9dd02cc..f42f6d1f9302 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 c985801021b1..c5303b26a8c1 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/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc index ac2de0f81376..0372e4b820b8 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc @@ -41,3 +41,15 @@ bin/solr start -Dsolr.node.roles=data:on,overseer:preferred A node started this way asks the Overseer to re-run its node prioritization, so a preferred node takes over without waiting for the current Overseer to restart. Note that node roles are fixed for the lifetime of a node: unlike `ADDROLE`, they cannot be changed on a running node. + +=== V2 collections tree + +`GET /api/collections?detailed=true` returns the collections, shards, and replicas tree under `collectionsDetail`. +It does not include `live_nodes`, the alias map, or cluster properties. +Use `GET /api/cluster/nodes`, `GET /api/aliases`, and `GET /api/cluster/properties` for those. + +There is no `includeAll` parameter. +A request with no other parameters returns every collection. +`collection`, `shard`, and `_route_` narrow which part of the tree is returned. + +The v1 `CLUSTERSTATUS` command is unchanged, and `includeAll` still defaults to true. diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index f61b4e8d51c1..1558918d09ea 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 6fbca386fe29..212493e84443 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 2ce9a392d1ce..87ae9eecf31e 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 d1834b2bd1e2..9292da4f7c6a 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 1c93b088b5e9..17707f34243f 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 c0b525b1698c..462d684121e0 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},