This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new c1199180 perf(collection): remove redundant Solr round-trips in 
get-collection-stats (#231)
c1199180 is described below

commit c1199180edf623c329cd498d9159dfe05cb638bc
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Oct 6 14:42:04 2026 -0400

    perf(collection): remove redundant Solr round-trips in get-collection-stats 
(#231)
    
    * perf(collection): remove redundant Solr round-trips in 
get-collection-stats
    
    get-collection-stats made six Solr requests per call; it now makes three.
    
    - The list-collections pre-flight is gone. The Luke request that follows
      already 404s on an unknown collection, and that 404 is translated into
      the same "Collection not found ... Hint: call list-collections" error.
      The pre-flight also made the tool fail outright on standalone Solr,
      where the Collections API LIST is unavailable. A blank or null name is
      rejected up front, since "" reaches Solr as a Jetty 400 rather than a
      404. validateCollectionExists, whose shard-prefix branch could never
      match, is deleted.
    - The three node-wide /admin/metrics requests (searcher caches, /select,
      /update) are one request using the Metrics API's comma-separated
      prefix filter; the same core NamedList feeds the cache and handler
      extractors. It still degrades to null when metrics are unavailable.
    - Luke now asks for show=index and numTerms=0 instead of
      includeIndexFieldFlags=true, so Solr no longer walks every field to
      build per-field flags that buildIndexStats never reads.
    
    The tool description no longer claims cacheStats/handlerStats are always
    null on Solr 10 because of /admin/mbeans; the code reads /admin/metrics.
    getCacheMetrics/getHandlerMetrics lose their own list-collections call
    and return null for an unknown collection because its core is absent
    from the metrics response.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    * refactor(collection): one metrics fetch path, drop no-op Luke param and 
blank guard
    
    - getCacheMetrics/getHandlerMetrics now go through the same combined
      /admin/metrics fetch as get-collection-stats, so the per-prefix
      fetchCacheMetrics/fetchHandlerMetrics/fetchFlatHandlerInfo path is
      gone and fetchMetrics owns the degrade-to-null handling.
    - numTerms=0 is dropped: without fl, Luke computes no term info, so it
      changed nothing (verified against Solr 9.9).
    - The blank-collection guard is dropped, keeping "" consistent with the
      other tools (see #108); "   " still gets the not-found hint from
      Luke's 404.
    - The unit test asserts the Luke request sends show=index.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    ---------
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../mcp/server/collection/CollectionService.java   | 231 +++++++--------------
 .../CollectionServiceIntegrationTest.java          |  12 ++
 .../server/collection/CollectionServiceTest.java   | 155 +++++++-------
 3 files changed, 169 insertions(+), 229 deletions(-)

diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java 
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
index 92a2af41..487e8b42 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
@@ -42,6 +42,7 @@ import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.client.solrj.response.SolrPingResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.mcp.server.config.SolrConfigurationProperties;
 import org.apache.solr.mcp.server.util.PromptNames;
@@ -186,6 +187,16 @@ public class CollectionService {
        /** Prefix for update handler metrics in the Metrics API */
        private static final String UPDATE_HANDLER_METRIC_PREFIX = 
"UPDATE./update";
 
+       /** Cache and handler prefixes, comma-separated so one request fetches 
all */
+       private static final String STATS_METRIC_PREFIXES = String.join(",", 
CACHE_METRIC_PREFIX,
+                       SELECT_HANDLER_METRIC_PREFIX, 
UPDATE_HANDLER_METRIC_PREFIX);
+
+       /** Request parameter name for restricting the Luke handler's output 
sections */
+       private static final String SHOW_PARAM = "show";
+
+       /** Luke {@value #SHOW_PARAM} value restricting output to the index 
section */
+       private static final String SHOW_INDEX_VALUE = "index";
+
        // ========================================
        // Constants for Response Parsing
        // ========================================
@@ -291,6 +302,20 @@ public class CollectionService {
                this.objectMapper = objectMapper;
        }
 
+       /**
+        * A {@link LukeRequest} for the {@code index} section only, which is 
all
+        * {@link #buildIndexStats(LukeResponse)} reads. Without {@code 
show=index} Solr
+        * also lists every field; SolrJ has no setter for it.
+        */
+       private static final class IndexOnlyLukeRequest extends LukeRequest {
+               @Override
+               public SolrParams getParams() {
+                       ModifiableSolrParams params = new 
ModifiableSolrParams(super.getParams());
+                       params.set(SHOW_PARAM, SHOW_INDEX_VALUE);
+                       return params;
+               }
+       }
+
        /**
         * MCP Resource endpoint that returns a list of all available Solr 
collections.
         *
@@ -489,9 +514,10 @@ public class CollectionService {
         * <strong>Validation:</strong>
         *
         * <p>
-        * The method validates that the specified collection exists before 
attempting
-        * to collect metrics. If the collection is not found, an
-        * {@code IllegalArgumentException} is thrown with a descriptive error 
message.
+        * Collection existence is not checked with a separate pre-flight call: 
the Luke
+        * request below already 404s for an unknown collection, and that 
response is
+        * translated into an {@code IllegalArgumentException} with a 
descriptive error
+        * message.
         *
         * <p>
         * <strong>MCP Tool Usage:</strong>
@@ -518,31 +544,34 @@ public class CollectionService {
        @McpTool(
                        name = "get-collection-stats",
                        annotations = @McpTool.McpAnnotations(readOnlyHint = 
true),
-                       description = "Get stats/metrics on a Solr collection. 
On Solr 10+ cacheStats and"
-                                       + " handlerStats are always null 
because the /admin/mbeans endpoint was removed"
-                                       + " from Solr; this is expected and not 
an error.")
+                       description = "Get stats/metrics on a Solr collection. 
cacheStats and handlerStats may be"
+                                       + " null when Solr's /admin/metrics API 
does not provide them for the collection;"
+                                       + " this is expected and not an error.")
        public SolrMetrics getCollectionStats(
                        @McpToolParam(description = "Solr collection to get 
stats/metrics for") String collection)
                        throws SolrServerException, IOException {
                // Extract actual collection name from shard name if needed
                String actualCollection = extractCollectionName(collection);
 
-               // Validate collection exists
-               if (!validateCollectionExists(actualCollection)) {
-                       throw new 
IllegalArgumentException(COLLECTION_NOT_FOUND_ERROR + actualCollection
-                                       + ". Hint: call list-collections to see 
available collections.");
+               // Index statistics using Luke; an unknown collection 404s here
+               LukeResponse lukeResponse;
+               try {
+                       lukeResponse = new 
IndexOnlyLukeRequest().process(solrClient, actualCollection);
+               } catch (SolrException e) {
+                       if (e.code() == SolrException.ErrorCode.NOT_FOUND.code) 
{
+                               throw new 
IllegalArgumentException(COLLECTION_NOT_FOUND_ERROR + actualCollection
+                                               + ". Hint: call 
list-collections to see available collections.", e);
+                       }
+                       throw e;
                }
 
-               // Index statistics using Luke
-               LukeRequest lukeRequest = new LukeRequest();
-               lukeRequest.setIncludeIndexFieldFlags(true);
-               LukeResponse lukeResponse = lukeRequest.process(solrClient, 
actualCollection);
-
                // Query performance metrics
                QueryResponse statsResponse = 
solrClient.query(actualCollection, new 
SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0));
 
-               return new SolrMetrics(buildIndexStats(lukeResponse), 
buildQueryStats(statsResponse),
-                               fetchCacheMetrics(actualCollection), 
fetchHandlerMetrics(actualCollection), Instant.now());
+               // One /admin/metrics call feeds both the cache and handler 
stats
+               NamedList<Object> coreMetrics = fetchMetrics(actualCollection);
+               return new SolrMetrics(buildIndexStats(lukeResponse), 
buildQueryStats(statsResponse), cacheStats(coreMetrics),
+                               handlerStats(coreMetrics), Instant.now());
        }
 
        /**
@@ -665,42 +694,26 @@ public class CollectionService {
         *            the collection name to retrieve cache metrics for
         * @return CacheStats object with all cache performance metrics, or 
null if
         *         unavailable
-        * @throws SolrServerException
-        *             if there are errors communicating with Solr
-        * @throws IOException
-        *             if there are I/O errors during communication
         * @see CacheStats
         * @see CacheInfo
         * @see #extractCacheStats(NamedList)
         * @see #isCacheStatsEmpty(CacheStats)
         */
-       @Nullable CacheStats getCacheMetrics(String collection) throws 
SolrServerException, IOException {
-               String actualCollection = extractCollectionName(collection);
-
-               if (!validateCollectionExists(actualCollection)) {
-                       return null;
-               }
-
-               return fetchCacheMetrics(actualCollection);
+       @Nullable CacheStats getCacheMetrics(String collection) {
+               // An unknown collection has no core in the metrics response, 
so this is null
+               return 
cacheStats(fetchMetrics(extractCollectionName(collection)));
        }
 
        /**
-        * Internal cache metrics fetch that assumes the collection has already 
been
-        * validated and the name has been extracted from any shard identifier.
+        * Extracts {@link CacheStats} from fetched core metrics, or {@code 
null} if
+        * there are none.
         */
-       private @Nullable CacheStats fetchCacheMetrics(String collectionName) {
-               try {
-                       NamedList<Object> coreMetrics = 
fetchMetrics(collectionName, CACHE_METRIC_PREFIX);
-                       if (coreMetrics == null) {
-                               return null;
-                       }
-
-                       CacheStats stats = extractCacheStats(coreMetrics);
-                       return isCacheStatsEmpty(stats) ? null : stats;
-               } catch (SolrServerException | IOException | SolrException e) {
-                       logger.debug("Cache metrics unavailable for collection: 
{}", collectionName, e);
+       private @Nullable CacheStats cacheStats(@Nullable NamedList<Object> 
coreMetrics) {
+               if (coreMetrics == null) {
                        return null;
                }
+               CacheStats stats = extractCacheStats(coreMetrics);
+               return isCacheStatsEmpty(stats) ? null : stats;
        }
 
        /**
@@ -782,44 +795,30 @@ public class CollectionService {
         *            the collection name to retrieve handler metrics for
         * @return HandlerStats object with performance metrics for all 
handlers, or
         *         null if unavailable
-        * @throws SolrServerException
-        *             if there are errors communicating with Solr
-        * @throws IOException
-        *             if there are I/O errors during communication
         * @see HandlerStats
         * @see HandlerInfo
-        * @see #fetchFlatHandlerInfo(String, String, String)
+        * @see #extractFlatHandlerInfo(NamedList, String)
         * @see #isHandlerStatsEmpty(HandlerStats)
         */
-       @Nullable HandlerStats getHandlerMetrics(String collection) throws 
SolrServerException, IOException {
-               String actualCollection = extractCollectionName(collection);
-
-               if (!validateCollectionExists(actualCollection)) {
-                       return null;
-               }
-
-               return fetchHandlerMetrics(actualCollection);
+       @Nullable HandlerStats getHandlerMetrics(String collection) {
+               // An unknown collection has no core in the metrics response, 
so this is null
+               return 
handlerStats(fetchMetrics(extractCollectionName(collection)));
        }
 
        /**
-        * Internal handler metrics fetch that assumes the collection has 
already been
-        * validated and the name has been extracted from any shard identifier.
+        * Extracts {@link HandlerStats} from fetched core metrics, or {@code 
null} if
+        * there are none. Handler metrics are flat keys (e.g.
+        * {@code QUERY./select.requests}), reassembled into a {@link 
HandlerInfo} per
+        * handler.
         */
-       private @Nullable HandlerStats fetchHandlerMetrics(String 
collectionName) {
-               try {
-                       // Handler metrics are flat keys (e.g. 
QUERY./select.requests) so we
-                       // fetch each handler prefix separately and reconstruct 
HandlerInfo
-                       HandlerInfo selectHandler = 
fetchFlatHandlerInfo(collectionName, SELECT_HANDLER_METRIC_PREFIX,
-                                       SELECT_HANDLER_KEY);
-                       HandlerInfo updateHandler = 
fetchFlatHandlerInfo(collectionName, UPDATE_HANDLER_METRIC_PREFIX,
-                                       UPDATE_HANDLER_KEY);
-
-                       HandlerStats stats = new HandlerStats(selectHandler, 
updateHandler);
-                       return isHandlerStatsEmpty(stats) ? null : stats;
-               } catch (SolrServerException | IOException | SolrException e) {
-                       logger.debug("Handler metrics unavailable for 
collection: {}", collectionName, e);
+       private @Nullable HandlerStats handlerStats(@Nullable NamedList<Object> 
coreMetrics) {
+               if (coreMetrics == null) {
                        return null;
                }
+               HandlerInfo selectHandler = extractFlatHandlerInfo(coreMetrics, 
SELECT_HANDLER_KEY);
+               HandlerInfo updateHandler = extractFlatHandlerInfo(coreMetrics, 
UPDATE_HANDLER_KEY);
+               HandlerStats stats = new HandlerStats(selectHandler, 
updateHandler);
+               return isHandlerStatsEmpty(stats) ? null : stats;
        }
 
        /**
@@ -839,26 +838,30 @@ public class CollectionService {
        }
 
        /**
-        * Fetches metrics from the Solr Metrics API for a given collection and 
prefix.
+        * Fetches the cache and handler metrics of a collection's core from 
the Solr
+        * Metrics API in one request.
         *
         * @param collection
         *            the collection name
-        * @param prefix
-        *            the metric key prefix to filter (e.g. "CACHE.searcher", 
"HANDLER")
         * @return the core-level metrics NamedList, or null if unavailable
         */
        @SuppressWarnings("unchecked")
-       private @Nullable NamedList<Object> fetchMetrics(String collection, 
String prefix)
-                       throws SolrServerException, IOException {
+       private @Nullable NamedList<Object> fetchMetrics(String collection) {
                ModifiableSolrParams params = new ModifiableSolrParams();
                params.set(GROUP_PARAM, CORE_GROUP);
-               params.set(PREFIX_PARAM, prefix);
+               params.set(PREFIX_PARAM, STATS_METRIC_PREFIXES);
                params.set(WT_PARAM, JSON_FORMAT);
 
                // Metrics API is a node-level endpoint, not per-collection
                GenericSolrRequest request = new 
GenericSolrRequest(SolrRequest.METHOD.GET, ADMIN_METRICS_PATH, params);
 
-               NamedList<Object> response = solrClient.request(request);
+               NamedList<Object> response;
+               try {
+                       response = solrClient.request(request);
+               } catch (SolrServerException | IOException | SolrException e) {
+                       logger.debug("Cache/handler metrics unavailable for 
collection: {}", collection, e);
+                       return null;
+               }
                NamedList<Object> metrics = (NamedList<Object>) 
response.get(METRICS_KEY);
                if (metrics == null || metrics.size() == 0) {
                        return null;
@@ -876,33 +879,6 @@ public class CollectionService {
                return null;
        }
 
-       /**
-        * Fetches and extracts handler metrics from flat Solr Metrics API keys.
-        *
-        * <p>
-        * Handler metrics in Solr are stored as flat keys (e.g.
-        * {@code QUERY./select.requests}) rather than nested objects. This 
method
-        * fetches core metrics filtered by the handler prefix and reconstructs 
a
-        * {@link HandlerInfo} from the individual flat keys.
-        *
-        * @param collection
-        *            the collection name
-        * @param metricPrefix
-        *            the prefix for the Metrics API filter (e.g. {@code 
QUERY./select})
-        * @param keyPrefix
-        *            the flat key prefix including trailing dot (e.g.
-        *            {@code QUERY./select.})
-        * @return HandlerInfo with stats, or null if unavailable
-        */
-       private @Nullable HandlerInfo fetchFlatHandlerInfo(String collection, 
String metricPrefix, String keyPrefix)
-                       throws SolrServerException, IOException {
-               NamedList<Object> coreMetrics = fetchMetrics(collection, 
metricPrefix);
-               if (coreMetrics == null) {
-                       return null;
-               }
-               return extractFlatHandlerInfo(coreMetrics, keyPrefix);
-       }
-
        /**
         * Extracts a {@link HandlerInfo} from flat metric keys in core metrics.
         *
@@ -972,53 +948,6 @@ public class CollectionService {
                return 
SHARD_SUFFIX_PATTERN.matcher(collectionOrShard).replaceFirst("");
        }
 
-       /**
-        * Validates that a specified collection exists in the Solr cluster.
-        *
-        * <p>
-        * Performs collection existence validation by checking against the 
list of
-        * available collections. Supports both exact collection name matches 
and
-        * shard-based matching for SolrCloud environments.
-        *
-        * <p>
-        * <strong>Validation Strategy:</strong>
-        *
-        * <ol>
-        * <li><strong>Exact Match</strong>: Checks if the collection name 
exists
-        * exactly
-        * <li><strong>Shard Match</strong>: Checks if any shards start with
-        * "collection{@value #SHARD_SUFFIX}" pattern
-        * </ol>
-        *
-        * <p>
-        * This dual approach ensures compatibility with SolrCloud environments 
where
-        * shard names may be returned alongside collection names.
-        *
-        * @param collection
-        *            the collection name to validate
-        * @return true if the collection exists (either exact or shard match), 
false
-        *         otherwise
-        * @throws SolrServerException
-        *             if there are errors communicating with Solr
-        * @throws IOException
-        *             if there are I/O errors during communication
-        * @see #listCollections()
-        * @see #extractCollectionName(String)
-        */
-       private boolean validateCollectionExists(String collection) throws 
SolrServerException, IOException {
-               List<String> collections = listCollections();
-
-               // Check for exact match first
-               if (collections.contains(collection)) {
-                       return true;
-               }
-
-               // Check if any of the returned collections start with the 
collection name (for
-               // shard
-               // names)
-               return collections.stream().anyMatch(c -> 
c.startsWith(collection + SHARD_SUFFIX));
-       }
-
        /**
         * Performs a comprehensive health check on a Solr collection.
         *
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
index 300f0198..ac87d499 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.ArrayList;
@@ -151,6 +152,17 @@ class CollectionServiceIntegrationTest {
                                "Update handler should have processed requests 
from indexing");
        }
 
+       @Test
+       void 
testGetCollectionStats_nonExistentCollection_throwsIllegalArgument() {
+               String missing = "non_existent_collection";
+
+               IllegalArgumentException exception = 
assertThrows(IllegalArgumentException.class,
+                               () -> 
collectionService.getCollectionStats(missing));
+
+               assertTrue(exception.getMessage().contains("Collection not 
found: " + missing),
+                               "Message should name the missing collection: " 
+ exception.getMessage());
+       }
+
        @Test
        void testCheckHealth_healthy() {
                SolrHealthStatus status = 
collectionService.checkHealth(TEST_COLLECTION);
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
index d0772ec9..360b12e7 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
@@ -26,21 +26,25 @@ import 
io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.IntStream;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.LukeRequest;
 import org.apache.solr.client.solrj.response.LukeResponse;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.client.solrj.response.SolrPingResponse;
 import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.NamedList;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledInNativeImage;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -378,65 +382,82 @@ class CollectionServiceTest {
                assertNull(result.segmentCount());
        }
 
-       // Collection validation tests
+       // Collection stats tests
        @Test
        void getCollectionStats_NotFound() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+               SolrException notFound = new 
SolrException(SolrException.ErrorCode.NOT_FOUND, "not found");
+               when(solrClient.request(any(LukeRequest.class), 
eq("non_existent"))).thenThrow(notFound);
 
                IllegalArgumentException exception = 
assertThrows(IllegalArgumentException.class,
-                               () -> 
spyService.getCollectionStats("non_existent"));
+                               () -> 
collectionService.getCollectionStats("non_existent"));
 
                assertTrue(exception.getMessage().contains("Collection not 
found: non_existent"));
+               assertSame(notFound, exception.getCause());
+               verify(solrClient, 
never()).request(any(CollectionAdminRequest.List.class), any());
        }
 
        @Test
-       void validateCollectionExists() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               List<String> collections = Arrays.asList("collection1", 
"films_shard1_replica_n1");
-               doReturn(collections).when(spyService).listCollections();
+       void getCollectionStats_OtherSolrException_Propagates() throws 
Exception {
+               SolrException serverError = new 
SolrException(SolrException.ErrorCode.SERVER_ERROR, "boom");
+               when(solrClient.request(any(LukeRequest.class), 
eq("test_collection"))).thenThrow(serverError);
 
-               Method method = 
CollectionService.class.getDeclaredMethod("validateCollectionExists", 
String.class);
-               method.setAccessible(true);
+               SolrException exception = assertThrows(SolrException.class,
+                               () -> 
collectionService.getCollectionStats("test_collection"));
 
-               assertTrue((boolean) method.invoke(spyService, "collection1"));
-               assertTrue((boolean) method.invoke(spyService, "films"));
-               assertFalse((boolean) method.invoke(spyService, 
"non_existent"));
+               assertSame(serverError, exception);
        }
 
        @Test
-       void validateCollectionExists_WithEmptyList() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+       void getCollectionStats_MakesExactlyOneMetricsRequestAndNoListCall() 
throws Exception {
+               // Luke response: only the "index" section is needed for 
buildIndexStats.
+               NamedList<Object> lukeIndexSection = new NamedList<>();
+               lukeIndexSection.add("numDocs", 42);
+               lukeIndexSection.add("segmentCount", 3);
+               NamedList<Object> lukeRaw = new NamedList<>();
+               lukeRaw.add("index", lukeIndexSection);
+               when(solrClient.request(any(LukeRequest.class), 
eq("test_collection"))).thenReturn(lukeRaw);
 
-               Method method = 
CollectionService.class.getDeclaredMethod("validateCollectionExists", 
String.class);
-               method.setAccessible(true);
+               SolrDocumentList docList = new SolrDocumentList();
+               docList.setNumFound(7);
+               when(solrClient.query(eq("test_collection"), 
any())).thenReturn(queryResponse);
+               when(queryResponse.getResults()).thenReturn(docList);
+
+               NamedList<Object> metricsResponse = 
wrapInMetricsResponse(createCacheCoreMetrics(), "test_collection");
+               when(solrClient.request(argThat(r -> r instanceof 
GenericSolrRequest))).thenReturn(metricsResponse);
 
-               assertFalse((boolean) method.invoke(spyService, 
"any_collection"));
+               SolrMetrics result = 
collectionService.getCollectionStats("test_collection");
+
+               assertNotNull(result);
+               assertEquals(42, result.indexStats().numDocs());
+               assertEquals(3, result.indexStats().segmentCount());
+               assertNotNull(result.cacheStats());
+
+               // Exactly one metrics call (combined cache+handler prefixes), 
no
+               // list-collections pre-flight, and exactly one Luke request.
+               verify(solrClient, 
never()).request(any(CollectionAdminRequest.List.class), any());
+               verify(solrClient, times(1)).request(argThat(r -> r instanceof 
GenericSolrRequest));
+               ArgumentCaptor<LukeRequest> luke = 
ArgumentCaptor.forClass(LukeRequest.class);
+               verify(solrClient).request(luke.capture(), 
eq("test_collection"));
+               assertEquals("index", luke.getValue().getParams().get("show"), 
"only the index section, not every field");
        }
 
        // Cache metrics tests
        @Test
        void getCacheMetrics_WithNonExistentCollection_ShouldReturnNull() 
throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+               // The metrics response has no core registry for this 
collection.
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(emptyMetricsResponse());
 
-               // When - collection not found in empty list
-               CacheStats result = spyService.getCacheMetrics("nonexistent");
+               CacheStats result = 
collectionService.getCacheMetrics("nonexistent");
 
-               // Then
                assertNull(result);
        }
 
        @Test
        void getCacheMetrics_Success() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                NamedList<Object> mbeans = createMockCacheData();
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(mbeans);
 
-               CacheStats result = 
spyService.getCacheMetrics("test_collection");
+               CacheStats result = 
collectionService.getCacheMetrics("test_collection");
 
                assertNotNull(result);
                assertNotNull(result.queryResultCache());
@@ -445,61 +466,48 @@ class CollectionServiceTest {
 
        @Test
        void getCacheMetrics_CollectionNotFound() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(emptyMetricsResponse());
 
-               CacheStats result = spyService.getCacheMetrics("non_existent");
+               CacheStats result = 
collectionService.getCacheMetrics("non_existent");
 
                assertNull(result);
        }
 
        @Test
        void getCacheMetrics_SolrServerException() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                when(solrClient.request(any(SolrRequest.class))).thenThrow(new 
SolrServerException("Error"));
 
-               CacheStats result = 
spyService.getCacheMetrics("test_collection");
+               CacheStats result = 
collectionService.getCacheMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getCacheMetrics_IOException() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                when(solrClient.request(any(SolrRequest.class))).thenThrow(new 
IOException("IO Error"));
 
-               CacheStats result = 
spyService.getCacheMetrics("test_collection");
+               CacheStats result = 
collectionService.getCacheMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getCacheMetrics_EmptyStats() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                // Metrics response with core metrics that contain no cache keys
                NamedList<Object> response = wrapInMetricsResponse(new 
NamedList<>());
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
-               CacheStats result = 
spyService.getCacheMetrics("test_collection");
+               CacheStats result = 
collectionService.getCacheMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getCacheMetrics_WithShardName() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
-
                NamedList<Object> response = 
wrapInMetricsResponse(createCacheCoreMetrics(), "films");
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
-               CacheStats result = 
spyService.getCacheMetrics("films_shard1_replica_n1");
+               CacheStats result = 
collectionService.getCacheMetrics("films_shard1_replica_n1");
 
                assertNotNull(result);
        }
@@ -562,26 +570,20 @@ class CollectionServiceTest {
        // Handler metrics tests
        @Test
        void getHandlerMetrics_WithNonExistentCollection_ShouldReturnNull() 
throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+               // The metrics response has no core registry for this 
collection.
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(emptyMetricsResponse());
 
-               // When - collection not found in empty list
-               HandlerStats result = 
spyService.getHandlerMetrics("nonexistent");
+               HandlerStats result = 
collectionService.getHandlerMetrics("nonexistent");
 
-               // Then
                assertNull(result);
        }
 
        @Test
        void getHandlerMetrics_Success() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
-               // getHandlerMetrics makes two fetchMetrics calls (select then 
update);
-               // return select handler data for both calls (second has no 
update keys -> null)
+               // select handler data only, so the update handler is null
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(createMockSelectHandlerData());
 
-               HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
+               HandlerStats result = 
collectionService.getHandlerMetrics("test_collection");
 
                assertNotNull(result);
                assertNotNull(result.selectHandler());
@@ -590,60 +592,47 @@ class CollectionServiceTest {
 
        @Test
        void getHandlerMetrics_CollectionNotFound() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Collections.emptyList()).when(spyService).listCollections();
+               
when(solrClient.request(any(SolrRequest.class))).thenReturn(emptyMetricsResponse());
 
-               HandlerStats result = 
spyService.getHandlerMetrics("non_existent");
+               HandlerStats result = 
collectionService.getHandlerMetrics("non_existent");
 
                assertNull(result);
        }
 
        @Test
        void getHandlerMetrics_SolrServerException() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                when(solrClient.request(any(SolrRequest.class))).thenThrow(new 
SolrServerException("Error"));
 
-               HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
+               HandlerStats result = 
collectionService.getHandlerMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getHandlerMetrics_IOException() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                when(solrClient.request(any(SolrRequest.class))).thenThrow(new 
IOException("IO Error"));
 
-               HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
+               HandlerStats result = 
collectionService.getHandlerMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getHandlerMetrics_EmptyStats() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("test_collection")).when(spyService).listCollections();
-
                // Metrics response with core metrics that contain no handler 
keys
                NamedList<Object> response = wrapInMetricsResponse(new 
NamedList<>());
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(response);
 
-               HandlerStats result = 
spyService.getHandlerMetrics("test_collection");
+               HandlerStats result = 
collectionService.getHandlerMetrics("test_collection");
 
                assertNull(result);
        }
 
        @Test
        void getHandlerMetrics_WithShardName() throws Exception {
-               CollectionService spyService = spy(collectionService);
-               
doReturn(Arrays.asList("films_shard1_replica_n1")).when(spyService).listCollections();
-
                
when(solrClient.request(any(SolrRequest.class))).thenReturn(createMockSelectHandlerData("films"));
 
-               HandlerStats result = 
spyService.getHandlerMetrics("films_shard1_replica_n1");
+               HandlerStats result = 
collectionService.getHandlerMetrics("films_shard1_replica_n1");
 
                assertNotNull(result);
        }
@@ -843,6 +832,16 @@ class CollectionServiceTest {
                return response;
        }
 
+       /**
+        * A Metrics API response with no core registries at all — models an 
unknown
+        * collection, which has no matching {@code solr.core.<name>.} entry.
+        */
+       private NamedList<Object> emptyMetricsResponse() {
+               NamedList<Object> response = new NamedList<>();
+               response.add("metrics", new NamedList<>());
+               return response;
+       }
+
        // createCollection tests
        @Test
        void createCollection_success() throws Exception {

Reply via email to