Skip to content

Commit 633e7d8

Browse files
Ignore conflicting fields during dynamic mapping update (#114227) (#116194)
This fixes a bug when concurrently executing index requests that have different types for the same field. (cherry picked from commit 9658940) Co-authored-by: Elastic Machine <[email protected]>
1 parent 325a305 commit 633e7d8

File tree

4 files changed

+97
-0
lines changed

4 files changed

+97
-0
lines changed

docs/changelog/114227.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 114227
2+
summary: Ignore conflicting fields during dynamic mapping update
3+
area: Mapping
4+
type: bug
5+
issues:
6+
- 114228

server/src/internalClusterTest/java/org/elasticsearch/index/mapper/DynamicMappingIT.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
import static org.hamcrest.Matchers.equalTo;
6464
import static org.hamcrest.Matchers.hasKey;
6565
import static org.hamcrest.Matchers.instanceOf;
66+
import static org.hamcrest.Matchers.is;
67+
import static org.hamcrest.Matchers.oneOf;
6668

6769
public class DynamicMappingIT extends ESIntegTestCase {
6870

@@ -190,6 +192,35 @@ private Map<String, Object> indexConcurrently(int numberOfFieldsToCreate, Settin
190192
return properties;
191193
}
192194

195+
public void testConcurrentDynamicMappingsWithConflictingType() throws Throwable {
196+
int numberOfDocsToCreate = 16;
197+
indicesAdmin().prepareCreate("index").setSettings(Settings.builder()).get();
198+
ensureGreen("index");
199+
final AtomicReference<Throwable> error = new AtomicReference<>();
200+
startInParallel(numberOfDocsToCreate, i -> {
201+
try {
202+
assertEquals(
203+
DocWriteResponse.Result.CREATED,
204+
prepareIndex("index").setId(Integer.toString(i)).setSource("field" + i, 0, "field" + (i + 1), 0.1).get().getResult()
205+
);
206+
} catch (Exception e) {
207+
error.compareAndSet(null, e);
208+
}
209+
});
210+
if (error.get() != null) {
211+
throw error.get();
212+
}
213+
client().admin().indices().prepareRefresh("index").get();
214+
for (int i = 0; i < numberOfDocsToCreate; ++i) {
215+
assertTrue(client().prepareGet("index", Integer.toString(i)).get().isExists());
216+
}
217+
Map<String, Object> index = indicesAdmin().prepareGetMappings("index").get().getMappings().get("index").getSourceAsMap();
218+
for (int i = 0, j = 1; i < numberOfDocsToCreate; i++, j++) {
219+
assertThat(new WriteField("properties.field" + i + ".type", () -> index).get(null), is(oneOf("long", "float")));
220+
assertThat(new WriteField("properties.field" + j + ".type", () -> index).get(null), is(oneOf("long", "float")));
221+
}
222+
}
223+
193224
public void testPreflightCheckAvoidsMaster() throws InterruptedException, IOException {
194225
// can't use INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING nor INDEX_MAPPING_DEPTH_LIMIT_SETTING as a check here, as that is already
195226
// checked at parse time, see testTotalFieldsLimitForDynamicMappingsUpdateCheckedAtDocumentParseTime

server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
package org.elasticsearch.index.mapper;
1111

12+
import org.apache.logging.log4j.LogManager;
13+
import org.apache.logging.log4j.Logger;
1214
import org.apache.lucene.index.LeafReader;
1315
import org.apache.lucene.util.BytesRef;
1416
import org.elasticsearch.ElasticsearchParseException;
@@ -41,6 +43,7 @@
4143
import java.util.stream.Stream;
4244

4345
public class ObjectMapper extends Mapper {
46+
private static final Logger logger = LogManager.getLogger(ObjectMapper.class);
4447
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ObjectMapper.class);
4548
public static final FeatureFlag SUB_OBJECTS_AUTO_FEATURE_FLAG = new FeatureFlag("sub_objects_auto");
4649

@@ -679,6 +682,13 @@ private static Map<String, Mapper> buildMergedMappers(
679682
// replaces an existing one.
680683
if (objectMergeContext.getMapperBuilderContext().getMergeReason() == MergeReason.INDEX_TEMPLATE) {
681684
putMergedMapper(mergedMappers, mergeWithMapper);
685+
} else if (isConflictingDynamicMapping(objectMergeContext, mergeWithMapper, mergeIntoMapper)) {
686+
logger.trace(
687+
"ignoring conflicting dynamic mapping update for field={} current_type={} new_type={}",
688+
mergeIntoMapper.fullPath(),
689+
mergeIntoMapper.typeName(),
690+
mergeWithMapper.typeName()
691+
);
682692
} else {
683693
putMergedMapper(mergedMappers, mergeIntoMapper.merge(mergeWithMapper, objectMergeContext));
684694
}
@@ -687,6 +697,22 @@ private static Map<String, Mapper> buildMergedMappers(
687697
return Map.copyOf(mergedMappers);
688698
}
689699

700+
/*
701+
* We're ignoring the field if a dynamic mapping update tries to define a conflicting field type.
702+
* This is caused by another index request with a different value racing to update the mappings.
703+
* After updating the mappings, the index request will be re-tried and sees the updated mappings for this field.
704+
* The updated mappings will then be taken into account when parsing the document
705+
* (for example by coercing the value, ignore_malformed values, or failing the index request due to a type conflict).
706+
*/
707+
private static boolean isConflictingDynamicMapping(
708+
MapperMergeContext objectMergeContext,
709+
Mapper mergeWithMapper,
710+
Mapper mergeIntoMapper
711+
) {
712+
return objectMergeContext.getMapperBuilderContext().getMergeReason().isAutoUpdate()
713+
&& mergeIntoMapper.typeName().equals(mergeWithMapper.typeName()) == false;
714+
}
715+
690716
private static void putMergedMapper(Map<String, Mapper> mergedMappers, @Nullable Mapper merged) {
691717
if (merged != null) {
692718
mergedMappers.put(merged.leafName(), merged);

server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99
package org.elasticsearch.index.mapper;
1010

1111
import org.elasticsearch.index.IndexVersion;
12+
import org.elasticsearch.script.ScriptCompiler;
1213
import org.elasticsearch.test.ESTestCase;
1314

1415
import java.util.Collections;
1516
import java.util.Optional;
1617

1718
import static org.elasticsearch.index.mapper.MapperService.MergeReason.INDEX_TEMPLATE;
19+
import static org.elasticsearch.index.mapper.MapperService.MergeReason.MAPPING_AUTO_UPDATE;
20+
import static org.elasticsearch.index.mapper.MapperService.MergeReason.MAPPING_AUTO_UPDATE_PREFLIGHT;
1821
import static org.elasticsearch.index.mapper.MapperService.MergeReason.MAPPING_UPDATE;
22+
import static org.hamcrest.Matchers.equalTo;
23+
import static org.hamcrest.Matchers.instanceOf;
24+
import static org.hamcrest.Matchers.is;
1925

2026
public final class ObjectMapperMergeTests extends ESTestCase {
2127

@@ -318,6 +324,34 @@ public void testMergeSubobjectsFalseWithObject() {
318324
assertNotNull(parentMapper.getMapper("child.grandchild"));
319325
}
320326

327+
public void testConflictingDynamicUpdate() {
328+
RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Optional.empty()).add(
329+
new KeywordFieldMapper.Builder("http.status_code", IndexVersion.current())
330+
).build(MapperBuilderContext.root(false, false));
331+
RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Optional.empty()).add(
332+
new NumberFieldMapper.Builder(
333+
"http.status_code",
334+
NumberFieldMapper.NumberType.LONG,
335+
ScriptCompiler.NONE,
336+
false,
337+
true,
338+
IndexVersion.current(),
339+
null
340+
)
341+
).build(MapperBuilderContext.root(false, false));
342+
343+
MapperService.MergeReason autoUpdateMergeReason = randomFrom(MAPPING_AUTO_UPDATE, MAPPING_AUTO_UPDATE_PREFLIGHT);
344+
ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, autoUpdateMergeReason, Long.MAX_VALUE));
345+
FieldMapper httpStatusCode = (FieldMapper) merged.getMapper("http.status_code");
346+
assertThat(httpStatusCode, is(instanceOf(KeywordFieldMapper.class)));
347+
348+
IllegalArgumentException e = expectThrows(
349+
IllegalArgumentException.class,
350+
() -> mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, MAPPING_UPDATE, Long.MAX_VALUE))
351+
);
352+
assertThat(e.getMessage(), equalTo("mapper [http.status_code] cannot be changed from type [keyword] to [long]"));
353+
}
354+
321355
private static RootObjectMapper createRootSubobjectFalseLeafWithDots() {
322356
FieldMapper.Builder fieldBuilder = new KeywordFieldMapper.Builder("host.name", IndexVersion.current());
323357
FieldMapper fieldMapper = fieldBuilder.build(MapperBuilderContext.root(false, false));

0 commit comments

Comments
 (0)