|
35 | 35 | import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags; |
36 | 36 | import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; |
37 | 37 | import org.elasticsearch.action.admin.indices.stats.ShardStats; |
| 38 | +import org.elasticsearch.action.bulk.BulkAction; |
38 | 39 | import org.elasticsearch.action.index.IndexRequestBuilder; |
| 40 | +import org.elasticsearch.action.support.ActionTestUtils; |
39 | 41 | import org.elasticsearch.action.support.ActiveShardCount; |
40 | 42 | import org.elasticsearch.action.support.ChannelActionListener; |
41 | 43 | import org.elasticsearch.action.support.PlainActionFuture; |
| 44 | +import org.elasticsearch.action.support.SubscribableListener; |
42 | 45 | import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; |
| 46 | +import org.elasticsearch.action.support.master.AcknowledgedResponse; |
43 | 47 | import org.elasticsearch.action.support.replication.ReplicationResponse; |
44 | 48 | import org.elasticsearch.cluster.ClusterState; |
45 | 49 | import org.elasticsearch.cluster.ClusterStateListener; |
|
70 | 74 | import org.elasticsearch.common.unit.ByteSizeUnit; |
71 | 75 | import org.elasticsearch.common.unit.ByteSizeValue; |
72 | 76 | import org.elasticsearch.common.util.CollectionUtils; |
| 77 | +import org.elasticsearch.common.util.concurrent.EsExecutors; |
| 78 | +import org.elasticsearch.core.TimeValue; |
73 | 79 | import org.elasticsearch.gateway.ReplicaShardAllocatorIT; |
74 | 80 | import org.elasticsearch.index.Index; |
75 | 81 | import org.elasticsearch.index.IndexService; |
|
85 | 91 | import org.elasticsearch.index.seqno.ReplicationTracker; |
86 | 92 | import org.elasticsearch.index.seqno.RetentionLeases; |
87 | 93 | import org.elasticsearch.index.seqno.SequenceNumbers; |
| 94 | +import org.elasticsearch.index.shard.GlobalCheckpointListeners; |
88 | 95 | import org.elasticsearch.index.shard.IndexShard; |
89 | 96 | import org.elasticsearch.index.shard.ShardId; |
90 | 97 | import org.elasticsearch.index.store.Store; |
|
122 | 129 | import java.util.Map; |
123 | 130 | import java.util.concurrent.CountDownLatch; |
124 | 131 | import java.util.concurrent.ExecutionException; |
| 132 | +import java.util.concurrent.Executor; |
125 | 133 | import java.util.concurrent.Semaphore; |
| 134 | +import java.util.concurrent.TimeUnit; |
126 | 135 | import java.util.concurrent.atomic.AtomicBoolean; |
127 | 136 | import java.util.function.Consumer; |
128 | 137 | import java.util.stream.Collectors; |
|
132 | 141 | import static java.util.stream.Collectors.toList; |
133 | 142 | import static org.elasticsearch.action.DocWriteResponse.Result.CREATED; |
134 | 143 | import static org.elasticsearch.action.DocWriteResponse.Result.UPDATED; |
| 144 | +import static org.elasticsearch.action.support.ActionTestUtils.assertNoFailureListener; |
135 | 145 | import static org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING; |
136 | 146 | import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; |
137 | 147 | import static org.elasticsearch.node.RecoverySettingsChunkSizePlugin.CHUNK_SIZE_SETTING; |
@@ -1688,6 +1698,104 @@ public void testWaitForClusterStateToBeAppliedOnSourceNode() throws Exception { |
1688 | 1698 | } |
1689 | 1699 | } |
1690 | 1700 |
|
| 1701 | + public void testDeleteIndexDuringFinalization() throws Exception { |
| 1702 | + internalCluster().startMasterOnlyNode(); |
| 1703 | + final var primaryNode = internalCluster().startDataOnlyNode(); |
| 1704 | + String indexName = "test-index"; |
| 1705 | + createIndex(indexName, indexSettings(1, 0).build()); |
| 1706 | + ensureGreen(indexName); |
| 1707 | + final List<IndexRequestBuilder> indexRequests = IntStream.range(0, between(10, 500)) |
| 1708 | + .mapToObj(n -> client().prepareIndex(indexName).setSource("foo", "bar")) |
| 1709 | + .toList(); |
| 1710 | + indexRandom(randomBoolean(), true, true, indexRequests); |
| 1711 | + assertThat(indicesAdmin().prepareFlush(indexName).get().getFailedShards(), equalTo(0)); |
| 1712 | + |
| 1713 | + final var replicaNode = internalCluster().startDataOnlyNode(); |
| 1714 | + |
| 1715 | + final SubscribableListener<Void> recoveryCompleteListener = new SubscribableListener<>(); |
| 1716 | + final PlainActionFuture<AcknowledgedResponse> deleteListener = new PlainActionFuture<>(); |
| 1717 | + |
| 1718 | + final var threadPool = internalCluster().clusterService().threadPool(); |
| 1719 | + |
| 1720 | + final var indexId = internalCluster().clusterService().state().routingTable().index(indexName).getIndex(); |
| 1721 | + final var primaryIndexShard = internalCluster().getInstance(IndicesService.class, primaryNode) |
| 1722 | + .indexServiceSafe(indexId) |
| 1723 | + .getShard(0); |
| 1724 | + final var globalCheckpointBeforeRecovery = primaryIndexShard.getLastSyncedGlobalCheckpoint(); |
| 1725 | + |
| 1726 | + final var replicaNodeTransportService = asInstanceOf( |
| 1727 | + MockTransportService.class, |
| 1728 | + internalCluster().getInstance(TransportService.class, replicaNode) |
| 1729 | + ); |
| 1730 | + replicaNodeTransportService.addRequestHandlingBehavior( |
| 1731 | + PeerRecoveryTargetService.Actions.TRANSLOG_OPS, |
| 1732 | + (handler, request, channel, task) -> handler.messageReceived( |
| 1733 | + request, |
| 1734 | + new TestTransportChannel(ActionTestUtils.assertNoFailureListener(response -> { |
| 1735 | + // Process the TRANSLOG_OPS response on the replica (avoiding failing it due to a concurrent delete) but |
| 1736 | + // before sending the response back send another document to the primary, advancing the GCP to prevent the replica |
| 1737 | + // being marked as in-sync (NB below we delay the replica write until after the index is deleted) |
| 1738 | + client().prepareIndex(indexName).setSource("foo", "baz").execute(ActionListener.noop()); |
| 1739 | + |
| 1740 | + primaryIndexShard.addGlobalCheckpointListener( |
| 1741 | + globalCheckpointBeforeRecovery + 1, |
| 1742 | + new GlobalCheckpointListeners.GlobalCheckpointListener() { |
| 1743 | + @Override |
| 1744 | + public Executor executor() { |
| 1745 | + return EsExecutors.DIRECT_EXECUTOR_SERVICE; |
| 1746 | + } |
| 1747 | + |
| 1748 | + @Override |
| 1749 | + public void accept(long globalCheckpoint, Exception e) { |
| 1750 | + assertNull(e); |
| 1751 | + |
| 1752 | + // Now the GCP has advanced the replica won't be marked in-sync so respond to the TRANSLOG_OPS request |
| 1753 | + // to start recovery finalization |
| 1754 | + try { |
| 1755 | + channel.sendResponse(response); |
| 1756 | + } catch (IOException ex) { |
| 1757 | + fail(ex); |
| 1758 | + } |
| 1759 | + |
| 1760 | + // Wait a short while for finalization to block on advancing the replica's GCP and then delete the index |
| 1761 | + threadPool.schedule( |
| 1762 | + () -> client().admin().indices().prepareDelete(indexName).execute(deleteListener), |
| 1763 | + TimeValue.timeValueMillis(100), |
| 1764 | + EsExecutors.DIRECT_EXECUTOR_SERVICE |
| 1765 | + ); |
| 1766 | + } |
| 1767 | + }, |
| 1768 | + TimeValue.timeValueSeconds(10) |
| 1769 | + ); |
| 1770 | + })), |
| 1771 | + task |
| 1772 | + ) |
| 1773 | + ); |
| 1774 | + |
| 1775 | + // delay the delivery of the replica write until the end of the test so the replica never becomes in-sync |
| 1776 | + replicaNodeTransportService.addRequestHandlingBehavior( |
| 1777 | + BulkAction.NAME + "[s][r]", |
| 1778 | + (handler, request, channel, task) -> recoveryCompleteListener.addListener( |
| 1779 | + assertNoFailureListener(ignored -> handler.messageReceived(request, channel, task)) |
| 1780 | + ) |
| 1781 | + ); |
| 1782 | + |
| 1783 | + // Create the replica to trigger the whole process |
| 1784 | + assertAcked( |
| 1785 | + client().admin() |
| 1786 | + .indices() |
| 1787 | + .prepareUpdateSettings(indexName) |
| 1788 | + .setSettings(Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1)) |
| 1789 | + ); |
| 1790 | + |
| 1791 | + // Wait for the index to be deleted |
| 1792 | + assertTrue(deleteListener.get(20, TimeUnit.SECONDS).isAcknowledged()); |
| 1793 | + |
| 1794 | + final var peerRecoverySourceService = internalCluster().getInstance(PeerRecoverySourceService.class, primaryNode); |
| 1795 | + assertBusy(() -> assertEquals(0, peerRecoverySourceService.numberOfOngoingRecoveries())); |
| 1796 | + recoveryCompleteListener.onResponse(null); |
| 1797 | + } |
| 1798 | + |
1691 | 1799 | private void assertGlobalCheckpointIsStableAndSyncedInAllNodes(String indexName, List<String> nodes, int shard) throws Exception { |
1692 | 1800 | assertThat(nodes, is(not(empty()))); |
1693 | 1801 |
|
|
0 commit comments