Skip to content

Commit 11bd5a0

Browse files
authored
Fix mapping conflicts in clone/split/shrink APIs (elastic#137096) (elastic#137118)
If an index is in either `logsdb` or `time_series` mode and specifies a non-default `@timestamp` type mapping (e.g. `date_nanos`), using the clone, split, or shrink API will result in shards that are unable to initialize/recover due to a mapping conflict. As of elastic#133954, the `searchable_snapshot` ILM action makes use of the clone API by default - if the index has more than `0` replicas - and will thus also run into this issue.
1 parent fb5008a commit 11bd5a0

File tree

6 files changed

+170
-14
lines changed

6 files changed

+170
-14
lines changed

docs/changelog/137096.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 137096
2+
summary: Fix mapping conflicts in clone/split/shrink APIs
3+
area: Indices APIs
4+
type: bug
5+
issues: []

server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.index.seqno.SeqNoStats;
2121
import org.elasticsearch.test.ESIntegTestCase;
2222
import org.elasticsearch.test.index.IndexVersionUtils;
23+
import org.elasticsearch.xcontent.ObjectPath;
2324
import org.elasticsearch.xcontent.XContentType;
2425

2526
import java.util.List;
@@ -203,4 +204,60 @@ public void testResizeChangeIndexSorts() {
203204
});
204205
assertThat(error.getMessage(), containsString("can't override index sort when resizing an index"));
205206
}
207+
208+
/**
209+
* Test that cloning a logsdb index with a non-default timestamp mapping doesn't result in any mapping conflicts.
210+
*/
211+
public void testCloneLogsdbIndexWithNonDefaultTimestamp() {
212+
// Create a logsdb index with a date_nanos @timestamp field
213+
final int numberOfReplicas = randomInt(internalCluster().numDataNodes() - 1);
214+
final var settings = indexSettings(1, numberOfReplicas).put("index.mode", "logsdb").put("index.blocks.write", true);
215+
prepareCreate("source").setSettings(settings).setMapping("@timestamp", "type=date_nanos").get();
216+
ensureGreen();
217+
218+
// Clone the index
219+
indicesAdmin().prepareResizeIndex("source", "target")
220+
.setResizeType(ResizeType.CLONE)
221+
// We need to explicitly set the number of replicas in case the source has 0 replicas and the cluster has only 1 data node
222+
.setSettings(Settings.builder().put("index.number_of_replicas", numberOfReplicas).build())
223+
.get();
224+
225+
// Verify that the target index has the correct @timestamp mapping
226+
final var targetMappings = indicesAdmin().prepareGetMappings("target").get();
227+
assertThat(
228+
ObjectPath.eval("[email protected]", targetMappings.mappings().get("target").getSourceAsMap()),
229+
equalTo("date_nanos")
230+
);
231+
ensureGreen();
232+
}
233+
234+
/**
235+
* Test that cloning a time series index with a non-default timestamp mapping doesn't result in any mapping conflicts.
236+
*/
237+
public void testCloneTimeSeriesIndexWithNonDefaultTimestamp() {
238+
// Create a time series index with a date_nanos @timestamp field
239+
final int numberOfReplicas = randomInt(internalCluster().numDataNodes() - 1);
240+
final var settings = indexSettings(1, numberOfReplicas).put("index.mode", "time_series")
241+
.put("index.routing_path", "sensor_id")
242+
.put("index.blocks.write", true);
243+
prepareCreate("source").setSettings(settings)
244+
.setMapping("@timestamp", "type=date_nanos", "sensor_id", "type=keyword,time_series_dimension=true")
245+
.get();
246+
ensureGreen();
247+
248+
// Clone the index
249+
indicesAdmin().prepareResizeIndex("source", "target")
250+
.setResizeType(ResizeType.CLONE)
251+
// We need to explicitly set the number of replicas in case the source has 0 replicas and the cluster has only 1 data node
252+
.setSettings(Settings.builder().put("index.number_of_replicas", numberOfReplicas).build())
253+
.get();
254+
255+
// Verify that the target index has the correct @timestamp mapping
256+
final var targetMappings = indicesAdmin().prepareGetMappings("target").get();
257+
assertThat(
258+
ObjectPath.eval("[email protected]", targetMappings.mappings().get("target").getSourceAsMap()),
259+
equalTo("date_nanos")
260+
);
261+
ensureGreen();
262+
}
206263
}

server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.elasticsearch.indices.IndicesService;
5252
import org.elasticsearch.test.ESIntegTestCase;
5353
import org.elasticsearch.test.index.IndexVersionUtils;
54+
import org.elasticsearch.xcontent.ObjectPath;
5455
import org.elasticsearch.xcontent.XContentType;
5556

