Skip to content

Commit 5e662c5

Browse files
authored
Optimize IngestDocMetadata isAvailable (#120753)
1 parent 856a4d7 commit 5e662c5

File tree

10 files changed

+92
-121
lines changed

10 files changed

+92
-121
lines changed

docs/changelog/120753.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120753
2+
summary: Optimize `IngestDocMetadata` `isAvailable`
3+
area: Ingest Node
4+
type: enhancement
5+
issues: []

modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorTests.java

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.elasticsearch.ingest.RandomDocumentPicks;
1515
import org.elasticsearch.ingest.TestIngestDocument;
1616
import org.elasticsearch.ingest.TestTemplateService;
17-
import org.elasticsearch.script.Metadata;
1817
import org.elasticsearch.test.ESTestCase;
1918

2019
import java.util.ArrayList;
@@ -140,42 +139,40 @@ public void testRenameExistingFieldNullValue() throws Exception {
140139

141140
public void testRenameAtomicOperationSetFails() throws Exception {
142141
Map<String, Object> metadata = new HashMap<>();
143-
metadata.put("list", List.of("item"));
144-
145-
IngestDocument ingestDocument = TestIngestDocument.ofMetadataWithValidator(
146-
metadata,
147-
Map.of("new_field", new Metadata.FieldProperty<>(Object.class, true, true, (k, v) -> {
148-
if (v != null) {
149-
throw new UnsupportedOperationException();
150-
}
151-
}), "list", new Metadata.FieldProperty<>(Object.class, true, true, null))
152-
);
153-
Processor processor = createRenameProcessor("list", "new_field", false, false);
142+
metadata.put("_index", "foobar");
143+
144+
IngestDocument ingestDocument = TestIngestDocument.withDefaultVersion(metadata);
145+
Processor processor = createRenameProcessor("_index", "_version_type", false, false);
154146
try {
155147
processor.execute(ingestDocument);
156148
fail("processor execute should have failed");
157-
} catch (UnsupportedOperationException e) {
149+
} catch (IllegalArgumentException e) {
158150
// the set failed, the old field has not been removed
159-
assertThat(ingestDocument.getSourceAndMetadata().containsKey("list"), equalTo(true));
160-
assertThat(ingestDocument.getSourceAndMetadata().containsKey("new_field"), equalTo(false));
151+
assertThat(
152+
e.getMessage(),
153+
equalTo(
154+
"_version_type must be a null or one of [internal, external, external_gte] "
155+
+ "but was [foobar] with type [java.lang.String]"
156+
)
157+
);
158+
assertThat(ingestDocument.getSourceAndMetadata().containsKey("_index"), equalTo(true));
159+
assertThat(ingestDocument.getSourceAndMetadata().containsKey("_version_type"), equalTo(false));
161160
}
162161
}
163162

164163
public void testRenameAtomicOperationRemoveFails() throws Exception {
165164
Map<String, Object> metadata = new HashMap<>();
166-
metadata.put("list", List.of("item"));
165+
metadata.put("foo", "bar");
167166

168-
IngestDocument ingestDocument = TestIngestDocument.ofMetadataWithValidator(
169-
metadata,
170-
Map.of("list", new Metadata.FieldProperty<>(Object.class, false, true, null))
171-
);
172-
Processor processor = createRenameProcessor("list", "new_field", false, false);
167+
IngestDocument ingestDocument = TestIngestDocument.withDefaultVersion(metadata);
168+
Processor processor = createRenameProcessor("_version", "new_field", false, false);
173169
try {
174170
processor.execute(ingestDocument);
175171
fail("processor execute should have failed");
176172
} catch (IllegalArgumentException e) {
177-
// the set failed, the old field has not been removed
178-
assertThat(ingestDocument.getSourceAndMetadata().containsKey("list"), equalTo(true));
173+
// the remove failed, the old field has not been removed
174+
assertThat(e.getMessage(), equalTo("_version cannot be removed"));
175+
assertThat(ingestDocument.getSourceAndMetadata().containsKey("_version"), equalTo(true));
179176
assertThat(ingestDocument.getSourceAndMetadata().containsKey("new_field"), equalTo(false));
180177
}
181178
}

server/src/main/java/org/elasticsearch/ingest/IngestCtxMap.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*
3030
* The map is expected to be used by processors, server code should the typed getter and setters where possible.
3131
*/
32-
class IngestCtxMap extends CtxMap<IngestDocMetadata> {
32+
final class IngestCtxMap extends CtxMap<IngestDocMetadata> {
3333

3434
/**
3535
* Create an IngestCtxMap with the given metadata, source and default validators

server/src/main/java/org/elasticsearch/ingest/IngestDocMetadata.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.Map;
2020
import java.util.stream.Collectors;
2121

22-
class IngestDocMetadata extends Metadata {
22+
final class IngestDocMetadata extends Metadata {
2323

2424
static final Map<String, FieldProperty<?>> PROPERTIES = Map.of(
2525
INDEX,
@@ -42,18 +42,25 @@ class IngestDocMetadata extends Metadata {
4242
new FieldProperty<>(Map.class).withWritable().withNullable()
4343
);
4444

45+
private static final char UNDERSCORE = '_';
46+
static {
47+
// there's an optimization here in the overridden isAvailable below, but it only works if the first character of each of these
48+
// keys starts with an underscore, since we know all the keys up front, though, we can just make sure that's always true
49+
for (String key : PROPERTIES.keySet()) {
50+
if (key.charAt(0) != UNDERSCORE) {
51+
throw new IllegalArgumentException("IngestDocMetadata keys must begin with an underscore, but found [" + key + "]");
52+
}
53+
}
54+
}
55+
4556
protected final ZonedDateTime timestamp;
4657

4758
IngestDocMetadata(String index, String id, long version, String routing, VersionType versionType, ZonedDateTime timestamp) {
4859
this(metadataMap(index, id, version, routing, versionType), timestamp);
4960
}
5061

5162
IngestDocMetadata(Map<String, Object> metadata, ZonedDateTime timestamp) {
52-
this(metadata, PROPERTIES, timestamp);
53-
}
54-
55-
IngestDocMetadata(Map<String, Object> metadata, Map<String, FieldProperty<?>> properties, ZonedDateTime timestamp) {
56-
super(metadata, properties);
63+
super(metadata, PROPERTIES);
5764
this.timestamp = timestamp;
5865
}
5966

@@ -100,4 +107,16 @@ private static void versionTypeValidator(String key, String value) {
100107
+ "]"
101108
);
102109
}
110+
111+
@Override
112+
public boolean isAvailable(String key) {
113+
// the key cannot be null or empty because of the nature of the calling code, and this is already validated in IngestDocument
114+
assert key != null && key.isEmpty() == false;
115+
// we can avoid a map lookup on most keys since we know that the only keys that are 'metadata keys' for an ingest document
116+
// must be keys that start with an underscore
117+
if (key.charAt(0) != UNDERSCORE) {
118+
return false;
119+
}
120+
return super.isAvailable(key);
121+
}
103122
}

server/src/main/java/org/elasticsearch/ingest/IngestService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,8 +1204,9 @@ private static void updateIndexRequestMetadata(final IndexRequest request, final
12041204
request.id(metadata.getId());
12051205
request.routing(metadata.getRouting());
12061206
request.version(metadata.getVersion());
1207-
if (metadata.getVersionType() != null) {
1208-
request.versionType(VersionType.fromString(metadata.getVersionType()));
1207+
String versionType;
1208+
if ((versionType = metadata.getVersionType()) != null) {
1209+
request.versionType(VersionType.fromString(versionType));
12091210
}
12101211
Number number;
12111212
if ((number = metadata.getIfSeqNo()) != null) {

server/src/main/java/org/elasticsearch/script/CtxMap.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.AbstractCollection;
1515
import java.util.AbstractMap;
1616
import java.util.AbstractSet;
17+
import java.util.ArrayList;
1718
import java.util.Collection;
1819
import java.util.HashSet;
1920
import java.util.Iterator;
@@ -150,10 +151,12 @@ public Object remove(Object key) {
150151
@Override
151152
public void clear() {
152153
// AbstractMap uses entrySet().clear(), it should be quicker to run through the validators, then call the wrapped maps clear
153-
for (String key : metadata.keySet()) {
154+
for (String key : new ArrayList<>(metadata.keySet())) { // copy the key set to get around the ConcurrentModificationException
154155
metadata.remove(key);
155156
}
156-
// TODO: this is just bogus, there isn't any case where metadata won't trip a failure above?
157+
// note: this is actually bogus in the general case, though! for this to work there must be some Metadata or subclass of Metadata
158+
// for which all the FieldPoperty properties of the metadata are nullable and therefore could have been removed in the previous
159+
// loop -- does such a class even exist? (that is, is there any *real* CtxMap for which the previous loop didn't throw?)
157160
source.clear();
158161
}
159162

server/src/test/java/org/elasticsearch/ingest/IngestCtxMapTests.java

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void testRemoveSource() {
110110
source.put("abc", 123);
111111
source.put("def", 456);
112112
source.put("hij", 789);
113-
map = new IngestCtxMap(source, new TestIngestCtxMetadata(new HashMap<>(), new HashMap<>()));
113+
map = new IngestCtxMap(source, new IngestDocMetadata(new HashMap<>(Map.of("_version", 1L)), null));
114114

115115
// Make sure there isn't a ConcurrentModificationException when removing a key from the iterator
116116
String removedKey = null;
@@ -129,31 +129,18 @@ public void testRemoveSource() {
129129
}
130130

131131
public void testRemove() {
132-
String cannotRemove = "cannotRemove";
133-
String canRemove = "canRemove";
134-
Map<String, Object> metadata = new HashMap<>();
135-
metadata.put(cannotRemove, "value");
136-
map = new IngestCtxMap(
137-
new HashMap<>(),
138-
new TestIngestCtxMetadata(
139-
metadata,
140-
Map.of(
141-
cannotRemove,
142-
new Metadata.FieldProperty<>(String.class, false, true, null),
143-
canRemove,
144-
new Metadata.FieldProperty<>(String.class, true, true, null)
145-
)
146-
)
147-
);
148-
String msg = "cannotRemove cannot be removed";
132+
String cannotRemove = "_version"; // writable, but not *nullable*
133+
String canRemove = "_id"; // writable, and *nullable*
134+
map = new IngestCtxMap(new HashMap<>(), new IngestDocMetadata(new HashMap<>(Map.of(cannotRemove, 1L)), null));
135+
String msg = "_version cannot be removed";
149136
IllegalArgumentException err = expectThrows(IllegalArgumentException.class, () -> map.remove(cannotRemove));
150137
assertEquals(msg, err.getMessage());
151138

152139
err = expectThrows(IllegalArgumentException.class, () -> map.put(cannotRemove, null));
153-
assertEquals("cannotRemove cannot be null", err.getMessage());
140+
assertEquals("_version cannot be null", err.getMessage());
154141

155142
err = expectThrows(IllegalArgumentException.class, () -> map.entrySet().iterator().next().setValue(null));
156-
assertEquals("cannotRemove cannot be null", err.getMessage());
143+
assertEquals("_version cannot be null", err.getMessage());
157144

158145
err = expectThrows(IllegalArgumentException.class, () -> {
159146
Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
@@ -176,6 +163,10 @@ public void testRemove() {
176163
err = expectThrows(IllegalArgumentException.class, () -> map.clear());
177164
assertEquals(msg, err.getMessage());
178165

166+
// depending on iteration order, this may have been removed, so put it back before checking the size
167+
map.put(canRemove, "value");
168+
assertEquals("value", map.get(canRemove));
169+
179170
assertEquals(2, map.size());
180171

181172
map.entrySet().remove(new TestEntry(canRemove, "value"));
@@ -205,7 +196,7 @@ public void testEntryAndIterator() {
205196
source.put("foo", "bar");
206197
source.put("baz", "qux");
207198
source.put("noz", "zon");
208-
map = new IngestCtxMap(source, TestIngestCtxMetadata.withNullableVersion(metadata));
199+
map = new IngestCtxMap(source, new IngestDocMetadata(metadata, null));
209200
md = map.getMetadata();
210201

211202
for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -240,8 +231,10 @@ public void testEntryAndIterator() {
240231
assertTrue(map.containsKey("noz"));
241232
assertEquals(3, map.entrySet().size());
242233
assertEquals(3, map.size());
243-
map.clear();
244-
assertEquals(0, map.size());
234+
235+
// since an IngestCtxMap must have a _version (and the _version cannot be null), we can't just .clear()
236+
map.entrySet().removeIf(e -> e.getKey().equals("_version") == false);
237+
assertEquals(1, map.size());
245238
}
246239

247240
public void testContainsValue() {

test/framework/src/main/java/org/elasticsearch/ingest/TestIngestCtxMetadata.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

test/framework/src/main/java/org/elasticsearch/ingest/TestIngestDocument.java

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
package org.elasticsearch.ingest;
1111

1212
import org.elasticsearch.common.lucene.uid.Versions;
13-
import org.elasticsearch.common.util.Maps;
1413
import org.elasticsearch.core.Tuple;
1514
import org.elasticsearch.index.VersionType;
16-
import org.elasticsearch.script.Metadata;
1715
import org.elasticsearch.test.ESTestCase;
1816

1917
import java.util.HashMap;
@@ -24,53 +22,30 @@
2422
*/
2523
public class TestIngestDocument {
2624
public static final long DEFAULT_VERSION = 12345L;
27-
private static String VERSION = IngestDocument.Metadata.VERSION.getFieldName();
28-
29-
/**
30-
* Create an IngestDocument for testing that pass an empty mutable map for ingestMetaata
31-
*/
32-
public static IngestDocument withNullableVersion(Map<String, Object> sourceAndMetadata) {
33-
return ofIngestWithNullableVersion(sourceAndMetadata, new HashMap<>());
34-
}
25+
private static final String VERSION = IngestDocument.Metadata.VERSION.getFieldName();
3526

3627
/**
3728
* Create an {@link IngestDocument} from the given sourceAndMetadata and ingestMetadata and a version validator that allows null
3829
* _versions. Normally null _version is not allowed, but many tests don't care about that invariant.
3930
*/
40-
public static IngestDocument ofIngestWithNullableVersion(Map<String, Object> sourceAndMetadata, Map<String, Object> ingestMetadata) {
41-
Map<String, Object> source = new HashMap<>(sourceAndMetadata);
42-
Map<String, Object> metadata = Maps.newHashMapWithExpectedSize(IngestDocument.Metadata.values().length);
43-
for (IngestDocument.Metadata m : IngestDocument.Metadata.values()) {
44-
String key = m.getFieldName();
45-
if (sourceAndMetadata.containsKey(key)) {
46-
metadata.put(key, source.remove(key));
47-
}
48-
}
49-
return new IngestDocument(new IngestCtxMap(source, TestIngestCtxMetadata.withNullableVersion(metadata)), ingestMetadata);
50-
}
51-
52-
/**
53-
* Create an {@link IngestDocument} with {@link #DEFAULT_VERSION} as the _version metadata, if _version is not already present.
54-
*/
55-
public static IngestDocument withDefaultVersion(Map<String, Object> sourceAndMetadata) {
31+
public static IngestDocument withDefaultVersion(Map<String, Object> sourceAndMetadata, Map<String, Object> ingestMetadata) {
5632
if (sourceAndMetadata.containsKey(VERSION) == false) {
5733
sourceAndMetadata = new HashMap<>(sourceAndMetadata);
5834
sourceAndMetadata.put(VERSION, DEFAULT_VERSION);
5935
}
60-
return new IngestDocument(sourceAndMetadata, new HashMap<>());
36+
return new IngestDocument(sourceAndMetadata, ingestMetadata);
6137
}
6238

6339
/**
64-
* Create an IngestDocument with a metadata map and validators. The metadata map is passed by reference, not copied, so callers
65-
* can observe changes to the map directly.
40+
* Create an {@link IngestDocument} with {@link #DEFAULT_VERSION} as the _version metadata, if _version is not already present.
6641
*/
67-
public static IngestDocument ofMetadataWithValidator(Map<String, Object> metadata, Map<String, Metadata.FieldProperty<?>> properties) {
68-
return new IngestDocument(new IngestCtxMap(new HashMap<>(), new TestIngestCtxMetadata(metadata, properties)), new HashMap<>());
42+
public static IngestDocument withDefaultVersion(Map<String, Object> sourceAndMetadata) {
43+
return withDefaultVersion(sourceAndMetadata, new HashMap<>());
6944
}
7045

7146
/**
7247
* Create an empty ingest document for testing.
73-
*
48+
* <p>
7449
* Adds the required {@code "_version"} metadata key with value {@link #DEFAULT_VERSION}.
7550
*/
7651
public static IngestDocument emptyIngestDocument() {

0 commit comments

Comments
 (0)