Skip to content

Commit dc4e9f8

Browse files
martijnvgdakrone
andauthored
[8.2] Allow the modify data stream api to remove broken reference to a backing index (#87030) (#87084)
* Allow the modify data stream api to remove broken reference to a backing index (#87030) This adds a `force_remove_backing_index` action to the modify data stream api to allow removing broken reference to backing indices from a data stream. ``` POST _data_stream/_modify { "actions": [ { "force_remove_backing_index": { "data_stream": "my-logs", "index": ".ds-my-logs-2099.01.01-000001" } } ] } ``` * Fix compilation for backport * Fix test Co-authored-by: Lee Hinman <[email protected]>
1 parent 5fa2ed3 commit dc4e9f8

File tree

5 files changed

+303
-16
lines changed

5 files changed

+303
-16
lines changed

modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
2929
import org.elasticsearch.action.admin.indices.rollover.RolloverResponse;
3030
import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
31+
import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
3132
import org.elasticsearch.action.admin.indices.template.delete.DeleteComposableIndexTemplateAction;
3233
import org.elasticsearch.action.admin.indices.template.get.GetComposableIndexTemplateAction;
3334
import org.elasticsearch.action.admin.indices.template.put.PutComposableIndexTemplateAction;
@@ -40,6 +41,7 @@
4041
import org.elasticsearch.action.datastreams.DeleteDataStreamAction;
4142
import org.elasticsearch.action.datastreams.GetDataStreamAction;
4243
import org.elasticsearch.action.datastreams.GetDataStreamAction.Response.DataStreamInfo;
44+
import org.elasticsearch.action.datastreams.ModifyDataStreamsAction;
4345
import org.elasticsearch.action.delete.DeleteRequest;
4446
import org.elasticsearch.action.index.IndexRequest;
4547
import org.elasticsearch.action.index.IndexResponse;
@@ -50,17 +52,23 @@
5052
import org.elasticsearch.action.search.SearchResponse;
5153
import org.elasticsearch.action.update.UpdateRequest;
5254
import org.elasticsearch.cluster.ClusterState;
55+
import org.elasticsearch.cluster.ClusterStateTaskExecutor;
56+
import org.elasticsearch.cluster.ClusterStateUpdateTask;
5357
import org.elasticsearch.cluster.health.ClusterHealthStatus;
5458
import org.elasticsearch.cluster.metadata.AliasMetadata;
5559
import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
5660
import org.elasticsearch.cluster.metadata.DataStream;
61+
import org.elasticsearch.cluster.metadata.DataStreamAction;
5762
import org.elasticsearch.cluster.metadata.DataStreamAlias;
5863
import org.elasticsearch.cluster.metadata.IndexMetadata;
64+
import org.elasticsearch.cluster.metadata.Metadata;
5965
import org.elasticsearch.cluster.metadata.Template;
66+
import org.elasticsearch.cluster.service.ClusterService;
6067
import org.elasticsearch.common.Strings;
6168
import org.elasticsearch.common.compress.CompressedXContent;
6269
import org.elasticsearch.common.settings.Settings;
6370
import org.elasticsearch.core.Nullable;
71+
import org.elasticsearch.index.Index;
6472
import org.elasticsearch.index.IndexNotFoundException;
6573
import org.elasticsearch.index.mapper.DataStreamTimestampFieldMapper;
6674
import org.elasticsearch.index.mapper.DateFieldMapper;
@@ -88,9 +96,11 @@
8896
import java.util.Map;
8997
import java.util.Optional;
9098
import java.util.Set;
99+
import java.util.concurrent.CountDownLatch;
91100
import java.util.concurrent.CyclicBarrier;
92101
import java.util.concurrent.ExecutionException;
93102
import java.util.concurrent.atomic.AtomicBoolean;
103+
import java.util.concurrent.atomic.AtomicReference;
94104
import java.util.stream.Collectors;
95105
import java.util.stream.IntStream;
96106

@@ -1711,6 +1721,74 @@ public void testCreateIndexAliasWithSameNameAsDataStreamAlias() throws Exception
17111721
}
17121722
}
17131723

