Skip to content

Commit c7df2b8

Browse files
dnhatnywelsch
andauthored
Noop peer recoveries on closed index (#41400)
If users close an index to change some non-dynamic index settings, then the current implementation forces replicas of that closed index to copy over segment files from the primary. With this change, we make peer recoveries of closed index skip both phases. Relates #33888 Co-authored-by: Yannick Welsch <[email protected]>
1 parent 2864e98 commit c7df2b8

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,10 @@ public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperS
25592559
@Override
25602560
public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException {
25612561
final long currentLocalCheckpoint = getLocalCheckpointTracker().getCheckpoint();
2562+
// avoid scanning translog if not necessary
2563+
if (startingSeqNo > currentLocalCheckpoint) {
2564+
return true;
2565+
}
25622566
final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1);
25632567
try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) {
25642568
Translog.Operation operation;

server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ public int estimateNumberOfHistoryOperations(String source, MapperService mapper
300300

301301
@Override
302302
public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException {
303-
return false;
303+
// we can do operation-based recovery if we don't have to replay any operation.
304+
return startingSeqNo > seqNoStats.getMaxSeqNo();
304305
}
305306

306307
@Override

server/src/test/java/org/elasticsearch/indices/state/CloseIndexIT.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,31 @@
2222
import org.elasticsearch.action.ActionRequestValidationException;
2323
import org.elasticsearch.action.admin.indices.close.CloseIndexResponse;
2424
import org.elasticsearch.action.support.ActiveShardCount;
25+
import org.elasticsearch.client.Client;
2526
import org.elasticsearch.cluster.ClusterState;
2627
import org.elasticsearch.cluster.block.ClusterBlockException;
2728
import org.elasticsearch.cluster.health.ClusterHealthStatus;
2829
import org.elasticsearch.cluster.metadata.IndexMetaData;
2930
import org.elasticsearch.cluster.metadata.MetaDataIndexStateService;
31+
import org.elasticsearch.cluster.node.DiscoveryNode;
3032
import org.elasticsearch.cluster.routing.ShardRouting;
3133
import org.elasticsearch.common.settings.Settings;
3234
import org.elasticsearch.common.unit.ByteSizeUnit;
3335
import org.elasticsearch.common.unit.ByteSizeValue;
36+
import org.elasticsearch.common.util.set.Sets;
3437
import org.elasticsearch.index.IndexNotFoundException;
3538
import org.elasticsearch.index.IndexSettings;
3639
import org.elasticsearch.indices.IndexClosedException;
40+
import org.elasticsearch.indices.recovery.RecoveryState;
3741
import org.elasticsearch.test.BackgroundIndexer;
3842
import org.elasticsearch.test.ESIntegTestCase;
43+
import org.elasticsearch.test.InternalTestCluster;
3944

4045
import java.util.ArrayList;
4146
import java.util.List;
4247
import java.util.Locale;
4348
import java.util.concurrent.CountDownLatch;
49+
import java.util.stream.Collectors;
4450
import java.util.stream.IntStream;
4551

4652
import static java.util.Collections.emptySet;
@@ -50,9 +56,11 @@
5056
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
5157
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
5258
import static org.hamcrest.Matchers.containsString;
59+
import static org.hamcrest.Matchers.empty;
5360
import static org.hamcrest.Matchers.equalTo;
5461
import static org.hamcrest.Matchers.hasSize;
5562
import static org.hamcrest.Matchers.is;
63+
import static org.hamcrest.Matchers.not;
5664
import static org.hamcrest.Matchers.notNullValue;
5765

5866
public class CloseIndexIT extends ESIntegTestCase {
@@ -338,6 +346,81 @@ public void testCloseIndexWaitForActiveShards() throws Exception {
338346
assertIndexIsClosed(indexName);
339347
}
340348

349+
public void testNoopPeerRecoveriesWhenIndexClosed() throws Exception {
350+
final String indexName = "noop-peer-recovery-test";
351+
int numberOfReplicas = between(1, 2);
352+
internalCluster().ensureAtLeastNumDataNodes(numberOfReplicas + between(1, 2));
353+
createIndex(indexName, Settings.builder()
354+
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
355+
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, numberOfReplicas)
356+
.put("index.routing.rebalance.enable", "none")
357+
.build());
358+
int iterations = between(1, 3);
359+
for (int iter = 0; iter < iterations; iter++) {
360+
indexRandom(randomBoolean(), randomBoolean(), randomBoolean(), IntStream.range(0, randomIntBetween(0, 50))
361+
.mapToObj(n -> client().prepareIndex(indexName, "_doc").setSource("num", n)).collect(toList()));
362+
ensureGreen(indexName);
363+
364+
// Closing an index should execute noop peer recovery
365+
assertAcked(client().admin().indices().prepareClose(indexName).get());
366+
assertIndexIsClosed(indexName);
367+
ensureGreen(indexName);
368+
assertNoFileBasedRecovery(indexName);
369+
internalCluster().assertSameDocIdsOnShards();
370+
371+
// Open a closed index should execute noop recovery
372+
assertAcked(client().admin().indices().prepareOpen(indexName).get());
373+
assertIndexIsOpened(indexName);
374+
ensureGreen(indexName);
375+
assertNoFileBasedRecovery(indexName);
376+
internalCluster().assertSameDocIdsOnShards();
377+
}
378+
}
379+
380+
/**
381+
* Ensures that if a replica of a closed index does not have the same content as the primary, then a file-based recovery will occur.
382+
*/
383+
public void testRecoverExistingReplica() throws Exception {
384+
final String indexName = "test-recover-existing-replica";
385+
internalCluster().ensureAtLeastNumDataNodes(2);
386+
List<String> dataNodes = randomSubsetOf(2, Sets.newHashSet(
387+
clusterService().state().nodes().getDataNodes().valuesIt()).stream().map(DiscoveryNode::getName).collect(Collectors.toSet()));
388+
createIndex(indexName, Settings.builder()
389+
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
390+
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)
391+
.put("index.routing.allocation.include._name", String.join(",", dataNodes))
392+
.build());
393+
indexRandom(randomBoolean(), randomBoolean(), randomBoolean(), IntStream.range(0, randomIntBetween(0, 50))
394+
.mapToObj(n -> client().prepareIndex(indexName, "_doc").setSource("num", n)).collect(toList()));
395+
ensureGreen(indexName);
396+
if (randomBoolean()) {
397+
client().admin().indices().prepareFlush(indexName).get();
398+
} else {
399+
client().admin().indices().prepareSyncedFlush(indexName).get();
400+
}
401+
// index more documents while one shard copy is offline
402+
internalCluster().restartNode(dataNodes.get(1), new InternalTestCluster.RestartCallback() {
403+
@Override
404+
public Settings onNodeStopped(String nodeName) throws Exception {
405+
Client client = client(dataNodes.get(0));
406+
int moreDocs = randomIntBetween(1, 50);
407+
for (int i = 0; i < moreDocs; i++) {
408+
client.prepareIndex(indexName, "_doc").setSource("num", i).get();
409+
}
410+
assertAcked(client.admin().indices().prepareClose(indexName));
411+
return super.onNodeStopped(nodeName);
412+
}
413+
});
414+
assertIndexIsClosed(indexName);
415+
ensureGreen(indexName);
416+
internalCluster().assertSameDocIdsOnShards();
417+
for (RecoveryState recovery : client().admin().indices().prepareRecoveries(indexName).get().shardRecoveryStates().get(indexName)) {
418+
if (recovery.getPrimary() == false) {
419+
assertThat(recovery.getIndex().fileDetails(), not(empty()));
420+
}
421+
}
422+
}
423+
341424
static void assertIndexIsClosed(final String... indices) {
342425
final ClusterState clusterState = client().admin().cluster().prepareState().get().getState();
343426
for (String index : indices) {
@@ -383,4 +466,12 @@ static void assertException(final Throwable throwable, final String indexName) {
383466
fail("Unexpected exception: " + t);
384467
}
385468
}
469+
470+
void assertNoFileBasedRecovery(String indexName) {
471+
for (RecoveryState recovery : client().admin().indices().prepareRecoveries(indexName).get().shardRecoveryStates().get(indexName)) {
472+
if (recovery.getPrimary() == false) {
473+
assertThat(recovery.getIndex().fileDetails(), empty());
474+
}
475+
}
476+
}
386477
}

0 commit comments

Comments
 (0)