Skip to content

Commit de6f132

Browse files
authored
[7.x] Foreach processor - fork recursive call (#50514) (#50773)
A very large number of recursive calls can cause a stack overflow exception. This commit forks the recursive calls for non-async processors. Once forked, each thread will handle at most 10 recursive calls to help keep the stack size and thread count down to a reasonable size.
1 parent 1d8e51f commit de6f132

File tree

10 files changed

+213
-38
lines changed

10 files changed

+213
-38
lines changed

modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/ForEachProcessor.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@
2323
import org.elasticsearch.ingest.ConfigurationUtils;
2424
import org.elasticsearch.ingest.IngestDocument;
2525
import org.elasticsearch.ingest.Processor;
26+
import org.elasticsearch.ingest.WrappingProcessor;
27+
import org.elasticsearch.script.ScriptService;
2628

2729
import java.util.ArrayList;
2830
import java.util.List;
2931
import java.util.Map;
3032
import java.util.Set;
3133
import java.util.concurrent.CopyOnWriteArrayList;
3234
import java.util.function.BiConsumer;
33-
34-
import org.elasticsearch.ingest.WrappingProcessor;
35-
import org.elasticsearch.script.ScriptService;
35+
import java.util.function.Consumer;
3636

3737
import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException;
3838
import static org.elasticsearch.ingest.ConfigurationUtils.readBooleanProperty;
@@ -50,16 +50,19 @@
5050
public final class ForEachProcessor extends AbstractProcessor implements WrappingProcessor {
5151

5252
public static final String TYPE = "foreach";
53+
static final int MAX_RECURSE_PER_THREAD = 10;
5354

5455
private final String field;
5556
private final Processor processor;
5657
private final boolean ignoreMissing;
58+
private final Consumer<Runnable> genericExecutor;
5759

58-
ForEachProcessor(String tag, String field, Processor processor, boolean ignoreMissing) {
60+
ForEachProcessor(String tag, String field, Processor processor, boolean ignoreMissing, Consumer<Runnable> genericExecutor) {
5961
super(tag);
6062
this.field = field;
6163
this.processor = processor;
6264
this.ignoreMissing = ignoreMissing;
65+
this.genericExecutor = genericExecutor;
6366
}
6467

6568
boolean isIgnoreMissing() {
@@ -91,6 +94,7 @@ void innerExecute(int index, List<?> values, List<Object> newValues, IngestDocum
9194

9295
Object value = values.get(index);
9396
Object previousValue = document.getIngestMetadata().put("_value", value);
97+
final Thread thread = Thread.currentThread();
9498
processor.execute(document, (result, e) -> {
9599
if (e != null) {
96100
newValues.add(document.getIngestMetadata().put("_value", previousValue));
@@ -99,7 +103,15 @@ void innerExecute(int index, List<?> values, List<Object> newValues, IngestDocum
99103
handler.accept(null, null);
100104
} else {
101105
newValues.add(document.getIngestMetadata().put("_value", previousValue));
102-
innerExecute(index + 1, values, newValues, document, handler);
106+
if (thread == Thread.currentThread() && (index + 1) % MAX_RECURSE_PER_THREAD == 0) {
107+
// we are on the same thread and we need to fork to another thread to avoid recursive stack overflow on a single thread
108+
// only fork after 10 recursive calls, then fork every 10 to keep the number of threads down
109+
genericExecutor.accept(() -> innerExecute(index + 1, values, newValues, document, handler));
110+
} else {
111+
// we are on a different thread (we went asynchronous), it's safe to recurse
112+
// or we have recursed less then 10 times with the same thread, it's safe to recurse
113+
innerExecute(index + 1, values, newValues, document, handler);
114+
}
103115
}
104116
});
105117
}
@@ -125,9 +137,11 @@ public Processor getInnerProcessor() {
125137
public static final class Factory implements Processor.Factory {
126138

127139
private final ScriptService scriptService;
140+
private final Consumer<Runnable> genericExecutor;
128141

129-
Factory(ScriptService scriptService) {
142+
Factory(ScriptService scriptService, Consumer<Runnable> genericExecutor) {
130143
this.scriptService = scriptService;
144+
this.genericExecutor = genericExecutor;
131145
}
132146

133147
@Override
@@ -143,7 +157,7 @@ public ForEachProcessor create(Map<String, Processor.Factory> factories, String
143157
Map.Entry<String, Map<String, Object>> entry = entries.iterator().next();
144158
Processor processor =
145159
ConfigurationUtils.readProcessor(factories, scriptService, entry.getKey(), entry.getValue());
146-
return new ForEachProcessor(tag, field, processor, ignoreMissing);
160+
return new ForEachProcessor(tag, field, processor, ignoreMissing, genericExecutor);
147161
}
148162
}
149163
}

modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public Map<String, Processor.Factory> getProcessors(Processor.Parameters paramet
7474
processors.put(ConvertProcessor.TYPE, new ConvertProcessor.Factory());
7575
processors.put(GsubProcessor.TYPE, new GsubProcessor.Factory());
7676
processors.put(FailProcessor.TYPE, new FailProcessor.Factory(parameters.scriptService));
77-
processors.put(ForEachProcessor.TYPE, new ForEachProcessor.Factory(parameters.scriptService));
77+
processors.put(ForEachProcessor.TYPE, new ForEachProcessor.Factory(parameters.scriptService, parameters.genericExecutor));
7878
processors.put(DateIndexNameProcessor.TYPE, new DateIndexNameProcessor.Factory(parameters.scriptService));
7979
processors.put(SortProcessor.TYPE, new SortProcessor.Factory());
8080
processors.put(GrokProcessor.TYPE, new GrokProcessor.Factory(GROK_PATTERNS, createGrokThreadWatchdog(parameters)));

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,21 @@
2929
import java.util.Collections;
3030
import java.util.HashMap;
3131
import java.util.Map;
32+
import java.util.function.Consumer;
3233

3334
import static org.hamcrest.Matchers.equalTo;
3435
import static org.mockito.Mockito.mock;
3536

3637
public class ForEachProcessorFactoryTests extends ESTestCase {
3738

3839
private final ScriptService scriptService = mock(ScriptService.class);
40+
private final Consumer<Runnable> genericExecutor = Runnable::run;
3941

4042
public void testCreate() throws Exception {
4143
Processor processor = new TestProcessor(ingestDocument -> { });
4244
Map<String, Processor.Factory> registry = new HashMap<>();
4345
registry.put("_name", (r, t, c) -> processor);
44-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
46+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
4547

4648
Map<String, Object> config = new HashMap<>();
4749
config.put("field", "_field");
@@ -57,7 +59,7 @@ public void testSetIgnoreMissing() throws Exception {
5759
Processor processor = new TestProcessor(ingestDocument -> { });
5860
Map<String, Processor.Factory> registry = new HashMap<>();
5961
registry.put("_name", (r, t, c) -> processor);
60-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
62+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
6163

6264
Map<String, Object> config = new HashMap<>();
6365
config.put("field", "_field");
@@ -75,7 +77,7 @@ public void testCreateWithTooManyProcessorTypes() throws Exception {
7577
Map<String, Processor.Factory> registry = new HashMap<>();
7678
registry.put("_first", (r, t, c) -> processor);
7779
registry.put("_second", (r, t, c) -> processor);
78-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
80+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
7981

8082
Map<String, Object> config = new HashMap<>();
8183
config.put("field", "_field");
@@ -88,7 +90,7 @@ public void testCreateWithTooManyProcessorTypes() throws Exception {
8890
}
8991

9092
public void testCreateWithNonExistingProcessorType() throws Exception {
91-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
93+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
9294
Map<String, Object> config = new HashMap<>();
9395
config.put("field", "_field");
9496
config.put("processor", Collections.singletonMap("_name", Collections.emptyMap()));
@@ -101,15 +103,15 @@ public void testCreateWithMissingField() throws Exception {
101103
Processor processor = new TestProcessor(ingestDocument -> { });
102104
Map<String, Processor.Factory> registry = new HashMap<>();
103105
registry.put("_name", (r, t, c) -> processor);
104-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
106+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
105107
Map<String, Object> config = new HashMap<>();
106108
config.put("processor", Collections.singletonList(Collections.singletonMap("_name", Collections.emptyMap())));
107109
Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(registry, null, config));
108110
assertThat(exception.getMessage(), equalTo("[field] required property is missing"));
109111
}
110112

111113
public void testCreateWithMissingProcessor() {
112-
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService);
114+
ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService, genericExecutor);
113115
Map<String, Object> config = new HashMap<>();
114116
config.put("field", "_field");
115117
Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(Collections.emptyMap(), null, config));

0 commit comments

Comments
 (0)