This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 6630a8bb04477787db92c29e48873bb20d6ee310 Author: Jan Høydahl <[email protected]> AuthorDate: Wed Aug 5 12:21:37 2026 +0200 Add mock-based unit test for NodeHealth cloud-mode logic (#4703) (cherry picked from commit 45206ae5a177a58ff76e67cc4a2de5fc016f114f) --- .../handler/admin/api/NodeHealthSolrCloudTest.java | 70 +------- .../solr/handler/admin/api/NodeHealthTest.java | 194 +++++++++++++++++++++ 2 files changed, 197 insertions(+), 67 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthSolrCloudTest.java index 61ab10b4acd..1cf7da03d54 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthSolrCloudTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthSolrCloudTest.java @@ -18,22 +18,18 @@ package org.apache.solr.handler.admin.api; import static org.apache.solr.client.api.model.NodeHealthResponse.NodeStatus.OK; -import static org.hamcrest.Matchers.containsString; -import java.util.concurrent.TimeUnit; -import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.NodeApi; import org.apache.solr.cloud.SolrCloudTestCase; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.SolrException.ErrorCode; -import org.apache.solr.embedded.JettySolrRunner; import org.junit.BeforeClass; import org.junit.Test; /** - * Tests for the node-health API, on SolrCloud clusters + * Tests for the node-health API, on SolrCloud clusters. Failure scenarios (ZK connection loss, + * missing live node, unhealthy cores) are covered by the mock-based {@link NodeHealthTest}. * + * @see NodeHealthTest * @see NodeHealthStandaloneTest */ public class NodeHealthSolrCloudTest extends SolrCloudTestCase { @@ -66,64 +62,4 @@ public class NodeHealthSolrCloudTest extends SolrCloudTestCase { assertEquals(OK, response.status); assertEquals("All cores are healthy", response.message); } - - @Test - public void testCloudMode_UnhealthyWhenZkClientClosed() throws Exception { - // Use a fresh node so closing its ZK client does not break the primary cluster node - JettySolrRunner newJetty = cluster.startJettySolrRunner(); - cluster.waitForNode(newJetty, 30); - try (SolrClient nodeClient = newJetty.newClient()) { - // Sanity check: the new node should start out healthy - assertEquals(OK, new NodeApi.Healthcheck().process(nodeClient).status); - - // Break the ZK connection to put the node into an unhealthy state - newJetty.getCoreContainer().getZkController().getZkClient().close(); - - SolrException e = - assertThrows(SolrException.class, () -> new NodeApi.Healthcheck().process(nodeClient)); - assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); - assertThat(e.getMessage(), containsString(("Host Unavailable"))); - } finally { - newJetty.stop(); - } - } - - /** - * Verifies that when the node's name is absent from ZooKeeper's live-nodes set (while the ZK - * session itself is still connected), the v2 health-check API throws a {@code - * SERVICE_UNAVAILABLE} exception with a message identifying the live-nodes check as the cause. - * - * <p>This specifically exercises the code path at NodeHealth#getClusterState() that checks {@code - * clusterState.getLiveNodes().contains(nodeName)}. - */ - @Test - public void testNotInLiveNodes_ThrowsServiceUnavailable() throws Exception { - JettySolrRunner newJetty = cluster.startJettySolrRunner(); - cluster.waitForNode(newJetty, 30); - try (SolrClient nodeClient = newJetty.newClient()) { - // Sanity check: the new node should start out healthy - assertEquals(OK, new NodeApi.Healthcheck().process(nodeClient).status); - - String nodeName = newJetty.getCoreContainer().getZkController().getNodeName(); - - // Remove the node from ZooKeeper's live_nodes without closing the ZK session. - // This ensures the "ZK not connected" check passes and only the "not in live nodes" - // check fires, isolating the code path under test. - newJetty.getCoreContainer().getZkController().removeEphemeralLiveNode(); - - // Wait for the node's own ZkStateReader to reflect the removal before querying it. - newJetty - .getCoreContainer() - .getZkController() - .getZkStateReader() - .waitForLiveNodes(10, TimeUnit.SECONDS, missingLiveNode(nodeName)); - - SolrException e = - assertThrows(SolrException.class, () -> new NodeApi.Healthcheck().process(nodeClient)); - assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); - assertThat(e.getMessage(), containsString("Not in live nodes")); - } finally { - newJetty.stop(); - } - } } diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthTest.java new file mode 100644 index 00000000000..0b8080c62d2 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeHealthTest.java @@ -0,0 +1,194 @@ +/* + * 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.api.model.NodeHealthResponse.NodeStatus.OK; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import org.apache.solr.SolrTestCase; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.cloud.CloudDescriptor; +import org.apache.solr.cloud.ClusterStateMockUtil; +import org.apache.solr.cloud.ZkController; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.cloud.ClusterState; +import org.apache.solr.common.cloud.Replica; +import org.apache.solr.common.cloud.SolrZkClient; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.core.CoreDescriptor; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Unit test for the cloud-mode logic of {@link NodeHealth}, using mocks instead of a real cluster. + * + * @see NodeHealthSolrCloudTest + * @see NodeHealthStandaloneTest + */ +public class NodeHealthTest extends SolrTestCase { + + private static final String NODE_NAME = "baseUrl1:8983_"; + + private CoreContainer mockCoreContainer; + private ZkController mockZkController; + private ZkStateReader mockZkStateReader; + private SolrZkClient mockZkClient; + private NodeHealth nodeHealth; + + @BeforeClass + public static void ensureWorkingMockito() { + SolrTestCaseJ4.assumeWorkingMockito(); + } + + @Before + public void setupMocks() { + mockCoreContainer = mock(CoreContainer.class); + mockZkController = mock(ZkController.class); + mockZkStateReader = mock(ZkStateReader.class); + mockZkClient = mock(SolrZkClient.class); + + when(mockCoreContainer.isZooKeeperAware()).thenReturn(true); + when(mockCoreContainer.getZkController()).thenReturn(mockZkController); + when(mockZkController.getZkStateReader()).thenReturn(mockZkStateReader); + when(mockZkController.getNodeName()).thenReturn(NODE_NAME); + when(mockZkStateReader.getZkClient()).thenReturn(mockZkClient); + when(mockZkClient.isConnected()).thenReturn(true); + when(mockZkClient.isClosed()).thenReturn(false); + when(mockZkStateReader.getClusterState()) + .thenReturn(new ClusterState(Set.of(NODE_NAME), Map.of())); + + nodeHealth = new NodeHealth(mockCoreContainer); + } + + @Test + public void testUninitializedCoreContainerReturns500() { + SolrException e = + expectThrows(SolrException.class, () -> new NodeHealth(null).healthcheck(null, null)); + assertEquals(ErrorCode.SERVER_ERROR.code, e.code()); + + when(mockCoreContainer.isShutDown()).thenReturn(true); + e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(null, null)); + assertEquals(ErrorCode.SERVER_ERROR.code, e.code()); + } + + @Test + public void testHealthyNodeReturnsOkStatus() { + final var response = nodeHealth.healthcheck(null, null); + + assertEquals(OK, response.status); + assertNull("Expected no error on a healthy node", response.error); + } + + @Test + public void testZkClientClosedReturns503() { + when(mockZkClient.isClosed()).thenReturn(true); + + SolrException e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(null, null)); + assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); + assertThat(e.getMessage(), containsString("Not connected to zk")); + } + + @Test + public void testZkClientDisconnectedReturns503() { + when(mockZkClient.isConnected()).thenReturn(false); + + SolrException e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(null, null)); + assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); + assertThat(e.getMessage(), containsString("Not connected to zk")); + } + + @Test + public void testNotInLiveNodesReturns503() { + when(mockZkStateReader.getClusterState()) + .thenReturn(new ClusterState(Set.of("someOtherNode:8983_"), Map.of())); + + SolrException e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(null, null)); + assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); + assertThat(e.getMessage(), containsString("Not in live nodes")); + } + + @Test + public void testCoreLoadingNotCompleteReturns503() { + when(mockCoreContainer.isStatusLoadComplete()).thenReturn(false); + + SolrException e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(true, null)); + assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); + assertThat(e.getMessage(), containsString("Core Loading not complete")); + } + + @Test + public void testRequireHealthyCoresReturnsOkWhenAllCoresHealthy() { + when(mockCoreContainer.isStatusLoadComplete()).thenReturn(true); + CoreDescriptor activeCore = mockCoreDescriptor(Replica.State.ACTIVE); + when(mockCoreContainer.getCoreDescriptors()).thenReturn(List.of(activeCore)); + + final var response = nodeHealth.healthcheck(true, null); + + assertEquals(OK, response.status); + assertEquals("All cores are healthy", response.message); + } + + @Test + public void testUnhealthyCoresReturns503() { + when(mockCoreContainer.isStatusLoadComplete()).thenReturn(true); + CoreDescriptor recoveringCore = mockCoreDescriptor(Replica.State.RECOVERING); + when(mockCoreContainer.getCoreDescriptors()).thenReturn(List.of(recoveringCore)); + when(mockCoreContainer.getNumAllCores()).thenReturn(1); + + SolrException e = expectThrows(SolrException.class, () -> nodeHealth.healthcheck(true, null)); + assertEquals(ErrorCode.SERVICE_UNAVAILABLE.code, e.code()); + assertThat( + e.getMessage(), + containsString("1 out of 1 replicas are currently initializing or recovering")); + } + + /** + * Creates a core descriptor for a core of collection1/slice1 in the given state, and points the + * mocked cluster state at a matching collection. + */ + private CoreDescriptor mockCoreDescriptor(Replica.State state) { + CoreDescriptor coreDescriptor = + new CoreDescriptor( + "slice1_replica1", + createTempDir(), + Map.of( + CoreDescriptor.CORE_SHARD, "slice1", + CoreDescriptor.CORE_COLLECTION, "collection1", + CoreDescriptor.CORE_NODE_NAME, "slice1_replica1"), + new Properties(), + mockZkController); + CloudDescriptor cloudDescriptor = coreDescriptor.getCloudDescriptor(); + cloudDescriptor.setHasRegistered(true); + cloudDescriptor.setLastPublished(state); + + // collection1 with slice1 holding one active replica, on our (live) node + try (ZkStateReader stateReader = ClusterStateMockUtil.buildClusterState("csr", NODE_NAME)) { + when(mockZkStateReader.getClusterState()).thenReturn(stateReader.getClusterState()); + } + + return coreDescriptor; + } +}