1724+
public void testRemoveGhostReference() throws Exception {
1725+
String dataStreamName = "logs-es";
1726+
DataStreamIT.putComposableIndexTemplate("my-template", List.of("logs-*"));
1727+
var request = new CreateDataStreamAction.Request(dataStreamName);
1728+
assertAcked(client().execute(CreateDataStreamAction.INSTANCE, request).actionGet());
1729+
assertAcked(client().admin().indices().rolloverIndex(new RolloverRequest(dataStreamName, null)).actionGet());
1730+
var indicesStatsResponse = client().admin().indices().stats(new IndicesStatsRequest()).actionGet();
1731+
assertThat(indicesStatsResponse.getIndices().size(), equalTo(2));
1732+
ClusterState before = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state();
1733+
assertThat(before.getMetadata().dataStreams().get(dataStreamName).getIndices(), hasSize(2));
1734+
1735+
CountDownLatch latch = new CountDownLatch(1);
1736+
AtomicReference<DataStream> brokenDataStreamHolder = new AtomicReference<>();
1737+
internalCluster().getCurrentMasterNodeInstance(ClusterService.class)
1738+
.submitStateUpdateTask(getTestName(), new ClusterStateUpdateTask() {
1739+
@Override
1740+
public ClusterState execute(ClusterState currentState) throws Exception {
1741+
DataStream original = currentState.getMetadata().dataStreams().get(dataStreamName);
1742+
DataStream broken = new DataStream(
1743+
original.getName(),
1744+
List.of(new Index(original.getIndices().get(0).getName(), "broken"), original.getIndices().get(1)),
1745+
original.getGeneration(),
1746+
original.getMetadata(),
1747+
original.isHidden(),
1748+
original.isReplicated(),
1749+
original.isSystem(),
1750+
original.isAllowCustomRouting(),
1751+
original.getIndexMode()
1752+
);
1753+
brokenDataStreamHolder.set(broken);
1754+
return ClusterState.builder(currentState)
1755+
.metadata(Metadata.builder(currentState.getMetadata()).put(broken).build())
1756+
.build();
1757+
}
1758+
1759+
@Override
1760+
public void clusterStateProcessed(ClusterState oldState, ClusterState newState) {
1761+
latch.countDown();
1762+
}
1763+
1764+
@Override
1765+
public void onFailure(Exception e) {
1766+
logger.error("error while adding a broken data stream", e);
1767+
latch.countDown();
1768+
}
1769+
}, ClusterStateTaskExecutor.unbatched());
1770+
latch.await();
1771+
var ghostReference = brokenDataStreamHolder.get().getIndices().get(0);
1772+
1773+
// Many APIs fail with NPE, because of broken data stream:
1774+
expectThrows(NullPointerException.class, () -> client().admin().indices().stats(new IndicesStatsRequest()).actionGet());
1775+
expectThrows(NullPointerException.class, () -> client().search(new SearchRequest()).actionGet());
1776+
1777+
assertAcked(
1778+
client().execute(
1779+
ModifyDataStreamsAction.INSTANCE,
1780+
new ModifyDataStreamsAction.Request(List.of(DataStreamAction.removeBackingIndex(dataStreamName, ghostReference.getName())))
1781+
).actionGet()
1782+
);
1783+
ClusterState after = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state();
1784+
assertThat(after.getMetadata().dataStreams().get(dataStreamName).getIndices(), hasSize(1));
1785+
// Data stream resolves now to one backing index.
1786+
// Note, that old backing index still exists and has been unhidden.
1787+
// The modify data stream api only fixed the data stream by removing a broken reference to a backing index.
1788+
indicesStatsResponse = client().admin().indices().stats(new IndicesStatsRequest()).actionGet();
1789+
assertThat(indicesStatsResponse.getIndices().size(), equalTo(2));
1790+
}
1791+
17141792
private static void verifyResolvability(String dataStream, ActionRequestBuilder<?, ?> requestBuilder, boolean fail) {
17151793
verifyResolvability(dataStream, requestBuilder, fail, 0);
17161794
}