5657
import java.util.Arrays;
@@ -608,6 +609,63 @@ public void testShrinkThenSplitWithFailedNode() throws Exception {
608609
assertNoResizeSourceIndexSettings("splitagain");
609610
}
610611

612+
/**
613+
* Tests that shrinking a logsdb index with a non-default timestamp mapping doesn't result in any mapping conflicts.
614+
*/
615+
public void testShrinkLogsdbIndexWithNonDefaultTimestamp() {
616+
// Create a logsdb index with a date_nanos @timestamp field
617+
final var settings = indexSettings(2, 0).put("index.mode", "logsdb")
618+
.put("index.blocks.write", true)
619+
.put("index.routing.allocation.require._name", internalCluster().getRandomDataNodeName());
620+
prepareCreate("source").setSettings(settings).setMapping("@timestamp", "type=date_nanos").get();
621+
ensureGreen();
622+
623+
// Shrink the index
624+
indicesAdmin().prepareResizeIndex("source", "target")
625+
.setResizeType(ResizeType.SHRINK)
626+
// We need to explicitly set the number of replicas in case the source has 0 replicas and the cluster has only 1 data node
627+
.setSettings(Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build())
628+
.get();
629+
630+
// Verify that the target index has the correct @timestamp mapping
631+
final var targetMappings = indicesAdmin().prepareGetMappings("target").get();
632+
assertThat(
633+
ObjectPath.eval("[email protected]", targetMappings.mappings().get("target").getSourceAsMap()),
634+
equalTo("date_nanos")
635+
);
636+
ensureGreen();
637+
}
638+
639+
/**
640+
* Tests that shrinking a time series index with a non-default timestamp mapping doesn't result in any mapping conflicts.
641+
*/
642+
public void testShrinkTimeSeriesIndexWithNonDefaultTimestamp() {
643+
// Create a time series index with a date_nanos @timestamp field
644+
final var settings = indexSettings(2, 0).put("index.mode", "time_series")
645+
.put("index.routing_path", "sensor_id")
646+
.put("index.routing.allocation.require._name", internalCluster().getRandomDataNodeName())
647+
.put("index.blocks.write", true);
648+
prepareCreate("source").setSettings(settings)
649+
.setMapping("@timestamp", "type=date_nanos", "sensor_id", "type=keyword,time_series_dimension=true")
650+
.get();
651+
ensureGreen();
652+
653+
// Shrink the index
654+
indicesAdmin().prepareResizeIndex("source", "target")
655+
.setResizeType(ResizeType.SHRINK)
656+
// We need to explicitly set the number of replicas in case the source has 0 replicas and the cluster has only 1 data node
657+
.setSettings(Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build())
658+
.get();
659+
660+
// Verify that the target index has the correct @timestamp mapping
661+
final var targetMappings = indicesAdmin().prepareGetMappings("target").get();
662+
assertThat(
663+
ObjectPath.eval("[email protected]", targetMappings.mappings().get("target").getSourceAsMap()),
664+
equalTo("date_nanos")
665+
);
666+
ensureGreen();
667+
}
668+
611669
static void assertNoResizeSourceIndexSettings(final String index) {
612670
ClusterStateResponse clusterStateResponse = clusterAdmin().prepareState(TEST_REQUEST_TIMEOUT)
613671
.clear()

server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/SplitIndexIT.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.elasticsearch.indices.IndicesService;
4747
import org.elasticsearch.test.ESIntegTestCase;
4848
import org.elasticsearch.test.index.IndexVersionUtils;
49+
import org.elasticsearch.xcontent.ObjectPath;
4950
import org.elasticsearch.xcontent.XContentType;
5051

5152
import java.io.IOException;
@@ -493,4 +494,31 @@ public void testCreateSplitWithIndexSort() throws Exception {
493494
assertSortedSegments("target", expectedIndexSort);
494495
assertNoResizeSourceIndexSettings("target");
495496
}
497+
498+
/**
499+
* Tests that splitting a logsdb index with a non-default timestamp mapping doesn't result in any mapping conflicts.
500+
* N.B.: we don't test time_series indices as split is not supported for them.
501+
*/
502+
public void testSplitLogsdbIndexWithNonDefaultTimestamp() {
503+
// Create a logsdb index with a date_nanos @timestamp field
504+
final int numberOfReplicas = randomInt(internalCluster().numDataNodes() - 1);
505+
final var settings = indexSettings(1, numberOfReplicas).put("index.mode", "logsdb").put("index.blocks.write", true);
506+
prepareCreate("source").setSettings(settings).setMapping("@timestamp", "type=date_nanos").get();
507+
ensureGreen();
508+
509+
// Split the index
510+
indicesAdmin().prepareResizeIndex("source", "target")
511+
.setResizeType(ResizeType.SPLIT)
512+
// We need to explicitly set the number of replicas in case the source has 0 replicas and the cluster has only 1 data node
513+
.setSettings(Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", numberOfReplicas).build())
514+
.get();
515+
516+
// Verify that the target index has the correct @timestamp mapping
517+
final var targetMappings = indicesAdmin().prepareGetMappings("target").get();
518+
assertThat(
519+
ObjectPath.eval("[email protected]", targetMappings.mappings().get("target").getSourceAsMap()),
520+
equalTo("date_nanos")
521+
);
522+
ensureGreen();
523+
}
496524
}

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -512,11 +512,15 @@ private ClusterState applyCreateIndexWithTemporaryService(
512512
assert indicesService.hasIndex(temporaryIndexMeta.getIndex()) == false
513513
: Strings.format("Index [%s] already exists", temporaryIndexMeta.getIndex().getName());
514514
return indicesService.<ClusterState, Exception>withTempIndexService(temporaryIndexMeta, indexService -> {
515-
try {
516-
updateIndexMappingsAndBuildSortOrder(indexService, request, mappings, sourceMetadata);
517-
} catch (Exception e) {
518-
logger.log(silent ? Level.DEBUG : Level.INFO, "failed on parsing mappings on index creation [{}]", request.index(), e);
519-
throw e;
515+
// If we're creating the index from an existing index, we should not provide any mappings, as the new index shards will take
516+
// care of copying the mappings from the source index during recovery. Providing mappings here would cause conflicts.
517+
if (sourceMetadata == null) {
518+
try {
519+
updateIndexMappingsAndBuildSortOrder(indexService, request, mappings);
520+
} catch (Exception e) {
521+
logger.log(silent ? Level.DEBUG : Level.INFO, "failed on parsing mappings on index creation [{}]", request.index(), e);
522+
throw e;
523+
}
520524
}
521525

522526
final List<AliasMetadata> aliases = aliasSupplier.apply(indexService);
@@ -1422,8 +1426,7 @@ private static IndexMetadata.Builder createIndexMetadataBuilder(
14221426
private static void updateIndexMappingsAndBuildSortOrder(
14231427
IndexService indexService,
14241428
CreateIndexClusterStateUpdateRequest request,
1425-
List<CompressedXContent> mappings,
1426-
@Nullable IndexMetadata sourceMetadata
1429+
List<CompressedXContent> mappings
14271430
) throws IOException {
14281431
MapperService mapperService = indexService.mapperService();
14291432
IndexMode indexMode = indexService.getIndexSettings() != null ? indexService.getIndexSettings().getMode() : IndexMode.STANDARD;
@@ -1437,13 +1440,11 @@ private static void updateIndexMappingsAndBuildSortOrder(
14371440

14381441
indexMode.validateTimestampFieldMapping(request.dataStreamName() != null, mapperService.mappingLookup());
14391442

1440-
if (sourceMetadata == null) {
1441-
// now that the mapping is merged we can validate the index sort.
1442-
// we cannot validate for index shrinking since the mapping is empty
1443-
// at this point. The validation will take place later in the process
1444-
// (when all shards are copied in a single place).
1445-
indexService.getIndexSortSupplier().get();
1446-
}
1443+
// now that the mapping is merged we can validate the index sort.
1444+
// we cannot validate for index shrinking since the mapping is empty
1445+
// at this point. The validation will take place later in the process
1446+
// (when all shards are copied in a single place).
1447+
indexService.getIndexSortSupplier().get();
14471448
}
14481449

14491450
private static void validateActiveShardCount(ActiveShardCount waitForActiveShards, IndexMetadata indexMetadata) {

test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,6 +2073,13 @@ public String getRandomNodeName() {
20732073
return getNodeNameThat(Predicates.always());
20742074
}
20752075

2076+
/**
2077+
* @return the name of a random data node in a cluster
2078+
*/
2079+
public String getRandomDataNodeName() {
2080+
return getNodeNameThat(DiscoveryNode::canContainData);
2081+
}
2082+
20762083
/**
20772084
* @return the name of a random node in a cluster that match the {@code predicate}
20782085
*/

0 commit comments

Comments
 (0)