-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Handle a default/request pipeline and a final pipeline with minimal additional overhead #93329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
joegallo
merged 16 commits into
elastic:main
from
joegallo:ingest-service-internal-rewrite
Jan 30, 2023
Merged
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
848ccd1
Make these variables final
joegallo 073c2ab
Drop early returns
joegallo 72ff31d
Use a document listener for all this
joegallo 822d900
Move top-level ingest stats to the top-level
joegallo 3ced88f
Move parse/generate out of innerExecute
joegallo 887ee71
Add more detail to this error message
joegallo c2fbb08
Move this logic
joegallo 02f6160
Rename and move innerExecute
joegallo ae2e3ff
Update docs/changelog/93329.yaml
joegallo 5b9d4a9
Update docs/changelog/93329.yaml
joegallo b943913
Update docs/changelog/93329.yaml
joegallo 8bd83b8
Reword the release highlights
joegallo 90ba8c5
Merge branch 'main' into ingest-service-internal-rewrite
joegallo be9d35e
This can be final
joegallo 8b60a8b
Rename these iterators for clarity
joegallo 87c64f1
Add more detail to this error message
joegallo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| pr: 93329 | ||
| summary: Handle a default/request pipeline and a final pipeline with minimal additional | ||
| overhead | ||
| area: Ingest Node | ||
| type: bug | ||
| issues: | ||
| - 92843 | ||
| - 81244 | ||
| - 93118 | ||
| highlight: | ||
| title: Speed up ingest processing with multiple pipelines | ||
| body: |- | ||
| Processing documents with both a request/default and a final | ||
| pipeline is significantly faster. | ||
|
|
||
| Rather than marshalling a document from and to json once per | ||
| pipeline, a document is now marshalled from json before any | ||
| pipelines execute and then back to json after all pipelines have | ||
| executed. | ||
| notable: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,8 +44,10 @@ | |
| import org.elasticsearch.common.bytes.BytesReference; | ||
| import org.elasticsearch.common.regex.Regex; | ||
| import org.elasticsearch.common.settings.Settings; | ||
| import org.elasticsearch.common.util.CollectionUtils; | ||
| import org.elasticsearch.common.util.concurrent.AbstractRunnable; | ||
| import org.elasticsearch.common.xcontent.XContentHelper; | ||
| import org.elasticsearch.core.Releasable; | ||
| import org.elasticsearch.core.TimeValue; | ||
| import org.elasticsearch.core.Tuple; | ||
| import org.elasticsearch.env.Environment; | ||
|
|
@@ -713,15 +715,37 @@ protected void doRun() { | |
| continue; | ||
| } | ||
|
|
||
| executePipelines( | ||
| i, | ||
| pipelines.iterator(), | ||
| hasFinalPipeline, | ||
| indexRequest, | ||
| onDropped, | ||
| onFailure, | ||
| refs.acquireListener() | ||
| ); | ||
| // start the stopwatch and acquire a ref to indicate that we're working on this document | ||
| final long startTimeInNanos = System.nanoTime(); | ||
| totalMetrics.preIngest(); | ||
| final int slot = i; | ||
| final Releasable ref = refs.acquire(); | ||
| // the document listener gives us three-way logic: a document can fail processing (1), or it can | ||
| // be successfully processed. a successfully processed document can be kept (2) or dropped (3). | ||
| final ActionListener<Boolean> documentListener = ActionListener.runAfter(new ActionListener<>() { | ||
| @Override | ||
| public void onResponse(Boolean kept) { | ||
| assert kept != null; | ||
| if (kept == false) { | ||
| onDropped.accept(slot); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onFailure(Exception e) { | ||
| totalMetrics.ingestFailed(); | ||
| onFailure.accept(slot, e); | ||
| } | ||
| }, () -> { | ||
| // regardless of success or failure, we always stop the ingest "stopwatch" and release the ref to indicate | ||
| // that we're finished with this document | ||
| final long ingestTimeInNanos = System.nanoTime() - startTimeInNanos; | ||
| totalMetrics.postIngest(ingestTimeInNanos); | ||
| ref.close(); | ||
| }); | ||
|
|
||
| IngestDocument ingestDocument = newIngestDocument(indexRequest); | ||
| executePipelines(pipelines.iterator(), hasFinalPipeline, indexRequest, ingestDocument, documentListener); | ||
|
|
||
| i++; | ||
| } | ||
|
|
@@ -731,30 +755,25 @@ protected void doRun() { | |
| } | ||
|
|
||
| private void executePipelines( | ||
| final int slot, | ||
| final Iterator<String> it, | ||
| final boolean hasFinalPipeline, | ||
| final IndexRequest indexRequest, | ||
| final IntConsumer onDropped, | ||
| final BiConsumer<Integer, Exception> onFailure, | ||
| final ActionListener<Void> onFinished | ||
| final IngestDocument ingestDocument, | ||
| final ActionListener<Boolean> listener | ||
| ) { | ||
| assert it.hasNext(); | ||
| final String pipelineId = it.next(); | ||
| try { | ||
| PipelineHolder holder = pipelines.get(pipelineId); | ||
| final PipelineHolder holder = pipelines.get(pipelineId); | ||
| if (holder == null) { | ||
| throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist"); | ||
| } | ||
| Pipeline pipeline = holder.pipeline; | ||
| String originalIndex = indexRequest.indices()[0]; | ||
| long startTimeInNanos = System.nanoTime(); | ||
| totalMetrics.preIngest(); | ||
| innerExecute(slot, indexRequest, pipeline, onDropped, e -> { | ||
| long ingestTimeInNanos = System.nanoTime() - startTimeInNanos; | ||
| totalMetrics.postIngest(ingestTimeInNanos); | ||
| final Pipeline pipeline = holder.pipeline; | ||
| final String originalIndex = indexRequest.indices()[0]; | ||
| executePipeline(ingestDocument, pipeline, (keep, e) -> { | ||
| assert keep != null; | ||
|
|
||
| if (e != null) { | ||
| totalMetrics.ingestFailed(); | ||
| logger.debug( | ||
| () -> format( | ||
| "failed to execute pipeline [%s] for document [%s/%s]", | ||
|
|
@@ -764,10 +783,38 @@ private void executePipelines( | |
| ), | ||
| e | ||
| ); | ||
| onFailure.accept(slot, e); | ||
| // document failed! no further processing of this doc | ||
| onFinished.onResponse(null); | ||
| return; | ||
| listener.onFailure(e); | ||
| return; // document failed! | ||
| } | ||
|
|
||
| if (keep == false) { | ||
| listener.onResponse(false); | ||
| return; // document dropped! | ||
| } | ||
|
|
||
| // update the index request so that we can execute additional pipelines (if any), etc | ||
| updateIndexRequestMetadata(indexRequest, ingestDocument.getMetadata()); | ||
| try { | ||
| // check for self-references if necessary, (i.e. if a script processor has run), and clear the bit | ||
| if (ingestDocument.doNoSelfReferencesCheck()) { | ||
| CollectionUtils.ensureNoSelfReferences(ingestDocument.getSource(), null); | ||
| ingestDocument.doNoSelfReferencesCheck(false); | ||
| } | ||
| } catch (IllegalArgumentException ex) { | ||
| // An IllegalArgumentException can be thrown when an ingest processor creates a source map that is self-referencing. | ||
| // In that case, we catch and wrap the exception, so we can include more details | ||
| listener.onFailure( | ||
| new IllegalArgumentException( | ||
| format( | ||
| "Failed to generate the source document for ingest pipeline [%s] for document [%s/%s]", | ||
| pipelineId, | ||
| indexRequest.index(), | ||
| indexRequest.id() | ||
| ), | ||
| ex | ||
| ) | ||
| ); | ||
| return; // document failed! | ||
| } | ||
|
|
||
| Iterator<String> newIt = it; | ||
|
|
@@ -776,14 +823,8 @@ private void executePipelines( | |
|
|
||
| if (Objects.equals(originalIndex, newIndex) == false) { | ||
| if (hasFinalPipeline && it.hasNext() == false) { | ||
| totalMetrics.ingestFailed(); | ||
| onFailure.accept( | ||
| slot, | ||
| new IllegalStateException("final pipeline [" + pipelineId + "] can't change the target index") | ||
| ); | ||
| // document failed! no further processing of this doc | ||
| onFinished.onResponse(null); | ||
| return; | ||
| listener.onFailure(new IllegalStateException("final pipeline [" + pipelineId + "] can't change the target index")); | ||
| return; // document failed! | ||
| } else { | ||
| indexRequest.isPipelineResolved(false); | ||
| resolvePipelines(null, indexRequest, state.metadata()); | ||
|
|
@@ -797,21 +838,40 @@ private void executePipelines( | |
| } | ||
|
|
||
| if (newIt.hasNext()) { | ||
| executePipelines(slot, newIt, newHasFinalPipeline, indexRequest, onDropped, onFailure, onFinished); | ||
| executePipelines(newIt, newHasFinalPipeline, indexRequest, ingestDocument, listener); | ||
| } else { | ||
| onFinished.onResponse(null); | ||
| // update the index request's source and (potentially) cache the timestamp for TSDB | ||
| updateIndexRequestSource(indexRequest, ingestDocument); | ||
| cacheRawTimestamp(indexRequest, ingestDocument); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 nice work |
||
| listener.onResponse(true); // document succeeded! | ||
| } | ||
| }); | ||
| } catch (Exception e) { | ||
| logger.debug( | ||
| () -> format("failed to execute pipeline [%s] for document [%s/%s]", pipelineId, indexRequest.index(), indexRequest.id()), | ||
| e | ||
| ); | ||
| onFailure.accept(slot, e); | ||
| onFinished.onResponse(null); | ||
| listener.onFailure(e); // document failed! | ||
| } | ||
| } | ||
|
|
||
| private void executePipeline( | ||
| final IngestDocument ingestDocument, | ||
| final Pipeline pipeline, | ||
| final BiConsumer<Boolean, Exception> handler | ||
| ) { | ||
| // adapt our {@code BiConsumer<Boolean, Exception>} handler shape to the | ||
| // {@code BiConsumer<IngestDocument, Exception>} handler shape used internally | ||
| // by ingest pipelines and processors | ||
| ingestDocument.executePipeline(pipeline, (result, e) -> { | ||
| if (e != null) { | ||
| handler.accept(true, e); | ||
| } else { | ||
| handler.accept(result != null, null); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| public IngestStats stats() { | ||
| IngestStats.Builder statsBuilder = new IngestStats.Builder(); | ||
| statsBuilder.addTotalMetrics(totalMetrics); | ||
|
|
@@ -863,56 +923,6 @@ static String getProcessorName(Processor processor) { | |
| return sb.toString(); | ||
| } | ||
|
|
||
| private void innerExecute( | ||
| final int slot, | ||
| final IndexRequest indexRequest, | ||
| final Pipeline pipeline, | ||
| final IntConsumer itemDroppedHandler, | ||
| final Consumer<Exception> handler | ||
| ) { | ||
| if (pipeline.getProcessors().isEmpty()) { | ||
| handler.accept(null); | ||
| return; | ||
| } | ||
|
|
||
| IngestDocument ingestDocument = newIngestDocument(indexRequest); | ||
| ingestDocument.executePipeline(pipeline, (result, e) -> { | ||
| if (e != null) { | ||
| handler.accept(e); | ||
| } else if (result == null) { | ||
| itemDroppedHandler.accept(slot); | ||
| handler.accept(null); | ||
| } else { | ||
| updateIndexRequestMetadata(indexRequest, ingestDocument.getMetadata()); | ||
| try { | ||
| updateIndexRequestSource(indexRequest, ingestDocument); | ||
| } catch (IllegalArgumentException ex) { | ||
| // An IllegalArgumentException can be thrown when an ingest processor creates a source map that is self-referencing. | ||
| // In that case, we catch and wrap the exception, so we can include which pipeline failed. | ||
| handler.accept( | ||
| new IllegalArgumentException( | ||
| "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", | ||
| ex | ||
| ) | ||
| ); | ||
| return; | ||
| } catch (Exception ex) { | ||
| // If anything goes wrong here, we want to know, and cannot proceed with normal execution. For example, | ||
| // *rarely*, a ConcurrentModificationException could be thrown if a pipeline leaks a reference to a shared mutable | ||
| // collection, and another indexing thread modifies the shared reference while we're trying to ensure it has | ||
| // no self references. | ||
| handler.accept( | ||
| new RuntimeException("Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", ex) | ||
| ); | ||
| return; | ||
| } | ||
| cacheRawTimestamp(indexRequest, ingestDocument); | ||
|
|
||
| handler.accept(null); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Builds a new ingest document from the passed-in index request. | ||
| */ | ||
|
|
@@ -960,6 +970,9 @@ private static void updateIndexRequestMetadata(final IndexRequest request, final | |
| */ | ||
| private static void updateIndexRequestSource(final IndexRequest request, final IngestDocument document) { | ||
| boolean ensureNoSelfReferences = document.doNoSelfReferencesCheck(); | ||
| // we already check for self references elsewhere (and clear the bit), so this should always be false, | ||
| // keeping the check and assert as a guard against extraordinarily surprising circumstances | ||
| assert ensureNoSelfReferences == false; | ||
| request.source(document.getSource(), request.getContentType(), ensureNoSelfReferences); | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this going to potentially confuse people who are looking at ingest metrics? I would think it would be a pretty rare case -- how much does this optimization save us? Is it worth the potential future "why are my metrics wrong?" support tickets?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be wrong about the metrics?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note that this is the same logic as before, it's just in a slightly different place. (See c2fbb08 for the deets.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I don't know what would be wrong about the metrics -- I thought I had traced where this would impact them last week, but now I have no idea what I was seeing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, it's certainly a valid question to ask and we are indeed in a maze of twisty passages all alike. 😄