server/src/main/java/org/elasticsearch/action/datastreams/ModifyDataStreamsAction.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ private ModifyDataStreamsAction() {
4141

4242
public static final class Request extends AcknowledgedRequest<Request> implements IndicesRequest, ToXContentObject {
4343

44-
// relevant only for authorizing the request, so require every specified
45-
// index to exist, expand wildcards only to open indices, prohibit
46-
// wildcard expressions that resolve to zero indices, and do not attempt
47-
// to resolve expressions as aliases
44+
// The actual DataStreamAction don't support wildcards, so supporting it doesn't make sense.
45+
// Also supporting wildcards it would prohibit this api from removing broken references to backing indices. (in case of bugs).
46+
// For this case, when removing broken backing indices references that don't exist, we need to allow ignore_unavailable and
47+
// allow_no_indices. Otherwise, the data stream can't be repaired.
4848
private static final IndicesOptions INDICES_OPTIONS = IndicesOptions.fromOptions(
49+
true,
50+
true,
4951
false,
5052
false,
51-
true,
5253
false,
53-
true,
5454
false,
5555
true,
5656
false
@@ -108,7 +108,9 @@ public ActionRequestValidationException validate() {
108108

109109
@Override
110110
public String[] indices() {
111-
return actions.stream().map(DataStreamAction::getDataStream).toArray(String[]::new);
111+
// Return the indices instead of data streams, this api can be used to repair a broken data stream definition and
112+
// in that case, exceptions can occur while resolving data streams for doing authorization or looking up index blocks.
113+
return actions.stream().map(DataStreamAction::getIndex).toArray(String[]::new);
112114
}
113115

114116
@Override

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataDataStreamsService.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.elasticsearch.common.Priority;
2020
import org.elasticsearch.common.settings.Settings;
2121
import org.elasticsearch.core.SuppressForbidden;
22+
import org.elasticsearch.index.Index;
2223
import org.elasticsearch.index.mapper.MapperService;
2324
import org.elasticsearch.indices.IndicesService;
2425

@@ -122,17 +123,29 @@ private static void addBackingIndex(
122123
}
123124

124125
private static void removeBackingIndex(Metadata metadata, Metadata.Builder builder, String dataStreamName, String indexName) {
125-
var dataStream = validateDataStream(metadata, dataStreamName);
126-
var index = validateIndex(metadata, indexName);
127-
var writeIndex = metadata.index(index.getWriteIndex());
128-
builder.put(dataStream.getDataStream().removeBackingIndex(writeIndex.getIndex()));
126+
boolean indexNotRemoved = true;
127+
var dataStream = validateDataStream(metadata, dataStreamName).getDataStream();
128+
for (Index backingIndex : dataStream.getIndices()) {
129+
if (backingIndex.getName().equals(indexName)) {
130+
builder.put(dataStream.removeBackingIndex(backingIndex));
131+
indexNotRemoved = false;
132+
break;
133+
}
134+
}
135+
136+
if (indexNotRemoved) {
137+
throw new IllegalArgumentException("index [" + indexName + "] not found");
138+
}
129139

130140
// un-hide index
131-
builder.put(
132-
IndexMetadata.builder(writeIndex)
133-
.settings(Settings.builder().put(writeIndex.getSettings()).put("index.hidden", "false").build())
134-
.settingsVersion(writeIndex.getSettingsVersion() + 1)
135-
);
141+
var indexMetadata = builder.get(indexName);
142+
if (indexMetadata != null) {
143+
builder.put(
144+
IndexMetadata.builder(indexMetadata)
145+
.settings(Settings.builder().put(indexMetadata.getSettings()).put("index.hidden", "false").build())
146+
.settingsVersion(indexMetadata.getSettingsVersion() + 1)
147+
);
148+
}
136149
}
137150

138151
private static IndexAbstraction.DataStream validateDataStream(Metadata metadata, String dataStreamName) {

server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDataStreamsServiceTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.cluster.ClusterState;
1414
import org.elasticsearch.common.Strings;
1515
import org.elasticsearch.common.settings.Settings;
16+
import org.elasticsearch.core.Tuple;
1617
import org.elasticsearch.index.Index;
1718
import org.elasticsearch.index.mapper.MapperService;
1819
import org.elasticsearch.index.mapper.MapperServiceTestCase;
@@ -26,6 +27,7 @@
2627
import static org.hamcrest.Matchers.containsInAnyOrder;
2728
import static org.hamcrest.Matchers.containsString;
2829
import static org.hamcrest.Matchers.equalTo;
30+
import static org.hamcrest.Matchers.hasSize;
2931
import static org.hamcrest.Matchers.notNullValue;
3032

3133
public class MetadataDataStreamsServiceTests extends MapperServiceTestCase {
@@ -337,6 +339,48 @@ public void testMissingIndex() {
337339
assertThat(e.getMessage(), equalTo("index [" + missingIndex + "] not found"));
338340
}
339341

342+
public void testRemoveBrokenBackingIndexReference() {
343+
var dataStreamName = "my-logs";
344+
var state = DataStreamTestHelper.getClusterStateWithDataStreams(List.of(new Tuple<>(dataStreamName, 2)), List.of());
345+
var original = state.getMetadata().dataStreams().get(dataStreamName);
346+
var broken = new DataStream(
347+
original.getName(),
348+
List.of(new Index(original.getIndices().get(0).getName(), "broken"), original.getIndices().get(1)),
349+
original.getGeneration(),
350+
original.getMetadata(),
351+
original.isHidden(),
352+
original.isReplicated(),
353+
original.isSystem(),
354+
original.isAllowCustomRouting(),
355+
original.getIndexMode()
356+
);
357+
var brokenState = ClusterState.builder(state).metadata(Metadata.builder(state.getMetadata()).put(broken).build()).build();
358+
359+
var result = MetadataDataStreamsService.modifyDataStream(
360+
brokenState,
361+
List.of(DataStreamAction.removeBackingIndex(dataStreamName, broken.getIndices().get(0).getName())),
362+
this::getMapperService
363+
);
364+
assertThat(result.getMetadata().dataStreams().get(dataStreamName).getIndices(), hasSize(1));
365+
assertThat(result.getMetadata().dataStreams().get(dataStreamName).getIndices().get(0), equalTo(original.getIndices().get(1)));
366+
}
367+
368+
public void testRemoveBackingIndexThatDoesntExist() {
369+
var dataStreamName = "my-logs";
370+
var state = DataStreamTestHelper.getClusterStateWithDataStreams(List.of(new Tuple<>(dataStreamName, 2)), List.of());
371+
372+
String indexToRemove = DataStream.getDefaultBackingIndexName(dataStreamName, 3);
373+
var e = expectThrows(
374+
IllegalArgumentException.class,
375+
() -> MetadataDataStreamsService.modifyDataStream(
376+
state,
377+
List.of(DataStreamAction.removeBackingIndex(dataStreamName, indexToRemove)),
378+
this::getMapperService
379+
)
380+
);
381+
assertThat(e.getMessage(), equalTo("index [" + indexToRemove + "] not found"));
382+
}
383+
340384
private MapperService getMapperService(IndexMetadata im) {
341385
try {
342386
String mapping = im.mapping().source().toString();

0 commit comments

Comments
 (0)