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 6b2da97f8286ab4a8dc937187a5eee6ad11fb99d Author: Boyi Dai <[email protected]> AuthorDate: Wed Oct 21 17:39:15 2026 -0400 SOLR-18538: CREATESNAPSHOT reports success when a shard has no active replica (#5114) Replace the per-shard failure counting with the check that the existing comment describes: after collecting the successful core snapshots, every active slice must have at least one replica that reported a successful snapshot. (cherry picked from commit 20eed9a06ab527bf938134c15deff24e92469668) --- .../SOLR-18538-snapshot-false-success.yml | 12 ++++ .../cloud/api/collections/CreateSnapshotCmd.java | 35 ++++----- .../TestSolrCloudSnapshotInactiveShard.java | 83 ++++++++++++++++++++++ 3 files changed, 109 insertions(+), 21 deletions(-) diff --git a/changelog/unreleased/SOLR-18538-snapshot-false-success.yml b/changelog/unreleased/SOLR-18538-snapshot-false-success.yml new file mode 100644 index 00000000000..48bca4b08cb --- /dev/null +++ b/changelog/unreleased/SOLR-18538-snapshot-false-success.yml @@ -0,0 +1,12 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc + +title: > + CREATESNAPSHOT now fails instead of reporting success when a shard has no active replica with a + core snapshot +type: fixed +authors: + - name: Boyi Dai + nick: SpookyJumpyBeans +links: + - name: SOLR-18538 + url: https://issues.apache.org/jira/browse/SOLR-18538 diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java index db4918f7759..2c901cea406 100644 --- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java @@ -99,8 +99,9 @@ public class CreateSnapshotCmd implements CollApiCmds.CollectionApiCommand { final ShardRequestTracker shardRequestTracker = CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc); - for (Slice slice : - ccc.getZkStateReader().getClusterState().getCollection(collectionName).getSlices()) { + DocCollection collection = + ccc.getZkStateReader().getClusterState().getCollection(collectionName); + for (Slice slice : collection.getActiveSlices()) { for (Replica replica : slice.getReplicas()) { if (replica.getState() != State.ACTIVE) { if (log.isInfoEnabled()) { @@ -166,26 +167,18 @@ public class CreateSnapshotCmd implements CollApiCmds.CollectionApiCommand { "Unable to create a snapshot with name {} for following cores {}", commitName, shardByCoreName.keySet()); + } - // Count number of failures per shard. - Map<String, Integer> failuresByShardId = new HashMap<>(); - for (Map.Entry<String, Slice> entry : shardByCoreName.entrySet()) { - int f = 0; - if (failuresByShardId.get(entry.getValue().getName()) != null) { - f = failuresByShardId.get(entry.getValue().getName()); - } - failuresByShardId.put(entry.getValue().getName(), f + 1); - } - - // Now that we know number of failures per shard, we can figure out - // if at-least one replica per shard was able to create a snapshot or not. - DocCollection collectionStatus = - ccc.getZkStateReader().getClusterState().getCollection(collectionName); - for (Map.Entry<String, Integer> entry : failuresByShardId.entrySet()) { - int replicaCount = collectionStatus.getSlice(entry.getKey()).getReplicas().size(); - if (replicaCount <= entry.getValue()) { - failedShards.add(entry.getKey()); - } + // A shard counts as captured only if at least one of its replicas reported a snapshot. Shards + // whose replicas were all skipped as inactive never received a request, so they must be + // checked against the collection state rather than against the requests that were sent. + Set<String> shardsWithSnapshot = new HashSet<>(); + for (CoreSnapshotMetaData replica : replicas) { + shardsWithSnapshot.add(replica.getShardId()); + } + for (Slice slice : collection.getActiveSlices()) { + if (!shardsWithSnapshot.contains(slice.getName())) { + failedShards.add(slice.getName()); } } diff --git a/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCloudSnapshotInactiveShard.java b/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCloudSnapshotInactiveShard.java new file mode 100644 index 00000000000..a1dc72b7bcd --- /dev/null +++ b/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCloudSnapshotInactiveShard.java @@ -0,0 +1,83 @@ +/* + * 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.core.snapshots; + +import java.util.Collection; +import org.apache.solr.client.solrj.impl.CloudSolrClient; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.cloud.Replica; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.embedded.JettySolrRunner; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * A collection snapshot must not be recorded as successful when a shard has no active replica to + * take a core snapshot. + */ +public class TestSolrCloudSnapshotInactiveShard extends SolrCloudTestCase { + + @BeforeClass + public static void setupClass() throws Exception { + useFactory("solr.StandardDirectoryFactory"); + configureCluster(2) + .addConfig( + "conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + .configure(); + } + + @Test + public void testSnapshotFailsWhenShardHasNoActiveReplica() throws Exception { + CloudSolrClient solrClient = cluster.getSolrClient(); + String collectionName = "snapshotInactiveShard"; + JettySolrRunner hostingNode = cluster.getJettySolrRunner(1); + + CollectionAdminRequest.createCollection(collectionName, "conf1", 1, 1) + .setCreateNodeSet(hostingNode.getNodeName()) + .process(solrClient); + cluster.waitForActiveCollection(collectionName, 1, 1); + + cluster.stopJettySolrRunner(hostingNode); + cluster.waitForJettyToStop(hostingNode); + waitForState( + "the only replica should not be active", + collectionName, + (liveNodes, collection) -> + collection != null + && collection.replicaStream().noneMatch(r -> r.getState() == Replica.State.ACTIVE)); + + String commitName = "snap"; + SolrException e = + expectThrows( + SolrException.class, + () -> + new CollectionAdminRequest.CreateSnapshot(collectionName, commitName) + .process(solrClient)); + assertTrue(e.getMessage(), e.getMessage().contains("Failed to create snapshot on shards")); + + Collection<CollectionSnapshotMetaData> snapshots = + SolrSnapshotManager.listSnapshots( + ZkStateReader.from(solrClient).getZkClient(), collectionName); + assertEquals(1, snapshots.size()); + CollectionSnapshotMetaData meta = snapshots.iterator().next(); + assertEquals(commitName, meta.getName()); + assertEquals(CollectionSnapshotMetaData.SnapshotStatus.Failed, meta.getStatus()); + assertTrue(meta.getReplicaSnapshots().isEmpty()); + } +}
