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 {