diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/SearchWithIndexBlocksIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/SearchWithIndexBlocksIT.java new file mode 100644 index 0000000000000..91cd699f89682 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/SearchWithIndexBlocksIT.java @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.search; + +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.ClosePointInTimeRequest; +import org.elasticsearch.action.search.OpenPointInTimeRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchShardsGroup; +import org.elasticsearch.action.search.SearchShardsRequest; +import org.elasticsearch.action.search.TransportClosePointInTimeAction; +import org.elasticsearch.action.search.TransportOpenPointInTimeAction; +import org.elasticsearch.action.search.TransportSearchShardsAction; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlocks; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.ProjectId; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.builder.PointInTimeBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.test.ESIntegTestCase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.cluster.block.ClusterBlocks.EMPTY_CLUSTER_BLOCK; +import static org.elasticsearch.test.ClusterServiceUtils.setState; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; + +public class SearchWithIndexBlocksIT extends ESIntegTestCase { + + public void testSearchIndicesWithIndexRefreshBlocks() { + List indices = createIndices(); + Map numDocsPerIndex = indexDocuments(indices); + List unblockedIndices = addIndexRefreshBlockToSomeIndices(indices); + + int expectedHits = 0; + for (String index : unblockedIndices) { + expectedHits += numDocsPerIndex.get(index); + } + + assertHitCount(prepareSearch().setQuery(QueryBuilders.matchAllQuery()), expectedHits); + } + + public void testOpenPITOnIndicesWithIndexRefreshBlocks() { + List indices = createIndices(); + Map numDocsPerIndex = indexDocuments(indices); + List unblockedIndices = addIndexRefreshBlockToSomeIndices(indices); + + int expectedHits = 0; + for (String index : unblockedIndices) { + expectedHits += numDocsPerIndex.get(index); + } + + BytesReference pitId = null; + try { + OpenPointInTimeRequest openPITRequest = new OpenPointInTimeRequest(indices.toArray(new String[0])).keepAlive( + TimeValue.timeValueSeconds(10) + ).allowPartialSearchResults(true); + pitId = client().execute(TransportOpenPointInTimeAction.TYPE, openPITRequest).actionGet().getPointInTimeId(); + SearchRequest searchRequest = new SearchRequest().source( + new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(pitId).setKeepAlive(TimeValue.timeValueSeconds(10))) + ); + assertHitCount(client().search(searchRequest), expectedHits); + } finally { + if (pitId != null) { + client().execute(TransportClosePointInTimeAction.TYPE, new ClosePointInTimeRequest(pitId)).actionGet(); + } + } + } + + public void testMultiSearchIndicesWithIndexRefreshBlocks() { + List indices = createIndices(); + Map numDocsPerIndex = indexDocuments(indices); + List unblockedIndices = addIndexRefreshBlockToSomeIndices(indices); + + int expectedHits = 0; + for (String index : unblockedIndices) { + expectedHits += numDocsPerIndex.get(index); + } + + final long expectedHitsL = expectedHits; + assertResponse( + client().prepareMultiSearch() + .add(prepareSearch().setQuery(QueryBuilders.matchAllQuery())) + .add(prepareSearch().setQuery(QueryBuilders.termQuery("field", "blah"))), + response -> { + assertHitCount(Objects.requireNonNull(response.getResponses()[0].getResponse()), expectedHitsL); + assertHitCount(Objects.requireNonNull(response.getResponses()[1].getResponse()), 0); + } + ); + } + + public void testSearchShardsOnIndicesWithIndexRefreshBlocks() { + List indices = createIndices(); + indexDocuments(indices); + List unblockedIndices = addIndexRefreshBlockToSomeIndices(indices); + + var resp = client().execute( + TransportSearchShardsAction.TYPE, + new SearchShardsRequest( + indices.toArray(new String[0]), + IndicesOptions.DEFAULT, + new MatchAllQueryBuilder(), + null, + null, + true, + null + ) + ).actionGet(); + for (SearchShardsGroup group : resp.getGroups()) { + assertTrue(unblockedIndices.contains(group.shardId().getIndex().getName())); + } + } + + private List createIndices() { + int numIndices = randomIntBetween(1, 3); + List indices = new ArrayList<>(); + for (int i = 0; i < numIndices; i++) { + indices.add("test" + i); + createIndex("test" + i); + } + return indices; + } + + private Map indexDocuments(List indices) { + Map numDocsPerIndex = new HashMap<>(); + List indexRequests = new ArrayList<>(); + for (String index : indices) { + int numDocs = randomIntBetween(0, 10); + numDocsPerIndex.put(index, numDocs); + for (int i = 0; i < numDocs; i++) { + indexRequests.add(prepareIndex(index).setId(String.valueOf(i)).setSource("field", "value")); + } + } + indexRandom(true, indexRequests); + + return numDocsPerIndex; + } + + private List addIndexRefreshBlockToSomeIndices(List indices) { + List unblockedIndices = new ArrayList<>(); + var blocksBuilder = ClusterBlocks.builder().blocks(EMPTY_CLUSTER_BLOCK); + for (String index : indices) { + boolean blockIndex = randomBoolean(); + if (blockIndex) { + blocksBuilder.addIndexBlock(ProjectId.DEFAULT, index, IndexMetadata.INDEX_REFRESH_BLOCK); + } else { + unblockedIndices.add(index); + } + } + + var dataNodes = clusterService().state().getNodes().getAllNodes(); + for (DiscoveryNode dataNode : dataNodes) { + ClusterService clusterService = internalCluster().getInstance(ClusterService.class, dataNode.getName()); + ClusterState currentState = clusterService.state(); + ClusterState newState = ClusterState.builder(currentState).blocks(blocksBuilder).build(); + setState(clusterService, newState); + } + + return unblockedIndices; + } +} diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index 69260bcac105c..d4a41fbd038da 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -1864,6 +1864,7 @@ List getLocalShardsIterator( Set indicesAndAliases, String[] concreteIndices ) { + concreteIndices = ignoreBlockedIndices(projectState, concreteIndices); var routingMap = indexNameExpressionResolver.resolveSearchRouting( projectState.metadata(), searchRequest.routing(), @@ -1896,6 +1897,20 @@ List getLocalShardsIterator( return Arrays.asList(list); } + static String[] ignoreBlockedIndices(ProjectState projectState, String[] concreteIndices) { + // optimization: mostly we do not have any blocks so there's no point in the expensive per-index checking + boolean hasIndexBlocks = projectState.blocks().indices(projectState.projectId()).isEmpty() == false; + if (hasIndexBlocks) { + return Arrays.stream(concreteIndices) + .filter( + index -> projectState.blocks() + .hasIndexBlock(projectState.projectId(), index, IndexMetadata.INDEX_REFRESH_BLOCK) == false + ) + .toArray(String[]::new); + } + return concreteIndices; + } + private interface TelemetryListener { void setRemotes(int count); diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java index 4346351c1576c..7e487c6aa39b6 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -1812,4 +1812,35 @@ public void onFailure(Exception ex) { assertTrue(ESTestCase.terminate(threadPool)); } } + + public void testIgnoreIndicesWithIndexRefreshBlock() { + int numIndices = randomIntBetween(1, 10); + String[] concreteIndices = new String[numIndices]; + for (int i = 0; i < numIndices; i++) { + concreteIndices[i] = "index" + i; + } + + List shuffledIndices = Arrays.asList(concreteIndices); + Collections.shuffle(shuffledIndices, random()); + concreteIndices = shuffledIndices.toArray(new String[0]); + + final ProjectId projectId = randomProjectIdOrDefault(); + ClusterBlocks.Builder blocksBuilder = ClusterBlocks.builder(); + int numBlockedIndices = randomIntBetween(0, numIndices); + for (int i = 0; i < numBlockedIndices; i++) { + blocksBuilder.addIndexBlock(projectId, concreteIndices[i], IndexMetadata.INDEX_REFRESH_BLOCK); + } + final ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .putProjectMetadata(ProjectMetadata.builder(projectId).build()) + .blocks(blocksBuilder) + .build(); + final ProjectState projectState = clusterState.projectState(projectId); + + String[] actual = TransportSearchAction.ignoreBlockedIndices(projectState, concreteIndices); + String[] expected = Arrays.stream(concreteIndices) + .filter(index -> clusterState.blocks().hasIndexBlock(projectId, index, IndexMetadata.INDEX_REFRESH_BLOCK) == false) + .toArray(String[]::new); + + assertThat(Arrays.asList(actual), containsInAnyOrder(expected)); + } }